365 lines
13 KiB
PHP
365 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Backoffice;
|
|
|
|
use App\Models\BackofficeStatisticsSnapshot;
|
|
use App\Models\UserAbo;
|
|
use App\Models\UserSalesVolume;
|
|
use App\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Collection;
|
|
|
|
class BackofficeDashboardService
|
|
{
|
|
private const MAX_DEPTH_SAFETY_LIMIT = 100;
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
public function metricLabels(): array
|
|
{
|
|
return [
|
|
'consultants' => 'Berater',
|
|
'new_partners' => 'Neupartner',
|
|
'team_partner_abos' => 'Teamabos',
|
|
'team_customer_abos' => 'Teamkundenabos',
|
|
'own_points' => 'Eigenpunkte',
|
|
'external_points' => 'Externe Kundenpunkte',
|
|
'customer_abo_points' => 'Kundenabo-Punkte',
|
|
'customer_single_order_points' => 'Einzelbestellungs-Punkte',
|
|
'customer_other_points' => 'Sonstige Kundenpunkte',
|
|
'total_points' => 'Gesamtpunkte',
|
|
'shop_1000' => '1000 Punkte Shop',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{month: int, year: int, metric_labels: array<string, string>, lines: array<int, array<string, mixed>>, totals: array<string, mixed>}
|
|
*/
|
|
public function overview(User $user, int $month, int $year): array
|
|
{
|
|
if ($this->isClosedMonth($month, $year)) {
|
|
$snapshot = BackofficeStatisticsSnapshot::query()
|
|
->where('user_id', $user->id)
|
|
->where('month', $month)
|
|
->where('year', $year)
|
|
->first();
|
|
|
|
if ($snapshot) {
|
|
return $this->withMeta($snapshot->payload, 'snapshot', $snapshot->calculated_at?->format('d.m.Y H:i'));
|
|
}
|
|
}
|
|
|
|
return $this->withMeta($this->buildOverview($user, $month, $year), 'live');
|
|
}
|
|
|
|
public function storeSnapshot(User $user, int $month, int $year): BackofficeStatisticsSnapshot
|
|
{
|
|
$payload = $this->buildOverview($user, $month, $year);
|
|
|
|
return BackofficeStatisticsSnapshot::query()->updateOrCreate(
|
|
[
|
|
'user_id' => $user->id,
|
|
'year' => $year,
|
|
'month' => $month,
|
|
],
|
|
[
|
|
'payload' => $payload,
|
|
'calculated_at' => now(),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array{month: int, year: int, metric_labels: array<string, string>, lines: array<int, array<string, mixed>>, totals: array<string, mixed>}
|
|
*/
|
|
private function buildOverview(User $user, int $month, int $year): array
|
|
{
|
|
$lineBuckets = $this->lineBuckets($user->id);
|
|
$lines = [];
|
|
$totals = $this->emptyLine(0, []);
|
|
|
|
foreach ($lineBuckets as $line => $users) {
|
|
$row = $this->buildLineRow($line, $users, $month, $year);
|
|
$lines[$line] = $row;
|
|
$totals = $this->addToTotals($totals, $row);
|
|
}
|
|
|
|
$totals['label'] = 'Summe';
|
|
|
|
return [
|
|
'month' => $month,
|
|
'year' => $year,
|
|
'metric_labels' => $this->metricLabels(),
|
|
'lines' => $lines,
|
|
'totals' => $totals,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $overview
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function withMeta(array $overview, string $source, ?string $calculatedAt = null): array
|
|
{
|
|
$overview['_meta'] = [
|
|
'source' => $source,
|
|
'source_label' => $source === 'snapshot' ? 'Snapshot' : 'Live',
|
|
'calculated_at' => $calculatedAt,
|
|
];
|
|
|
|
return $overview;
|
|
}
|
|
|
|
private function isClosedMonth(int $month, int $year): bool
|
|
{
|
|
return Carbon::create($year, $month, 1)->endOfMonth()->lt(now()->startOfMonth());
|
|
}
|
|
|
|
/**
|
|
* @return array<int, \Illuminate\Support\Collection<int, \App\User>>
|
|
*/
|
|
public function lineBuckets(int $rootUserId): array
|
|
{
|
|
$lineBuckets = [];
|
|
$currentSponsorIds = [$rootUserId];
|
|
$visitedUserIds = [$rootUserId];
|
|
|
|
for ($line = 1; $line <= self::MAX_DEPTH_SAFETY_LIMIT && $currentSponsorIds !== []; $line++) {
|
|
$users = User::query()
|
|
->with('account')
|
|
->whereIn('m_sponsor', $currentSponsorIds)
|
|
->whereNotIn('id', $visitedUserIds)
|
|
->whereColumn('id', '!=', 'm_sponsor')
|
|
->whereNull('deleted_at')
|
|
->get();
|
|
|
|
if ($users->isEmpty()) {
|
|
break;
|
|
}
|
|
|
|
$lineBuckets[$line] = $users;
|
|
$currentSponsorIds = $users->pluck('id')->all();
|
|
$visitedUserIds = array_merge($visitedUserIds, $currentSponsorIds);
|
|
}
|
|
|
|
return $lineBuckets;
|
|
}
|
|
|
|
/**
|
|
* @return int[]
|
|
*/
|
|
public function lineUserIds(int $rootUserId, int $line): array
|
|
{
|
|
if ($line === 0) {
|
|
return $this->teamUserIds($rootUserId);
|
|
}
|
|
|
|
if ($line < 1) {
|
|
return [];
|
|
}
|
|
|
|
return ($this->lineBuckets($rootUserId)[$line] ?? collect())->pluck('id')->map(fn ($id) => (int) $id)->all();
|
|
}
|
|
|
|
/**
|
|
* @return int[]
|
|
*/
|
|
public function teamUserIds(int $rootUserId): array
|
|
{
|
|
return collect($this->lineBuckets($rootUserId))
|
|
->flatMap(fn (Collection $users) => $users->pluck('id'))
|
|
->map(fn ($id) => (int) $id)
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param \Illuminate\Support\Collection<int, \App\User> $users
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function buildLineRow(int $line, Collection $users, int $month, int $year): array
|
|
{
|
|
$userIds = $users->pluck('id')->map(fn ($id) => (int) $id)->all();
|
|
|
|
if ($userIds === []) {
|
|
return $this->emptyLine($line, $userIds);
|
|
}
|
|
|
|
$salesSummary = $this->salesSummary($userIds, $month, $year);
|
|
|
|
return [
|
|
'line' => $line,
|
|
'label' => 'Linie '.$line,
|
|
'user_ids' => $userIds,
|
|
'consultants' => $this->activeConsultants($users),
|
|
'new_partners' => $this->newPartners($users, $month, $year),
|
|
'team_partner_abos' => $this->activeAboQuery()->whereIn('user_id', $userIds)->where('is_for', 'me')->count(),
|
|
'team_partner_abos_new' => $this->newAboCount($userIds, 'user_id', 'me', $month, $year),
|
|
'team_customer_abos' => $this->activeAboQuery()->whereIn('member_id', $userIds)->where('is_for', 'ot')->count(),
|
|
'team_customer_abos_new' => $this->newAboCount($userIds, 'member_id', 'ot', $month, $year),
|
|
'own_points' => $salesSummary['own_points'],
|
|
'external_points' => $salesSummary['external_points'],
|
|
'customer_abo_points' => $salesSummary['customer_abo_points'],
|
|
'customer_single_order_points' => $salesSummary['customer_single_order_points'],
|
|
'customer_other_points' => $salesSummary['customer_other_points'],
|
|
'total_points' => $salesSummary['total_points'],
|
|
'turnover_net' => $salesSummary['turnover_net'],
|
|
'shop_1000' => $this->shop1000Count($userIds, $month, $year),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function emptyLine(int $line, array $userIds): array
|
|
{
|
|
return [
|
|
'line' => $line,
|
|
'label' => $line > 0 ? 'Linie '.$line : 'Summe',
|
|
'user_ids' => $userIds,
|
|
'consultants' => 0,
|
|
'new_partners' => 0,
|
|
'team_partner_abos' => 0,
|
|
'team_partner_abos_new' => 0,
|
|
'team_customer_abos' => 0,
|
|
'team_customer_abos_new' => 0,
|
|
'own_points' => 0.0,
|
|
'external_points' => 0.0,
|
|
'customer_abo_points' => 0.0,
|
|
'customer_single_order_points' => 0.0,
|
|
'customer_other_points' => 0.0,
|
|
'total_points' => 0.0,
|
|
'turnover_net' => 0.0,
|
|
'shop_1000' => 0,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $totals
|
|
* @param array<string, mixed> $row
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function addToTotals(array $totals, array $row): array
|
|
{
|
|
foreach (array_keys($this->metricLabels()) as $metric) {
|
|
$totals[$metric] += $row[$metric];
|
|
}
|
|
|
|
$totals['turnover_net'] += $row['turnover_net'];
|
|
$totals['team_partner_abos_new'] += $row['team_partner_abos_new'];
|
|
$totals['team_customer_abos_new'] += $row['team_customer_abos_new'];
|
|
$totals['user_ids'] = array_merge($totals['user_ids'], $row['user_ids']);
|
|
|
|
return $totals;
|
|
}
|
|
|
|
/**
|
|
* @param \Illuminate\Support\Collection<int, \App\User> $users
|
|
*/
|
|
private function activeConsultants(Collection $users): int
|
|
{
|
|
return $users
|
|
->filter(fn (User $user) => $user->m_level !== null && $user->payment_account !== null)
|
|
->count();
|
|
}
|
|
|
|
/**
|
|
* @param \Illuminate\Support\Collection<int, \App\User> $users
|
|
*/
|
|
private function newPartners(Collection $users, int $month, int $year): int
|
|
{
|
|
$startDate = Carbon::create($year, $month, 1)->startOfMonth();
|
|
$endDate = Carbon::create($year, $month, 1)->endOfMonth();
|
|
|
|
return $users
|
|
->filter(function (User $user) use ($startDate, $endDate): bool {
|
|
if ($user->m_level === null || $user->active_date === null || ! $this->hasActivePaymentAccount($user)) {
|
|
return false;
|
|
}
|
|
|
|
$activeDate = Carbon::parse($user->active_date);
|
|
|
|
return $activeDate->betweenIncluded($startDate, $endDate);
|
|
})
|
|
->count();
|
|
}
|
|
|
|
private function hasActivePaymentAccount(User $user): bool
|
|
{
|
|
return $user->payment_account !== null && Carbon::parse($user->payment_account)->isFuture();
|
|
}
|
|
|
|
private function activeAboQuery(): Builder
|
|
{
|
|
return UserAbo::query()
|
|
->where('active', true)
|
|
->whereNotIn('status', [4, 5, 6]);
|
|
}
|
|
|
|
/**
|
|
* @param int[] $userIds
|
|
*/
|
|
private function newAboCount(array $userIds, string $userColumn, string $isFor, int $month, int $year): int
|
|
{
|
|
$startDate = Carbon::create($year, $month, 1)->startOfMonth();
|
|
$endDate = Carbon::create($year, $month, 1)->endOfMonth();
|
|
|
|
return $this->activeAboQuery()
|
|
->whereIn($userColumn, $userIds)
|
|
->where('is_for', $isFor)
|
|
->whereBetween('start_date', [$startDate, $endDate])
|
|
->count();
|
|
}
|
|
|
|
/**
|
|
* @param int[] $userIds
|
|
* @return array{own_points: float, external_points: float, customer_abo_points: float, customer_single_order_points: float, customer_other_points: float, total_points: float, turnover_net: float}
|
|
*/
|
|
private function salesSummary(array $userIds, int $month, int $year): array
|
|
{
|
|
$summary = UserSalesVolume::query()
|
|
->leftJoin('shopping_orders', 'shopping_orders.id', '=', 'user_sales_volumes.shopping_order_id')
|
|
->whereIn('user_id', $userIds)
|
|
->where('month', $month)
|
|
->where('year', $year)
|
|
->selectRaw('COALESCE(SUM(month_KP_points), 0) as own_points')
|
|
->selectRaw('COALESCE(SUM(month_shop_points), 0) as external_points')
|
|
->selectRaw('COALESCE(SUM(CASE WHEN shopping_orders.is_abo = 1 THEN month_shop_points ELSE 0 END), 0) as customer_abo_points')
|
|
->selectRaw('COALESCE(SUM(CASE WHEN shopping_orders.id IS NOT NULL AND (shopping_orders.is_abo = 0 OR shopping_orders.is_abo IS NULL) THEN month_shop_points ELSE 0 END), 0) as customer_single_order_points')
|
|
->selectRaw('COALESCE(SUM(CASE WHEN shopping_orders.id IS NULL THEN month_shop_points ELSE 0 END), 0) as customer_other_points')
|
|
->selectRaw('COALESCE(SUM(month_total_net), 0) + COALESCE(SUM(month_shop_total_net), 0) as turnover_net')
|
|
->first();
|
|
|
|
$ownPoints = (float) ($summary->own_points ?? 0);
|
|
$externalPoints = (float) ($summary->external_points ?? 0);
|
|
|
|
return [
|
|
'own_points' => $ownPoints,
|
|
'external_points' => $externalPoints,
|
|
'customer_abo_points' => (float) ($summary->customer_abo_points ?? 0),
|
|
'customer_single_order_points' => (float) ($summary->customer_single_order_points ?? 0),
|
|
'customer_other_points' => (float) ($summary->customer_other_points ?? 0),
|
|
'total_points' => $ownPoints + $externalPoints,
|
|
'turnover_net' => (float) ($summary->turnover_net ?? 0),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param int[] $userIds
|
|
*/
|
|
private function shop1000Count(array $userIds, int $month, int $year): int
|
|
{
|
|
return UserSalesVolume::query()
|
|
->whereIn('user_id', $userIds)
|
|
->where('month', $month)
|
|
->where('year', $year)
|
|
->select('user_id')
|
|
->selectRaw('COALESCE(SUM(month_KP_points), 0) + COALESCE(SUM(month_shop_points), 0) as total_points')
|
|
->groupBy('user_id')
|
|
->havingRaw('COALESCE(SUM(month_KP_points), 0) + COALESCE(SUM(month_shop_points), 0) >= 1000')
|
|
->get()
|
|
->count();
|
|
}
|
|
}
|