Team-Statistik = Team-Struktur (Provisionsbaum, Umschalter) und Laufstand-Fix in user_sales_volumes
K11 aus dem September-Protokoll (Tickets C1 und C2 im Entwicklungsplan), live seit 14.09.2026. C1 – Statistik rechnet wie die Provision - Standard-Baum ist der Provisionsbaum (Mitglieder wie TreeCalcBot: Mitgliedsebene, Abrechnungskonto, active_date; ausgefilterte Knoten kappen ihren Unterbaum, max. 21 Linien). Gesamte Downline bleibt per Umschalter erreichbar, Wahl in der Session. - Punkte-Definition: Provisionspunkte (TP + Shop), reine Karrierepunkte ausgewiesen statt eingerechnet, negative Monatssalden je User wie in der Provision auf 0 gekappt. - Abgeschlossene Monate nehmen die Mitglieder je Linie aus dem gespeicherten Provisionslauf (user_businesses), Snapshots je Baum-Definition (Payload-Version 3, Migration). - Struktur-Modal: Gesamt aus den Linien-Punkten mit Dezimalen statt der int-Spalte total_pp. - Messlauf statistics:compare-trees (Downline / Provisionsbaum / Struktur je VIP, Laufstand-Drift). C2 – Laufstand-Race (Provision zu niedrig bei zeitgleichen Buchungen) - Neuberechnung liest die Zeilen des Beraters sperrend (FOR UPDATE), Index (user_id, year, month). - Paid-Pfad der Payone- und Stripe-Webhooks als DB::transaction mit 3 Anlaeufen; Deadlock und Lock-Timeout aus der Rechnungserstellung werden nicht mehr geschluckt. - Sicherheitsnetz sales:repair-running-totals taeglich 02:45 vor dem Provisionslauf; abgeschlossene Monate werden nicht angefasst (Entscheid 14.09.: keine Nachzahlung fuer 06/08). Doku: Entwicklungsplan zum Protokoll, Deploy-Notiz 14.09. (inkl. Migrations-Stolperstein: Migrationen tragen Werte als Literale, nicht als Konstanten aus dem Anwendungscode). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
685f7a90b9
commit
96545364f1
33 changed files with 2321 additions and 139 deletions
110
app/Console/Commands/SalesRepairRunningTotals.php
Normal file
110
app/Console/Commands/SalesRepairRunningTotals.php
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\BusinessPlan\SalesVolumeRunningTotalService;
|
||||
use App\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* Sicherheitsnetz gegen Laufstand-Drift (Ticket C2): laeuft taeglich um 02:45,
|
||||
* also vor dem Provisionslauf `business:store-optimized` um 03:00, und rechnet
|
||||
* die Laufstaende aller User neu, deren letzte Zeile in `user_sales_volumes`
|
||||
* nicht zur Zeilensumme passt. Ohne Argumente werden der laufende Monat und —
|
||||
* solange er noch nicht abgerechnet ist — der Vormonat geprueft.
|
||||
*
|
||||
* Hintergrund: {@see SalesVolumeRunningTotalService}.
|
||||
*/
|
||||
class SalesRepairRunningTotals extends Command
|
||||
{
|
||||
protected $signature = 'sales:repair-running-totals
|
||||
{month? : Monat (Standard: laufender Monat und, falls offen, Vormonat)}
|
||||
{year? : Jahr}
|
||||
{--dry-run : Nur anzeigen, nichts schreiben}
|
||||
{--force : Auch einen bereits abgerechneten Monat neu rechnen (Nachberechnung!)}';
|
||||
|
||||
protected $description = 'Laufstaende in user_sales_volumes gegen die Zeilensumme pruefen und abweichende User neu berechnen';
|
||||
|
||||
public function handle(SalesVolumeRunningTotalService $service): int
|
||||
{
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$months = $this->months($service);
|
||||
|
||||
if ($months === null) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
foreach ($months as ['month' => $month, 'year' => $year]) {
|
||||
$drift = $dryRun
|
||||
? $service->detectDrift($month, $year)
|
||||
: $service->repair($month, $year);
|
||||
|
||||
$label = sprintf('%02d/%d', $month, $year);
|
||||
|
||||
if ($drift === []) {
|
||||
$this->info("Laufstaende {$label}: keine Abweichung.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$users = User::query()->whereIn('id', array_keys($drift))->get()->keyBy('id');
|
||||
|
||||
$this->warn(sprintf(
|
||||
'Laufstaende %s: %d User mit Drift%s',
|
||||
$label,
|
||||
count($drift),
|
||||
$dryRun ? ' (DRY-RUN, nichts geschrieben)' : ', neu berechnet'
|
||||
));
|
||||
$this->table(
|
||||
['User', 'E-Mail', 'Zeilensumme', 'Laufstand vorher', 'Diff'],
|
||||
collect($drift)->map(fn (array $values, int $userId) => [
|
||||
$userId,
|
||||
$users->get($userId)?->email ?? '-',
|
||||
number_format($values['rows'], 2, ',', '.'),
|
||||
number_format($values['running'], 2, ',', '.'),
|
||||
number_format($values['running'] - $values['rows'], 2, ',', '.'),
|
||||
])->values()->all()
|
||||
);
|
||||
|
||||
\Log::channel('cron')->warning('sales:repair-running-totals: Laufstand-Drift', [
|
||||
'month' => $month,
|
||||
'year' => $year,
|
||||
'dry_run' => $dryRun,
|
||||
'users' => $drift,
|
||||
]);
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{month: int, year: int}>|null
|
||||
*/
|
||||
private function months(SalesVolumeRunningTotalService $service): ?array
|
||||
{
|
||||
if (! $this->argument('month')) {
|
||||
return $service->monthsToGuard();
|
||||
}
|
||||
|
||||
$month = (int) $this->argument('month');
|
||||
$year = (int) ($this->argument('year') ?: now()->year);
|
||||
|
||||
if ($month < 1 || $month > 12 || $year < 2020) {
|
||||
$this->error('Ungueltiger Monat/Jahr.');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (SalesVolumeRunningTotalService::isMonthClosed($month, $year) && ! $this->option('dry-run') && ! $this->option('force')) {
|
||||
$this->error(sprintf(
|
||||
'Monat %02d/%d ist abgerechnet — die Laufstaende sind so in die Provision eingegangen. Neu rechnen nur mit --force (aendert keine Gutschriften, nur die Datenbasis).',
|
||||
$month,
|
||||
$year
|
||||
));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return [['month' => $month, 'year' => $year]];
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue