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:
Kevin 2026-09-14 15:31:32 +00:00
parent 685f7a90b9
commit 96545364f1
33 changed files with 2321 additions and 139 deletions

View file

@ -18,6 +18,8 @@ class BackofficeStatisticsController extends Controller
private const SESSION_YEAR_KEY = 'backoffice_statistics_year';
private const SESSION_TREE_KEY = 'backoffice_statistics_tree';
public function __construct(
private BackofficeDashboardService $dashboardService,
private BackofficeDrilldownService $drilldownService,
@ -33,8 +35,9 @@ class BackofficeStatisticsController extends Controller
}
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
$selectedTree = $this->selectedTree($request);
$startTime = microtime(true);
$statistics = $this->dashboardService->overview($request->user(), $selectedMonth, $selectedYear);
$statistics = $this->dashboardService->overview($request->user(), $selectedMonth, $selectedYear, $selectedTree);
$performance = [
'duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'source' => $statistics['_meta']['source'] ?? 'live',
@ -45,6 +48,8 @@ class BackofficeStatisticsController extends Controller
return view('user.backoffice.statistics.index', [
'selectedMonth' => $selectedMonth,
'selectedYear' => $selectedYear,
'selectedTree' => $selectedTree,
'treeLabels' => BackofficeDashboardService::treeLabels(),
'filterMonths' => HTMLHelper::getTransMonths(),
'filterYears' => HTMLHelper::getYearRange(2022),
'statistics' => $statistics,
@ -77,6 +82,7 @@ class BackofficeStatisticsController extends Controller
}
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
$selectedTree = $this->selectedTree($request);
$line = (int) $request->get('line', 1);
$metric = (string) $request->get('metric', 'consultants');
$onlyNew = $request->boolean('only_new');
@ -84,7 +90,8 @@ class BackofficeStatisticsController extends Controller
return view('user.backoffice.statistics.details', [
'selectedMonth' => $selectedMonth,
'selectedYear' => $selectedYear,
'details' => $this->drilldownService->details($request->user(), $line, $metric, $selectedMonth, $selectedYear, $onlyNew),
'selectedTree' => $selectedTree,
'details' => $this->drilldownService->details($request->user(), $line, $metric, $selectedMonth, $selectedYear, $onlyNew, $selectedTree),
]);
}
@ -95,10 +102,11 @@ class BackofficeStatisticsController extends Controller
}
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
$selectedTree = $this->selectedTree($request);
$line = (int) $request->get('line', 1);
$metric = (string) $request->get('metric', 'consultants');
$onlyNew = $request->boolean('only_new');
$details = $this->drilldownService->details($request->user(), $line, $metric, $selectedMonth, $selectedYear, $onlyNew);
$details = $this->drilldownService->details($request->user(), $line, $metric, $selectedMonth, $selectedYear, $onlyNew, $selectedTree);
$filename = sprintf(
'backoffice-statistik-%s%s-linie-%s-%02d-%d.csv',
$metric,
@ -134,8 +142,9 @@ class BackofficeStatisticsController extends Controller
}
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
$statistics = $this->dashboardService->overview($request->user(), $selectedMonth, $selectedYear);
$filename = sprintf('backoffice-statistik-uebersicht-%02d-%d.csv', $selectedMonth, $selectedYear);
$selectedTree = $this->selectedTree($request);
$statistics = $this->dashboardService->overview($request->user(), $selectedMonth, $selectedYear, $selectedTree);
$filename = sprintf('backoffice-statistik-uebersicht-%s-%02d-%d.csv', $selectedTree, $selectedMonth, $selectedYear);
return response()->streamDownload(function () use ($statistics): void {
$output = fopen('php://output', 'w');
@ -172,6 +181,22 @@ class BackofficeStatisticsController extends Controller
return [$selectedMonth, $selectedYear];
}
/**
* Baum-Definition (Provisionsbaum / gesamte Downline) — wie Monat und Jahr in
* der Session gemerkt, damit Detaillisten und Exporte dieselbe Sicht nutzen wie
* die Uebersicht, aus der sie aufgerufen wurden.
*/
private function selectedTree(Request $request): string
{
$tree = BackofficeDashboardService::normalizeTree(
(string) $request->get('tree', session(self::SESSION_TREE_KEY, BackofficeDashboardService::TREE_COMMISSION))
);
session([self::SESSION_TREE_KEY => $tree]);
return $tree;
}
/**
* @return string[]
*/