user_id => Zeilensumme / Laufstand */ public function detectDrift(int $month, int $year, ?array $userIds = null): array { $userIds ??= UserSalesVolume::query() ->where('month', $month) ->where('year', $year) ->whereNotNull('user_id') ->distinct() ->pluck('user_id') ->map(fn ($id) => (int) $id) ->all(); if ($userIds === []) { return []; } $rowSums = SalesVolumePoints::commissionPointsByUser($userIds, $month, $year); $lastRows = UserSalesVolume::query() ->whereIn('user_id', $userIds) ->where('month', $month) ->where('year', $year) ->whereIn('id', function ($sub) use ($month, $year) { $sub->selectRaw('MAX(id)') ->from('user_sales_volumes') ->where('month', $month) ->where('year', $year) ->groupBy('user_id'); }) ->get(['user_id', 'month_TP_points', 'month_shop_points']) ->keyBy('user_id'); $drift = []; foreach ($rowSums as $userId => $rowSum) { $last = $lastRows->get($userId); $runningTotal = $last ? (float) $last->month_TP_points + (float) $last->month_shop_points : 0.0; if (abs($runningTotal - (float) $rowSum) > 0.005) { $drift[(int) $userId] = ['rows' => (float) $rowSum, 'running' => $runningTotal]; } } ksort($drift); return $drift; } /** * Rechnet die Laufstaende der abweichenden User neu. * * @param int[]|null $userIds * @return array die reparierten User (Stand vor der Reparatur) */ public function repair(int $month, int $year, ?array $userIds = null): array { $drift = $this->detectDrift($month, $year, $userIds); foreach (array_keys($drift) as $userId) { SalesPointsVolume::reCalculateSalesPointsVolume($userId, $month, $year); } return $drift; } /** * Monate, die der naechtliche Sicherheitsnetz-Lauf pruefen darf: der laufende * Monat immer, der Vormonat nur, solange sein Provisionslauf noch nicht * gelaufen ist (Monatserster vor 03:00 bzw. spaeterer Ausfuehrungstag). Ein * abgeschlossener Monat wird nicht angefasst — seine Laufstaende sind so * abgerechnet worden, und eine Korrektur waere eine Nachberechnung. * * @return array */ public function monthsToGuard(?Carbon $now = null): array { $now ??= now(); $months = []; $previous = $now->copy()->subMonthNoOverflow(); if (! self::isMonthClosed((int) $previous->month, (int) $previous->year)) { $months[] = ['month' => (int) $previous->month, 'year' => (int) $previous->year]; } $months[] = ['month' => (int) $now->month, 'year' => (int) $now->year]; return $months; } /** * Abgeschlossen ist ein Monat, sobald der Provisionslauf seine Struktur * gespeichert hat. */ public static function isMonthClosed(int $month, int $year): bool { return UserBusinessStructure::query() ->where('month', $month) ->where('year', $year) ->where('completed', true) ->exists(); } }