# Prüfprotokoll zum Statistik-Audit — 09.09.2026 Dieses Dokument gehört zum [Auditbericht](audit-statistiken-2026-09-09.md). 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 ```bash 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 ```bash 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: ```text en: 0 missing es: 25 missing fr: 25 missing ``` Fehlende Schlüssel in es und fr: ```text 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: ```text 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: ```bash 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 ```jsonl {"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 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: ```javascript 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: ```json [ { "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: ```text 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` |