mivita/dev/2026-07-22/audit-statistiken-2026-09-09-pruefprotokoll.md
Kevin 9a3940fca4 Doku: Statistik-Audit 09.09., Bewertung und Umsetzungsstand
Der Ordner dev/2026-07-22 ist ueber Monate gewachsen; ohne Einstieg findet man
darin nichts mehr. Deshalb ein README als Wegweiser, mit dem Hinweis, dass bei
Widerspruechen immer die juengere Entscheidung gilt (Beispiel: F7 wurde am 10.09.
abgeloest).

Die Bewertung traegt oben einen Stand-Block mit erledigten und offenen Befunden
samt Sprungmarken. Ueberholte Aussagen der ersten Fassung sind markiert statt
geloescht, damit der Weg nachvollziehbar bleibt.

Der Monatsabschluss steht als eigenes Dokument, weil es eine dauerhafte
Fachregel ist und keine Audit-Antwort: was eingefroren wird, was die Regel nicht
leisten kann, und die Betriebsbefehle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 11:19:50 +00:00

31 KiB
Raw Blame History

Prüfprotokoll zum Statistik-Audit — 09.09.2026

Dieses Dokument gehört zum Auditbericht. Es enthält die Ausführungsergebnisse, Prüfmethode und vollständigen zusätzlichen Gegenproben. Die temporären Arbeitsdateien sind für das Verständnis nicht erforderlich; ihre relevanten Inhalte stehen hier dauerhaft im Auditordner.

1. Arbeitsstand und Isolation

  • Repository: /var/www/html, Referenz-Commit 384b2dcff1533168d4a122f4c2e93a5e0336bd75.
  • Geprüft wurde der aktuelle Arbeitsstand einschließlich bereits vorhandener unversionierter Dateien und Änderungen. Aus einem Befund wird keine unbelegte Zuordnung zu einem bestimmten Entwickler oder Commit abgeleitet.
  • Vor Beginn wurden SHA-256-Prüfsummen von 10.385 vorhandenen regulären, von Git erfassten bzw. nicht ignorierten Dateien gespeichert. Bereits vorhandene Dateien wurden nicht bearbeitet.
  • Testkopie: /tmp/mivita-statistics-audit; Anwendung, Tests, Migrationen, Konfiguration, Routen, Views, lokale Pakete und eigene Kopie der vorhandenen vendor-Abhängigkeiten. Die Produktions-/Entwicklungs-.env wurde nicht übernommen, Bootstrap-Cache wurde nicht übernommen, temporäre Runtime-Verzeichnisse wurden separat angelegt.
  • Datenbank: SQLite :memory: gemäß Testkonfiguration. Mail/Cache im Testbetrieb, keine produktiven Zugangsdaten. Zum vollständigen Bootstrap wurden ausschließlich Testvariablen aus phpunit.xml, ein synthetischer APP_KEY, APP_URL=http://localhost und ein Null-Logkanal in der temporären Testumgebung verwendet.
  • Es wurden keine neuen Pakete im Repository installiert. PDF-Text-/Bildprüfung erfolgte mit einem ausschließlich unter /tmp entpackten PyMuPDF-Wheel. Die Testausführung verwendete die vorhandenen PHP-Abhängigkeiten in der Kopie.
  • Es wurden keine Testdateien im Repository angelegt. Der unten dokumentierte PHP-Code existierte als ausführbare Datei nur in der isolierten Kopie.

Die erste Testvorbereitung scheiterte am fehlenden PAYONE_KEY_HASH beim Bootstrap. Nach Übernahme der Testvariablen liefen Tests zunächst mit Warnungen wegen einer fehlenden .env; die isolierte Test-.env beseitigte diese Ursache. Die unten angegebenen Endergebnisse stammen aus den anschließend abgeschlossenen Läufen, nicht aus diesen Vorbereitungsversuchen.

2. Ausgeführte vorhandene Tests

Alle folgenden Befehle wurden im Verzeichnis /tmp/mivita-statistics-audit ausgeführt, nicht im Anwendungs-Arbeitsverzeichnis.

Statistik-Kernmodule

php artisan test --compact --filter='BackofficeStatistics|BackofficeDashboardService|PersonalStatisticsService|HomeMonthlyStats|PaylineForecastService|LiveCommissionService|TeamAboOverview|TeamFirstlinesPage|TopResultsService|AnnualFeeDueService|CustomerAboListView|PointsAboHighlight|TeamStructureAboBadges|TeamCustomerAbosRedirect'

Ergebnis: 109 bestanden, 678 Assertions, 1,74 s. Erfasst werden die vorhandenen Tests zu Zugriff, Monatsübersicht, Listen/Details, persönlichen Zahlen, Abo-/Payline-Kennzahlen, Prognosen, Firstlines, Ranglisten, Jahresgebühr und den neuen Ansichten.

Angrenzende Funktionen

php artisan test --compact --filter='AboPauseTest|AboOneAboRuleTest|AboLockDaysSettingTest|AboUpdateAuthorizationTest|AboExternalCompletionTest|AboOneTimeViewTest|ProfileImageTest|GdprDeletionTest|CheckoutOrderSourceValidationTest|YardShippingTaxTest|IncentiveRankingTest|IncentiveParticipantRankOrderingTest|IncentivePartnerRepairSponsorChangeTest|SponsorBuyingPointsRepairTest|LocalizationSyncTest'

Ergebnis: 105 bestanden, 2 fehlgeschlagen, 553 Assertions, 1,84 s. Beide Fehler liegen in Tests\Feature\LocalizationSyncTest, Vergleich der Sprachschlüssel gegen Deutsch, tests/Feature/LocalizationSyncTest.php:17.

Die gesonderte schreibfreie Prüfung der payment.php-Schlüssel ergab:

en: 0 missing
es: 25 missing
fr: 25 missing

Fehlende Schlüssel in es und fr:

manual_amount
manual_bank_reference
manual_bank_reference_help
manual_confirm_deviation
manual_error_already_paid
manual_error_amount_deviation
manual_error_cancelled
manual_error_future_date
manual_error_general
manual_error_payment_mismatch
manual_error_reference_required
manual_expected_amount
manual_method
manual_method_bank_transfer
manual_method_cash
manual_method_other
manual_note
manual_paid_at
manual_record_bank_hint
manual_record_button
manual_record_intro
manual_record_title
manual_recorded_by
manual_send_mail
manual_success

Diese Fehler sind vorhandene Übersetzungslücken im geprüften Stand. Sie belegen keine neu verursachte Statistikregression. Der übrige Testlauf bleibt von dieser Einordnung unabhängig.

In den finalen PHP-Läufen blieb die PHPUnit-Warnung erhalten:

Your XML configuration validates against a deprecated schema.
Migrate your XML configuration using "--migrate-configuration"!

Die vorgeschlagene Änderung wurde nicht ausgeführt. Die genannten Laufzeiten sind Messwerte der kleinen SQLite-Testläufe, keine Performanceaussage zur Produktionsanwendung. Es wurde nicht die komplette Repository-Testsuite ausgeführt.

3. Zusätzliche PHP-Gegenproben

Ausführung in der isolierten Kopie:

php artisan test --compact --filter=StatisticsAuditProbeTest

Endergebnis: 16 bestanden, 30 Assertions, 0,55 s.

Bedeutung: Die Assertions erwarten das im Audit beanstandete Ist-Verhalten. Ein bestandener Gegenproben-Test bestätigt den Fehlerzustand. Diese Tests dürfen bei einer späteren Behebung nicht unverändert als gewünschte fachliche Spezifikation übernommen werden; die Erwartungen müssen dann auf das im Audit beschriebene Soll umgestellt werden.

Prüfuhr überwiegend 09.09.2026, 12:00 Uhr; Liefertag-Probe 10.09.2026, 00:01 Uhr. Alle Benutzer, Namen, Konten, Produkte, Bestellungen und Buchungen wurden synthetisch erzeugt. Es wurden keine Kundendaten aus PDFs oder Datenbanken übernommen.

Methodische Grenzen:

  • A01 ruft den echten User-Controller mit angemeldetem Eigentümer auf. Es handelt sich um eine Prüfung der serverseitigen Zustandsänderung, nicht um einen Browser-/CSRF-End-to-End-Test. Die entsprechenden Routen wurden zusätzlich gelesen.
  • A07 belegt die unterschiedliche Aufnahme eines Teammitglieds. Punkte und Euro werden nicht als identische Einheiten verglichen; die Codeprüfung erklärt den fehlenden Beitrag zur zweiten Prognose.
  • A09 verwendet die echten privaten Export-Zuordnungsmethoden per Reflection. Der anschließende unveränderte fputcsv-Schreibpfad wurde gelesen. Ein Tabellenprogramm wurde nicht mit einer gefährlichen Formel geöffnet.
  • A10 misst gezielt die nach dem Vorladen verbleibenden Queries für die Lieferungszähler, nicht die gesamte HTTP-Anfrage.
  • A16 stellt den vom Recovery-Pfad erzeugbaren Datenzustand nach. Der Zahlungs-/Webhook-Pfad selbst wurde nicht ausgeführt.
  • Kleine Schemaergänzungen im beforeEach entsprechen den für diese isolierten Tests benötigten Testfeldern; sie sind keine Migrationen im Repository. Vorhandene Tests ergänzen einzelne fehlende Business-Felder ebenfalls. Die Gegenproben ersetzen keine Prüfung des vollständigen Produktionsschemas.
  • Die Query für start_date wurde mit einem synthetischen Start am 02.08. angesetzt, um einen SQLite-spezifischen Vergleich von reinen Datumsstrings am ersten Monatstag gegen Datetime-Grenzen nicht mit den untersuchten Bestandsfehlern zu vermischen.
  • Jede Gegenprobe leert den Testcache, damit Benutzer-IDs aus einer vorherigen In-memory-Datenbank nicht mit deren Cacheeinträgen wiederverwendet werden. Ein solcher anfänglicher Fixture-Konflikt wurde vor dem finalen Lauf korrigiert.

Aufgezeichnete Ergebnisse

{"case":"A01","paid_deliveries":0,"active":false,"status":6}
{"case":"A02","personal":175,"canonical":100}
{"case":"A03","overview":60,"details":100}
{"case":"A04","source":"snapshot","frozen_total":100,"ranking":200}
{"case":"A05","billed":100,"displayed_abo_count":0}
{"case":"A06","actual":100,"addon":20,"recurring":80}
{"case":"A07","day25_upcoming":50,"level_commission":0}
{"case":"A08","progress_exists":true,"forecast":null}
{"case":"A09","header_count":9,"summary_count":8,"summary":["Summe","1 Eintraege","","",100,300,"",""],"name":"=1+1"}
{"case":"A10","abos":5,"additional_queries":5}
{"case":"A11","expected_first_name":"Alpha","actual_first_name":"Zulu00"}
{"case":"A12","home_badge":1,"linked_details":0}
{"case":"A13","unexecuted_points":50,"forecast_day25":0}
{"case":"A15","booked_net_points":0,"aggregate_points":-100}
{"case":"A16","august_stock":0,"september_stock":1}
{"case":"A17","reference":"2026-01-31","due":"2026-03-02","bucket":"due_1_month"}

A14 ist ein JavaScript-Befund und folgt separat. A18 verwendet zusätzlich den in A09 dokumentierten unveränderten Formelstring.

Vollständiger isolierter Prüfcode

Nur zur Reproduktion in einer separaten Testkopie. Er ist bewusst hier als Dokumentation abgelegt; im Anwendungsrepository wurde daraus keine PHP-Datei erstellt.

<?php

use App\Models\{Product, ShoppingOrder, ShoppingOrderItem, UserAbo, UserAboItem, UserAccount, UserBusiness, UserSalesVolume};
use App\Services\Backoffice\{BackofficeDashboardService, BackofficeDrilldownService, PersonalStatisticsService, SalesVolumePoints};
use App\Services\Abo\{PaylineForecastService, TeamAboOverviewService};
use App\Services\BusinessPlan\LiveCommissionService;
use App\Http\Controllers\User\{AboController, BackofficeStatisticsController};
use App\Repositories\AboRepository;
use App\Services\Team\TopResultsService;
use App\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\{Cache, DB, Schema, URL};
use Tests\TestCase;

uses(TestCase::class, RefreshDatabase::class);
beforeEach(function () {
    Cache::flush();
    Carbon::setTestNow('2026-09-09 12:00:00');
    app()->setLocale('de');
    foreach (['sales_volume_points_KP_sum'=>'float','payline_points_qual_kp'=>'float','qual_user_level_next'=>'text'] as $col=>$type) { if (!Schema::hasColumn('user_businesses',$col)) Schema::table('user_businesses', fn($t)=>$t->{$type}($col)->nullable()); }
    URL::resolveMissingNamedRoutesUsing(fn ($name, $parameters = []) => '/'.$name);
    if (!Schema::hasColumn('products', 'slug')) Schema::table('products', fn ($t) => $t->string('slug')->nullable());
});
afterEach(fn () => Carbon::setTestNow());
function auditUser(?int $sponsor = null): User {
    return User::forceCreate(['email'=>'audit-'.uniqid().'@example.test','password'=>'test','lang'=>'de','admin'=>0,'m_level'=>1,'m_sponsor'=>$sponsor,'active_date'=>'2026-01-01','payment_account'=>'2027-01-01']);
}
function auditVolume(User $u, float $points, int $status = 1, ?ShoppingOrder $order = null, int $month = 9): UserSalesVolume {
    return UserSalesVolume::forceCreate(['user_id'=>$u->id,'shopping_order_id'=>$order?->id,'month'=>$month,'year'=>2026,'date'=>"2026-{$month}-01",'status'=>$status,'status_turnover'=>2,'points'=>$points,'total_net'=>$points]);
}
function auditOrder(User $u): ShoppingOrder {
    return ShoppingOrder::create(['shopping_user_id'=>0,'auth_user_id'=>$u->id,'member_id'=>$u->id,'country_id'=>1,'language'=>'de','user_shop_id'=>1,'payment_for'=>6,'total'=>100,'subtotal'=>100,'points'=>100,'paid'=>true,'shipped'=>0,'is_abo'=>true,'mode'=>'test']);
}
function auditAbo(User $u, string $date = '2026-09-20', int $points = 50): UserAbo {
    $a=UserAbo::create(['user_id'=>$u->id,'member_id'=>$u->id,'shopping_user_id'=>0,'email'=>$u->email,'is_for'=>'ot','payone_userid'=>0,'clearingtype'=>'cc','amount'=>5000,'active'=>true,'status'=>2,'abo_interval'=>20,'start_date'=>'2026-08-02','next_date'=>$date]);
    $p=Product::forceCreate(['name'=>'audit-'.uniqid(),'title'=>'Audit','price'=>10,'tax'=>19,'active'=>true,'points'=>$points,'weight'=>100,'slug'=>'audit-'.uniqid()]);
    UserAboItem::create(['user_abo_id'=>$a->id,'product_id'=>$p->id,'comp'=>0,'qty'=>1,'status'=>1]);
    return $a;
}
function auditRecord(string $case, array $data): void {
    file_put_contents('/tmp/statistics-audit-probes.jsonl',json_encode(['case'=>$case]+$data,JSON_UNESCAPED_UNICODE)."\n", FILE_APPEND);
}
it('AUDIT A01 selfservice accepts the hidden admin active toggle before six deliveries', function () {
    $u=auditUser(); $a=auditAbo($u); $this->actingAs($u);
    app('request')->merge(['action'=>'abo_update_settings','id'=>$a->id,'abo_interval'=>20,'abo_is_active'=>0]);
    (new AboController(app(AboRepository::class)))->update('ot',$a->id);
    auditRecord('A01',['paid_deliveries'=>$a->getCountPaidOrders(),'active'=>$a->fresh()->active,'status'=>$a->fresh()->status]);
    expect($a->fresh()->active)->toBeFalse()->and($a->fresh()->status)->toBe(6);
});
it('AUDIT A02 personal totals include unassigned and pending points', function () {
    $u=auditUser(); auditVolume($u,100); auditVolume($u,50,3); auditVolume($u,25,0);
    $p=app(PersonalStatisticsService::class)->monthly($u,9,2026);
    $canonical=SalesVolumePoints::totalPointsByUser([$u->id],9,2026)->get($u->id);
    auditRecord('A02',['personal'=>$p['totals']['total_points'],'canonical'=>$canonical]);
    expect($p['totals']['total_points'])->toBe(175.0)->and($canonical)->toBe(100.0);
});
it('AUDIT A03 drilldown loses negative balances', function () {
    $u=auditUser(); $a=auditUser($u->id); $b=auditUser($u->id); auditVolume($a,100); auditVolume($b,-40,4);
    $overview=app(BackofficeDashboardService::class)->overview($u,9,2026);
    $detail=app(BackofficeDrilldownService::class)->details($u,0,'total_points',9,2026);
    auditRecord('A03',['overview'=>$overview['totals']['total_points'],'details'=>$detail['summary']['total_points']]);
    expect($overview['totals']['total_points'])->toBe(60.0)->and($detail['summary']['total_points'])->toBe(100.0);
});
it('AUDIT A04 snapshot overlay replaces billed historical ranking', function () {
    $u=auditUser(); $a=auditUser($u->id); $order=auditOrder($a); $v=auditVolume($a,100,2,$order,8);
    $service=app(BackofficeDashboardService::class); $service->storeSnapshot($u,8,2026);
    $v->update(['points'=>200]);
    $overview=$service->overview($u,8,2026);
    auditRecord('A04',['source'=>$overview['_meta']['source'],'frozen_total'=>$overview['totals']['total_points'],'ranking'=>$overview['top_customer_abos']['by_points'][0]['points_actual']]);
    expect((float) $overview['totals']['total_points'])->toBe(100.0)->and($overview['top_customer_abos']['by_points'][0]['points_actual'])->toBe(200.0);
});
it('AUDIT A05 billed ranking uses current subscription count', function () {
    $u=auditUser(); $a=auditUser($u->id); $abo=auditAbo($a); $abo->update(['active'=>false,'status'=>4,'cancel_date'=>'2026-09-08']);
    auditVolume($a,100,2,auditOrder($a));
    $row=app(BackofficeDashboardService::class)->overview($u,9,2026)['top_customer_abos']['by_points'][0];
    auditRecord('A05',['billed'=>$row['points_actual'],'displayed_abo_count'=>$row['abo_count']]);
    expect($row['points_actual'])->toBe(100.0)->and($row['abo_count'])->toBe(0);
});
it('AUDIT A06 addon allocation survives full reversal of its order', function () {
    $u=auditUser(); $a=auditUser($u->id); $order=auditOrder($a);
    ShoppingOrderItem::forceCreate(['shopping_order_id'=>$order->id,'product_id'=>0,'qty'=>1,'price'=>20,'points'=>20,'is_abo_addon'=>true]);
    auditVolume($a,100,2,$order); auditVolume($a,-100,6,$order); auditVolume($a,100,2,auditOrder($a));
    $row=app(BackofficeDashboardService::class)->overview($u,9,2026)['top_customer_abos']['by_points'][0];
    auditRecord('A06',['actual'=>$row['points_actual'],'addon'=>$row['points_addon'],'recurring'=>$row['points_recurring']]);
    expect($row['points_actual'])->toBe(100.0)->and($row['points_addon'])->toBe(20.0)->and($row['points_recurring'])->toBe(80.0);
});
it('AUDIT A07 two forecast methods disagree about team membership', function () {
    $u=auditUser(); $a=auditUser($u->id); $a->forceFill(['m_level'=>null])->save(); auditAbo($a);
    UserBusiness::forceCreate(['user_id'=>$u->id,'month'=>9,'year'=>2026,'version'=>1,'qual_user_level'=>['name'=>'Test','paylines'=>1,'qual_kp'=>0,'qual_pp'=>0,'pr_line_1'=>10],'sales_volume_points_KP_sum'=>0]);
    $s=app(PaylineForecastService::class); $days=$s->forecastDays($u,0); $level=$s->levelAndCommissionForecast($u,9,2026);
    auditRecord('A07',['day25_upcoming'=>$days['days'][3]['upcoming'],'level_commission'=>$level['estimated_commission']]);
    expect($days['days'][3]['upcoming'])->toBe(50.0)->and($level['estimated_commission'])->toBe(0.0);
});
it('AUDIT A08 forecast absent before first monthly qualification', function () {
    $u=auditUser(); auditAbo($u);
    UserBusiness::forceCreate(['user_id'=>$u->id,'month'=>9,'year'=>2026,'version'=>1,'qual_user_level'=>null,'qual_user_level_next'=>['name'=>'First','paylines'=>1,'qual_kp'=>50,'qual_pp'=>0],'sales_volume_points_KP_sum'=>0]);
    $progress=app(LiveCommissionService::class)->levelProgress($u,9,2026);
    $forecast=app(PaylineForecastService::class)->levelAndCommissionForecast($u,9,2026);
    auditRecord('A08',['progress_exists'=>$progress!==null,'forecast'=>$forecast]);
    expect($progress)->not->toBeNull()->and($forecast)->toBeNull();
});
it('AUDIT A09 CSV summary has shifted columns and text is unprotected', function () {
    $c=app(BackofficeStatisticsController::class);
    $headers=(new ReflectionMethod($c,'csvHeaders'))->invoke($c,'new_partners');
    $summary=(new ReflectionMethod($c,'csvSummaryRow'))->invoke($c,['metric'=>'new_partners','summary'=>['count'=>1,'customer_points'=>100,'total_points'=>300]]);
    $row=(new ReflectionMethod($c,'csvRow'))->invoke($c,'new_partners',['name'=>'=1+1']);
    auditRecord('A09',['header_count'=>count($headers),'summary_count'=>count($summary),'summary'=>$summary,'name'=>$row[0]]);
    expect(count($headers))->toBe(9)->and(count($summary))->toBe(8)->and($summary[4])->toBe(100)->and($row[0])->toBe('=1+1');
});
it('AUDIT A10 deliveries cause one additional query per subscription', function () {
    $u=auditUser(); for($i=0;$i<5;$i++) auditAbo($u);
    $abos=UserAbo::with(['user_abo_items.product','shopping_user'])->where('member_id',$u->id)->get();
    DB::enableQueryLog(); DB::flushQueryLog();
    foreach($abos as $abo) $abo->getCountPaidOrders();
    $count=count(DB::getQueryLog()); DB::disableQueryLog();
    auditRecord('A10',['abos'=>5,'additional_queries'=>$count]);
    expect($count)->toBe(5);
});
it('AUDIT A11 top three truncates tied candidates before sorting their names', function () {
    $values=[]; $expectedId=null;
    for($i=0;$i<26;$i++) {
        $u=auditUser(); $account=UserAccount::forceCreate(['first_name'=>$i===25?'Alpha':'Zulu'.sprintf('%02d',$i),'last_name'=>'Audit']);
        $u->forceFill(['account_id'=>$account->id])->save(); $values[$u->id]=100;
        if($i===25) $expectedId=$u->id;
    }
    $s=app(TopResultsService::class); $rows=(new ReflectionMethod($s,'rankRows'))->invoke($s,$values,0);
    auditRecord('A11',['expected_first_name'=>'Alpha','actual_first_name'=>$rows[0]['first_name']]);
    expect(array_column($rows,'user_id'))->not->toContain($expectedId);
});
it('AUDIT A12 historical new abo badge differs from its linked VIP detail', function () {
    $u=auditUser(); $a=auditUser($u->id); $abo=auditAbo($a); $abo->update(['active'=>false,'status'=>4,'cancel_date'=>'2026-09-08']);
    $n=app(TeamAboOverviewService::class)->teamKpis($u,8,2026)['team_customer_abos_new'];
    $d=app(BackofficeDrilldownService::class)->details($u,0,'team_customer_abos',8,2026,true);
    auditRecord('A12',['home_badge'=>$n,'linked_details'=>$d['summary']['abo_count']]);
    expect($n)->toBe(1)->and($d['summary']['abo_count'])->toBe(0);
});
it('AUDIT A13 current-day unexecuted subscriptions disappear from forecast', function () {
    $u=auditUser(); Carbon::setTestNow('2026-09-10 00:01:00'); auditAbo($u,'2026-09-10');
    $f=app(PaylineForecastService::class)->personalCustomerAboForecast($u);
    auditRecord('A13',['unexecuted_points'=>50,'forecast_day25'=>$f['days'][3]['total']]);
    expect($f['days'][3]['total'])->toBe(0.0);
});

it('AUDIT A15 shop reversal is included twice in total points', function () {
    $u=auditUser(); auditVolume($u,100,2); auditVolume($u,-100,6);
    $total=SalesVolumePoints::totalPointsByUser([$u->id],9,2026)->get($u->id);
    auditRecord('A15',['booked_net_points'=>0,'aggregate_points'=>$total]);
    expect($total)->toBe(-100.0);
});
it('AUDIT A16 unlogged recovery reactivation leaves historical stock empty', function () {
    Carbon::setTestNow('2026-07-02 12:00:00');
    $u=auditUser(); $a=auditAbo($u); $a->update(['start_date'=>'2026-07-02']);
    Carbon::setTestNow('2026-07-25 12:00:00');
    $a->update(['active'=>false,'status'=>6]);
    \App\Models\UserAboStatusHistory::forceCreate(['user_abo_id'=>$a->id,'action'=>'deactivated']);
    // Reproduce persisted state written by AboHelper recovery, without executing payment code.
    Carbon::setTestNow('2026-08-15 12:00:00');
    $a->update(['active'=>true,'status'=>2]);
    Carbon::setTestNow('2026-09-09 12:00:00');
    $service=app(TeamAboOverviewService::class);
    $historical=$service->aboStockCount([$u->id],'member_id','ot',8,2026);
    $current=$service->aboStockCount([$u->id],'member_id','ot',9,2026);
    auditRecord('A16',['august_stock'=>$historical,'september_stock'=>$current]);
    expect($historical)->toBe(0)->and($current)->toBe(1);
});
it('AUDIT A17 month overflow includes March in January one month bucket', function () {
    $service=app(\App\Services\Team\AnnualFeeDueService::class);
    $key=(new ReflectionMethod($service,'groupKeyFor'))->invoke($service,Carbon::parse('2026-03-02'),Carbon::parse('2026-01-31'));
    auditRecord('A17',['reference'=>'2026-01-31','due'=>'2026-03-02','bucket'=>$key]);
    expect($key)->toBe(\App\Services\Team\AnnualFeeDueService::GROUP_DUE_1_MONTH);
});

4. Gegenprobe der JavaScript-Sortierung (A14)

Die Funktion wurde direkt aus der bestehenden Blade-Datei extrahiert und mit einem minimalen Ersatz für $.trim ausgeführt. Sie wurde nicht umgeschrieben:

const fs = require('fs');
const source = fs.readFileSync(
    'resources/views/components/_sortable_table.blade.php', 'utf8'
);
const start = source.indexOf('        function getSortValue(');
const fn = source.slice(start, source.indexOf('\n    });', start));
const $ = { trim: value => value.trim() };
eval(fn);
const results = ['2026-09-20', '2026-08-05'].map(value => ({
    value, type: 'date', parsed: getSortValue(value, 'date')
})).concat(['999.00', '1,000.00'].map(value => ({
    value, type: 'number', parsed: getSortValue(value, 'number')
})));
console.log(JSON.stringify(results));

Tatsächliche Ausgabe:

[
  {
    "value": "2026-09-20",
    "type": "date",
    "parsed": 0
  },
  {
    "value": "2026-08-05",
    "type": "date",
    "parsed": 0
  },
  {
    "value": "999.00",
    "type": "number",
    "parsed": 99900
  },
  {
    "value": "1,000.00",
    "type": "number",
    "parsed": 1
  }
]

Damit ist der Parserfehler nachgewiesen. Nicht durchgeführt wurde ein vollständiger Browser-Test von DOM-Ereignissen, Tastaturbedienung oder sämtlichen eingebundenen Tabellen.

5. Geprüfte Implementierungsbereiche

Die Kernservices wurden inhaltlich durchgelesen und mit den aufrufenden Controllern, Datenmodellen, Templates und Tests abgeglichen. Bei großen, übergreifenden Legacy-Dateien wurden die einschlägigen Methoden und Schreib-/Lesepfade geprüft; die Auflistung behauptet keinen vollständigen Audit aller übrigen Funktionen dieser Dateien.

Berechnung und Datenzugriff

  • app/Services/Backoffice/BackofficeDashboardService.php: Monatsübersicht, Baumaufbau, aktive Berater, Snapshots, Bestandsüberlagerung, Punkteaggregate, Neupartner, Führungskräfte, Abo-Toplisten und Recruiter-Ketten.
  • app/Services/Backoffice/BackofficeDrilldownService.php: Mitgliederauswahl, Detailzeilen, Punktegruppen, Abo-Listen, Telefon-/Namensfelder und Summen.
  • app/Services/Backoffice/PersonalStatisticsService.php und SalesVolumePoints.php: persönliche Gruppierung und zentrale SQL-Prädikate.
  • app/Services/Abo/TeamAboOverviewService.php: Monatsendbestände, Team-/Eigenkennzahlen, Wachstum, Abo-Zeilen und Ausführungsdaten.
  • app/Services/Abo/PaylineForecastService.php: Kontext/Cache, Payline-Filter, Abo-Soll/Ist, allgemeine und persönliche Terminvorschau, Level-/Provisionsprognose.
  • app/Services/BusinessPlan/LiveCommissionService.php: gespeicherte/live Werte, Bestandteile, Qualifikationsdaten, Cache und Fehlerpfad.
  • app/Services/Team/TopResultsService.php, AnnualFeeDueService.php: Ranglisten, Fotos, Fälligkeitsgruppen und Teamumfang.
  • app/Services/BusinessPlan/SalesPointsVolume.php, BusinessUserItemOptimized.php: relevante Status-/Stornobehandlung und Provisionsbaumfilter.

Abo-Lebenszyklus, Modelle und Persistenz

  • app/Repositories/AboRepository.php: Statusänderung, Pause, Kündigung, Mindestdauer, Sperrfristen, administrative Ausnahme und Protokollierung.
  • app/Services/AboHelper.php: Zähl-/Berechtigungsregeln, Ein-Abo-Prüfung, Pausenfähigkeit, Datumslogik und Recovery-Fortschreibung.
  • app/User.php, app/Models/UserAbo.php, UserAboOrder.php, UserAboStatusHistory.php, UserAccount.php, UserBusiness.php, UserSalesVolume.php: relevante Beziehungen, Scopes, Attribute und Statusinterpretation.
  • app/Models/BackofficeStatisticsSnapshot.php, app/Console/Commands/BackofficeStoreStatisticsSnapshots.php, AboStoreChartSnapshots.php, app/Console/Kernel.php, app/Providers/AppServiceProvider.php: Speicherung, Zeitplanung und Lebensdauer der Services.
  • Migrationen für Statistik-Snapshots, Abo-Statushistorie und Profilfoto; kein Ausführen gegen die Arbeitsdatenbank.

Controller, Ansichten, Routen und angrenzende Änderungen

  • app/Http/Controllers/HomeController.php, User/BackofficeStatisticsController.php, User/TeamController.php, User/AboController.php, Portal/AboController.php: relevante Dashboard-/Listen-/Export-/Abo-Aktionen.
  • routes/domains/crm.php, routes/domains/portal.php, relevante gemeinsame Routen sowie config/domains.php und config/subdomain.php: Zugangs- und Hostanbindung.
  • resources/views/user/backoffice/statistics/index.blade.php, details.blade.php, my.blade.php.
  • resources/views/dashboard/_statistics.blade.php, _points.blade.php, _top_results.blade.php und einschlägige Teamansichten firstlines, annual-fee, abos, points.
  • resources/views/user/abo/_customer_abo_list.blade.php, Abo-Einstellungen und resources/views/components/_sortable_table.blade.php.
  • app/Http/Controllers/ProfileImageController.php, app/Services/Slim.php, Bild-URL/-Löschung und Einbindung in den vorhandenen Löschprozess.
  • Herkunftsfeld und Validierung im Checkout, Steueroptionen in app/Services/HTMLHelper.php, Versandsteuer in app/Services/Yard.php und zugehörige Tests.
  • Relevante Incentive-Rangfolge, Teilnehmerordnung und Sponsor-/Punkte-Reparaturen anhand des Feedbacks und vorhandener Tests; kein vollständiger Audit sämtlicher Incentive- oder Provisionsfunktionen.

6. Abnahmeplanung für spätere Korrekturen

Die Befunde sollten als getrennte, nachvollziehbare Änderungen umgesetzt werden. Eine geeignete Reihenfolge der Regressionen ist:

  1. Serverseitige Geschäftsregeln gegen manipulierte/fehlende Formularfelder (A01).
  2. Disjunkte Status-/Stornokategorien und identische Netto-Summen in Übersicht, Detail und Export (A15, A02, A03, A06).
  3. Zeitbezug: Snapshot, Korrekturbuchung, Monatswechsel, Statushistorie und Teamwechsel (A04, A12, A16).
  4. Ein konsistenter Payline-Kontext und gültige Prognosen ohne bereits erreichtes Monatslevel, am Ausführungstag und bei Fehlerzuständen (A07, A08, A13, A19, A23).
  5. CSV-Spaltenschema/Formeltexte, lokalisierte Sortierung, Ranggleichstände und Fälligkeitsgrenzen (A09, A11, A14, A17, A18, A21).
  6. Query-/Speicherbudgets und Zugriff auf Profilfotos (A10, A20, A22).

Die offenen K10–K15 sollten separat geplant werden. Ihre Umsetzung ist keine Voraussetzung dafür, die belegten Fehler der bereits gelieferten Funktionen zu beheben. Produktionsnachberechnungen, umfassende Cache-Löschungen und Kundenkommunikation sind aus diesem Audit nicht ausgeführt worden.

7. Integrität vorhandener Dateien

Der Abschlussvergleich der vorab erfassten Datei-Prüfsummen ergab:

Vorhandene Dateien in der Prüfbasis: 10385
Abweichende oder fehlende Dateien: 0

Die Prüfbasis umfasst reguläre, getrackte und nicht ignorierte ungetrackte Dateien, einschließlich bereits vorhandener Dokumente und Implementierungsänderungen. Ignorierte Laufzeitdateien wurden nicht mit dieser Aussage gleichgesetzt; insbesondere wurde die Anwendungstest-Runtime deshalb separat gehalten.

Neu angelegt wurden ausschließlich:

  • dev/2026-07-22/audit-statistiken-2026-09-09.md
  • dev/2026-07-22/audit-statistiken-2026-09-09-pruefprotokoll.md

Es wurde weder ein Commit erstellt noch eine bestehende Datei korrigiert, formatiert oder überschrieben.

8. Fingerabdrücke zentraler geprüfter Dateien

Da der Arbeitsstand nicht vollständig eingecheckt ist, identifizieren diese SHA-256-Werte die tatsächlich geprüften Fassungen zusätzlich zum Referenz-Commit. Sie ersetzen keine Quellcode-Sicherung.

Datei SHA-256
app/Services/Backoffice/BackofficeDashboardService.php 2867ed5c2af5d3369587f55e6516f56f73e9024133ab3eab537f115c8ae7a914
app/Services/Backoffice/BackofficeDrilldownService.php 7bc5be4c19b5e9fbc2502b77ece6ba65f5d1296c951f127f59ecd60b2e8325c8
app/Services/Backoffice/PersonalStatisticsService.php d1ac5253ec0a5725115df8a442d0edd9638d4c6f234156b908fd9ef3df689607
app/Services/Backoffice/SalesVolumePoints.php 4266fe107dde4fd4521a4e80411c432f2b3cdcc9c50158bca0ee46bc12af699d
app/Services/Abo/TeamAboOverviewService.php 59d08a6c3e4eda3b01fe220283c2ddf8f5af83cf4f4b6e7551df8561ed74d03d
app/Services/Abo/PaylineForecastService.php f63db7453f06c81f2167266923810a9c95f235365c27ec69c6384296b991e492
app/Services/BusinessPlan/LiveCommissionService.php ca0ce2549b3baaeaa9af98316b461fd37ad2d7ff6471a70ce9e07ba43d67c260
app/Services/BusinessPlan/SalesPointsVolume.php 036e0acbcb281c06602e20334220038082e0e7214e20171d184aca00ff7699f7
app/Services/Team/TopResultsService.php cbb4d7598b2778bf2f5db85cd931ff1315d01848fbd22c611b1be1332c11feb1
app/Services/Team/AnnualFeeDueService.php c8f0c831ac4bd6089e145af484f4b0ccf3c4d138c6319daff95b0a69e6bb084d
app/Repositories/AboRepository.php ff335e21ac6dfc77b7dbd60cb273126924b3671ae5c994ad44cfcbe0d58119e6
app/Services/AboHelper.php 452b7d44449ea4b307f7f61654cf90408a8ae01b320a5bb1bd3de7a9a929ac9e
app/Http/Controllers/User/BackofficeStatisticsController.php 3ee2aecb9206c7e6be1a68c77c14fe5887ce260dd4eff686c315765a126ae43c
app/Http/Controllers/User/AboController.php a5b0a804b014ae330e918da36d573d5ee5efdbc226fca628091f6277240f0e1a
resources/views/components/_sortable_table.blade.php 266f501ee613718e940de430bbfde92e727c7c6ca5aa8ab22bd3312251ae770b
resources/views/user/abo/_customer_abo_list.blade.php e3196841806651fc6282ab06e634d9f83c891fe6f54cf4f414b5b5aaf3164253
resources/views/user/backoffice/statistics/index.blade.php 60a71fd73304753cfe846b9c1cab88e344606846a6311e73477266ebd1e439c7
app/Console/Commands/BackofficeStoreStatisticsSnapshots.php 48d2649454305e0ff8466804e15fda0c78a6e7b8bc327b9539654ea49e69389c