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>
31 KiB
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-Commit384b2dcff1533168d4a122f4c2e93a5e0336bd75. - 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 vorhandenenvendor-Abhängigkeiten. Die Produktions-/Entwicklungs-.envwurde 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 ausphpunit.xml, ein synthetischer APP_KEY,APP_URL=http://localhostund 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
/tmpentpackten 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
beforeEachentsprechen 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_datewurde 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.phpundSalesVolumePoints.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 sowieconfig/domains.phpundconfig/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.phpund einschlägige Teamansichtenfirstlines,annual-fee,abos,points.resources/views/user/abo/_customer_abo_list.blade.php, Abo-Einstellungen undresources/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 inapp/Services/Yard.phpund 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:
- Serverseitige Geschäftsregeln gegen manipulierte/fehlende Formularfelder (A01).
- Disjunkte Status-/Stornokategorien und identische Netto-Summen in Übersicht, Detail und Export (A15, A02, A03, A06).
- Zeitbezug: Snapshot, Korrekturbuchung, Monatswechsel, Statushistorie und Teamwechsel (A04, A12, A16).
- Ein konsistenter Payline-Kontext und gültige Prognosen ohne bereits erreichtes Monatslevel, am Ausführungstag und bei Fehlerzuständen (A07, A08, A13, A19, A23).
- CSV-Spaltenschema/Formeltexte, lokalisierte Sortierung, Ranggleichstände und Fälligkeitsgrenzen (A09, A11, A14, A17, A18, A21).
- 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.mddev/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 |