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

@ -0,0 +1,60 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* Backoffice-Statistik: je Monat ein Snapshot pro Baum-Definition
* (Provisionsbaum / gesamte Downline, Kundenentscheid 2026-09-14).
*
* Bestehende Snapshots wurden mit der kompletten Downline und der alten
* Punkte-Definition gebaut — sie werden als `downline` gekennzeichnet. Die
* Provisionsbaum-Snapshots der abgeschlossenen Monate legt der naechste Lauf von
* `backoffice:store-statistics-snapshots` an (Deploy: einmal mit `--force`, damit
* beide Varianten die neue Punkte-Definition tragen).
*
* Die Werte stehen bewusst als Literale hier (nicht als Konstanten aus
* BackofficeDashboardService): eine Migration darf nicht vom Stand des
* Anwendungscodes abhaengen — auf dem Server lief sie mit einer noch nicht
* neu geladenen Klasse auf „Undefined constant".
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('backoffice_statistics_snapshots', function (Blueprint $table) {
$table->string('tree', 16)->default('commission')->after('month');
});
DB::table('backoffice_statistics_snapshots')->update(['tree' => 'downline']);
// Erst den neuen Unique-Index anlegen, dann den alten entfernen: der
// Fremdschluessel auf user_id braucht in MySQL durchgehend einen Index,
// der mit user_id beginnt.
Schema::table('backoffice_statistics_snapshots', function (Blueprint $table) {
$table->unique(['user_id', 'year', 'month', 'tree'], 'backoffice_statistics_snapshot_tree_unique');
});
Schema::table('backoffice_statistics_snapshots', function (Blueprint $table) {
$table->dropUnique('backoffice_statistics_snapshot_unique');
});
}
public function down(): void
{
DB::table('backoffice_statistics_snapshots')
->where('tree', '<>', 'downline')
->delete();
Schema::table('backoffice_statistics_snapshots', function (Blueprint $table) {
$table->unique(['user_id', 'year', 'month'], 'backoffice_statistics_snapshot_unique');
});
Schema::table('backoffice_statistics_snapshots', function (Blueprint $table) {
$table->dropUnique('backoffice_statistics_snapshot_tree_unique');
$table->dropColumn('tree');
});
}
};

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Laufstand-Race (Ticket C2): die Neuberechnung der Laufstaende liest die
* Zeilen eines Beraters im Monat jetzt sperrend (SELECT ... FOR UPDATE). Mit
* diesem Index sperrt InnoDB genau den Bereich (user_id, year, month) statt
* aller Zeilen des Users ueber den Fremdschluessel-Index.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('user_sales_volumes', function (Blueprint $table) {
$table->index(['user_id', 'year', 'month'], 'user_sales_volumes_user_year_month_index');
});
}
public function down(): void
{
Schema::table('user_sales_volumes', function (Blueprint $table) {
$table->dropIndex('user_sales_volumes_user_year_month_index');
});
}
};