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>
32 KiB
Nachprüfung der Audit-Bewertung und Storno-Korrekturen — 09.09.2026
Geprüft wurden die Bewertung einschließlich Nachträgen 8 und 9, die Änderungen gegenüber dem ursprünglichen Audit, vorhandene Regressionstests und zusätzliche Gegenbeispiele. Ergänzend erfolgten ausschließlich lesende Aggregatabfragen gegen die im Entwicklungscontainer konfigurierte MySQL-Datenbank. Diese Datenbank wird hier nicht ungeprüft mit dem laufenden Produktionssystem gleichgesetzt.
1. Ergebnis
Die Korrektur für reguläre Eigen- und Shop-Bestellungen ist nachvollziehbar und durch Tests bestätigt. Die abschließende Aussage „A15 und A06 erledigt“ ist jedoch zu weitgehend: A06 besteht weiterhin. Außerdem hat die neue Herkunftszuordnung einen nicht abgedeckten Sonderfall bei manuellen Gutschriften, und die neuen Tests zur Monatsregel sind nicht unabhängig vom tatsächlichen Systemmonat.
| Gegenstand | Ergebnis der Nachprüfung |
|---|---|
| A15: doppelte Berücksichtigung eines Shop-Stornos | Behoben: Eigen-/Shop-Prädikate sind disjunkt. Regulärer Shop-Storno mindert Shop-Punkte und Shop-Nettobasis genau einmal. |
| A15: früher falsche Herkunft bei regulären Shop-Bestellungen | Für neue Stornos behoben: Status 2 des Originals führt zuverlässig zu status_turnover=2. Vorhandene Altzeilen bleiben absichtlich unverändert. |
| A06: Aufteilung in wiederkehrende Punkte und Einmal-Zusätze | Nicht behoben: Zusätze einer vollständig stornierten Bestellung bleiben in der Monatsaufteilung enthalten; Nachweis R01. |
| Neue Zuordnung für Status 4/Shop-Umsatz | Fehlerhafter Sonderfall: Storno spiegelt die ursprünglichen Punktekategorien nicht; Nachweis R02. |
| Neue Monatsregel und Settings-Schalter | Im derzeitigen Monat funktionieren die dokumentierten Kalenderfälle. Schalter ist ohne Eintrag aus; eingeschaltet wird im laufenden Monat gebucht. Testuhr wird jedoch umgangen; R03. |
Migration status_turnover |
Passt zur gelesenen Datenbankdefinition: nullable, kein Default. Kein Daten-Backfill enthalten. |
| Übrige 21 Audit-Befunde | Die zugehörige Implementierung wurde gegenüber der ursprünglichen Prüfbasis nicht korrigiert. Änderungen ihrer Priorität sind Bewertungen, keine Behebungen. |
Die notwendigen nächsten Schritte sind damit überschaubar: A06 offen lassen und die Positionsaufteilung korrigieren, die Herkunft von Punkten und Nettoumsatz beim Status-4-Sonderfall getrennt behandeln und die Monatstests von der Systemuhr lösen. Für diese Korrekturen muss keine erneute Entscheidung über den bereits festgelegten Zeitpunkt der Stornobuchung abgewartet werden.
2. Tatsächlich geänderter Umfang
Der SHA-256-Vergleich mit der ursprünglichen Audit-Prüfbasis zeigt folgende neue bzw. geänderte Implementierung:
app/Services/BusinessPlan/SalesPointsVolume.php: Monatsregel, Schalterabfrage, Herkunft des Stornos und exklusive Eigen-/Shop-Neuberechnung.app/Services/Backoffice/SalesVolumePoints.php: disjunkte Eigen-/Shop-Prädikate.resources/views/admin/settings/index.blade.php: neuer Schalter.database/migrations/2026_09_09_140000_make_status_turnover_nullable_on_user_sales_volumes.php: neue Migration.tests/Feature/StornoClosedMonthTest.phpundStornoPointsCategoryTest.php: elf neue Tests.
Die Änderungen wurden gegen den damaligen Arbeitsstand verglichen, nicht nur gegen Git-HEAD. Dadurch werden bereits vor dem Audit vorhandene, noch nicht eingecheckte Änderungen nicht versehentlich als jetzige Korrekturen gewertet. Die Anwendungsdatei BackofficeDashboardService.php, in der A06 entsteht, ist unverändert.
3. Verbliebene und neu erkannte Probleme
R01 — P2: A06 bleibt trotz korrigierter Storno-Herkunft bestehen
Stellen: app/Services/Backoffice/BackofficeDashboardService.php:753, :851–883; neuer Schreibpfad app/Services/BusinessPlan/SalesPointsVolume.php:352–403.
Die neue Herkunftsangabe bewirkt, dass ein regulärer Shop-Storno die abgerechneten Kundenabo-Gesamtpunkte korrekt mindert. Die Zusatzpunkte werden aber weiterhin aus unterschiedlichen Benutzer-/Bestellungs-Paaren und deren ursprünglich positiven Positionen ermittelt. Der Nettosaldo der zugehörigen Buchungen und deren Vorzeichen fehlen in dieser Berechnung.
Mit dem echten korrigierten Storno-Service nachgestellt:
- Kundenabo-Bestellung mit 100 Punkten, davon 20 Einmal-Zusatzpunkte. Das Original hat wie im Bestellpfad
status_turnover=NULL. - Vollstorno über
cancelSalesPointsVolume(). Der neue Code erzeugt selbststatus_turnover=2; die Gegenprobe setzt den Stornostatus nicht künstlich. - Zweite, nicht stornierte Kundenabo-Bestellung mit 100 reinen wiederkehrenden Punkten.
- Monatsrangliste öffnen.
| Ausgabe | Ist | Soll |
|---|---|---|
| Gesamt abgerechnet | 100 | 100 |
| Einmal-Zusatzpunkte | 20 | 0 |
| Wiederkehrende Punkte | 80 | 100 |
Der ursprüngliche Audit beschrieb hier eine eigenständige Positions-/Vorzeichenursache. Die falsche Herkunft im alten Buchungspfad war ein zusätzliches Problem. Das Beheben der Herkunft beseitigt die Positionsursache nicht.
Empfehlung: Zuerst pro Bestellung die Nettowirkung der im Monat zählbaren Buchungen auf wiederkehrende und einmalige Positionen bestimmen, danach je Berater summieren. Die pauschale Begrenzung des Zusatzwertes auf die Monatsgesamtsumme reicht nicht. Regressionen für Vollstorno mit weiterer Bestellung, Teilstorno und optional erlaubtem Folgemonatsstorno ergänzen. A06 nicht als erledigt führen.
R02 — P2: Storno einer manuellen Gutschrift kann falsche Punktekategorien erzeugen
Stellen: app/Services/BusinessPlan/SalesPointsVolume.php:99–104,128–133,301–310; app/Services/Backoffice/SalesVolumePoints.php:36–38.
Die neue Methode behandelt Status 4 mit status_turnover=2 pauschal wie eine Shop-Bestellung. Der ursprüngliche Status-4-Zweig macht jedoch etwas anderes: Punkte gehen nach KP/TP, nur der Nettoumsatz wird dem Shop zugeordnet. Der Storno verschiebt dagegen sowohl Punkte als auch Nettoumsatz in die Shop-Kategorie.
Nachgestellt: Manuelle Gutschrift mit 100 Punkten, 100 Nettoumsatz, status=4, status_turnover=2, status_points=2 (nur KP); anschließend Vollstorno über den neuen Service.
| Kennzahl nach Vollstorno | Ist | Soll bei vollständiger Gegenbuchung |
|---|---|---|
| KP | 100 | 0 |
| Shop-Punkte | −100 | 0 |
| Shop-Nettoumsatz | 0 | 0 |
| Statistik: Eigenpunkte | 100 | 0 |
| Statistik: Kundenpunkte | −100 | 0 |
| Punkte ohne Payline-Wirkung | 100 | 0 |
Die Gesamtpunktesumme ist zwar null, aber die ursprünglichen KP bleiben stehen und im Shop entsteht ein negativer Punktesaldo, den die Ursprungsbuchung dort nie aufgebaut hat. Bei status_points=1 ist zusätzlich die TP-Zuordnung betroffen.
Reichweite: Der Sonderfall ist im neuen Service ausdrücklich vorgesehen und wurde auf Service-Ebene nachgewiesen. In der gelesenen Datenbank existieren 13 Status-4-Buchungen mit Shop-Nettoumsatz; ein bereits erfolgter Storno genau dieser Buchungen wurde nicht nachgewiesen. Die Erreichbarkeit für sämtliche administrativen Rechnungsarten wurde nicht per Oberfläche getestet. Dies ist daher keine Behauptung eines bereits entstandenen neuen Auszahlungsschadens.
Empfehlung: Herkunft der Punkte und Herkunft des Nettoumsatzes getrennt abbilden bzw. die Ursprungsbuchungsart bei der Gegenbuchung erhalten. Eine gemeinsame Zahl 1/2 reicht für den bestehenden Status-4-Sonderfall nicht. Tests für Status 4 × Umsatzzuordnung 1/2 × Punkteart KP bzw. KP+TP ergänzen; jede Gegenbuchung muss alle Kategorien des Originals aufheben.
R03 — P2: Die Monatstests laufen nur zufällig im passenden Systemmonat
Stellen: app/Services/BusinessPlan/SalesPointsVolume.php:320–323,368–370; tests/Feature/StornoClosedMonthTest.php:13–14,50–60; tests/Feature/StornoPointsCategoryTest.php:13.
Die neuen Tests frieren Carbon auf September 2026 ein. Der neue Monatsvergleich und die Stornobuchung verwenden dagegen PHP-date(). Diese Funktion folgt weiterhin der tatsächlichen Systemuhr. Die Fixtures und Erwartungen bleiben fest bei September 2026.
Nachgestellt: Reale Systemuhr September 2026; Anwendungs-/Carbon-Uhr auf Oktober 2026 gestellt; Ursprungsbuchung Oktober 2026. isStornoMonthOpen() ergibt false, obwohl Original und eingefrorener Anwendungsmonat identisch sind.
Die elf Tests bestehen heute. Nach einem tatsächlichen Monatswechsel werden mehrere trotz unveränderter fachlicher Implementation fehlschlagen bzw. andere Buchungsmonate verwenden. Das ist zunächst ein Reproduzierbarkeitsfehler der Tests; es ist kein Nachweis, dass die Produktionsanwendung heute grundsätzlich im falschen Monat bucht.
Empfehlung: Für Prüfung und Buchung denselben einmal ermittelten, testbaren Zeitpunkt verwenden, etwa now() oder eine injizierte Uhr. Monat/Jahr daraus gemeinsam ableiten. Monats- und Jahreswechsel unabhängig von der Systemuhr testen. Der Kommentar in StornoClosedMonthTest.php, die Testmigration verbiete NULL, ist durch die neue Migration ebenfalls überholt.
4. Was an den Korrekturen bestätigt ist
Normale Shop-/Eigenstornos: Ein Shop-Original mit status_turnover=NULL erhält im neuen Storno die Herkunft 2. Ein Vollstorno von 100 Punkten/100 Nettoumsatz ergibt KP 0, Shop-Punkte 0, Shop-Nettobasis 0 und Gesamtpunkte 0. Die ursprüngliche doppelte Berücksichtigung ist beseitigt. Eigenstornos und unveränderte Behandlung alter NULL-Stornos werden von den neuen vorhandenen Tests ebenfalls abgedeckt.
Zeitpunktregel: Der frühe Rücksprung verhindert bei ausgeschaltetem Schalter eine Punkte-/Umsatzzeile für ältere Kalendermonate. Der einzige gefundene aufrufende Rechnungspfad (InvoiceRepository::correctPointsForCancellation(), Zeile 511) dereferenziert das nun nullable Ergebnis nicht. Die Rechnungserstellung wird dadurch nicht durch einen neuen Nullzugriff unterbrochen. Der gesamte PDF-/Mail-Ablauf wurde nicht live ausgelöst.
Settings: Setting::getContentBySlug() liefert ohne Datensatz false; bei Typ bool liefert es einen echten Boolean. Der Controller setzt ein nicht übermitteltes Checkbox-Feld bei vorhandenem Settings-Eintrag auf false. Ein- und Ausschalten sind damit auch im Formularpfad nachvollziehbar, nicht nur bei direktem Schreiben des Settings.
Grenze der Abschlussregel: Implementiert ist „aktueller Kalendermonat“, keine Prüfung, ob tatsächlich schon eine Monatsauswertung/Gutschrift erstellt wurde. Bei der dokumentierten Auslegung ist das konsistent. Falls künftig ein expliziter Abschlussstatus maßgeblich werden soll, genügt die aktuelle Funktion nicht.
Incentive: Der frühe Rücksprung überspringt weiterhin auch IncentiveTracker::trackStorno(). Die Bewertung benennt den Fall einer noch laufenden Incentive-Aktion zu Recht als offen. Die neue Kategorienkorrektur löst diese Produktentscheidung nicht.
Migration: Die Entwicklungsdatenbank hat bereits tinyint unsigned, nullable, Default NULL. Die Migration ändert keine vorhandenen Werte. Der leere down()-Pfad ist offen kommentiert: Ein Rollback stellt die alte NOT-NULL-Definition nicht wieder her. „Kein Rückbau“ sollte auch in einer späteren Release-Notiz stehen; es wurde hier keine Migration gegen die Entwicklungsdatenbank ausgeführt.
5. Gegenprüfung der Daten- und Prioritätsaussagen
Lesende Datenbanknachweise
Die Abfragen liefen über eine separate PDO-Verbindung in START TRANSACTION READ ONLY, anschließend Rollback. Keine Laravel-Anwendung wurde dafür gebootet; keine Zugangsdaten, Namen, E-Mails oder einzelnen Kundendatensätze wurden ausgegeben. Die folgenden Zahlen sind Aggregate des konfigurierten Entwicklungsdatenbestands zum Prüfzeitpunkt.
| Aussage der Bewertung | Eigene Nachprüfung |
|---|---|
20 Stornos, alle status_turnover=NULL |
Bestätigt. Kein vorhandener Storno mit Herkunft 2. |
| 13 Stornos auf Kunden-/Shop-Bestellungen | Bestätigt über shopping_orders.payment_for=6. |
| Statusverteilung mit 19.525 Eigen-, 24.151 Shop-, 76/13 manuellen und 647 Registrierungszeilen | Bestätigt; zusätzlich 1 Status-0- und 8 Status-3-Zeilen wie dokumentiert. |
| 7 VIP-Konten, 5 englischsprachige Nutzer | Bestätigt bei nicht gelöschten Konten. |
| 901 Nutzer insgesamt | Präzisierung: 901 nicht gelöschte Konten, 1.634 physische Benutzerzeilen einschließlich Soft-Deletes. Kein abweichendes Wachstumsszenario daraus abgeleitet. |
| 44 Kündigungen, 10 Deaktivierungen, 6 Pausen, keine Reaktivierungshistorie | Bestätigt. Die 10 Abos mit Deaktivierungshistorie sind derzeit inaktiv. |
| 7 gleichmonatige Shop-Stornos | Bestätigt durch Verknüpfung Stornozeile → Originalrechnung → Ursprungsbuchung. Sie verteilen sich auf 6 Benutzer-/Monatskombinationen; betroffener Nettobetrag insgesamt −596,05 €. |
| 178,82 € Shop-Provisionswirkung | Als rechnerische Differenz der Shop-Basis bestätigt, siehe Einschränkung unten. |
Für die sechs Benutzer-/Monatskombinationen wurden die gespeicherten Monatsabrechnungen verknüpft. Die dortigen Shop-Margen betragen jeweils 30 %. Die Differenz
SUM(ROUND(Shopbasis × Marge / 100, 2) − ROUND((Shopbasis + Storno-Netto) × Marge / 100, 2))
ergibt 178,82 €. Einfaches Runden jedes einzelnen Stornoanteils und anschließendes Summieren kann hiervon um einen Cent abweichen; die Monatsberechnung ist daher ausdrücklich angegeben.
Nicht bestätigt ist die Formulierung „178,82 € zu viel ausgezahlt“. Es wurden keine Auszahlungstransaktionen abgestimmt. Zudem stimmt bei einer der sechs gespeicherten Monatsabrechnungen commission_shop_sales nicht exakt mit der bloßen Formel aus Shopbasis und Marge überein. Die Ursache dieser Abweichung ist nicht Gegenstand der Nachprüfung. Eine sichere Auszahlungsaussage braucht den Abgleich der tatsächlich festgesetzten und ausgezahlten Beträge einschließlich vorhandener Korrekturen.
Einordnung der Bewertung
- A15, Bestandsdaten: Die Konkretisierung ist richtig: Die alte Doppelberücksichtigung war in den gelesenen Daten nicht aktiv, weil dort Herkunft 2 bei Stornos fehlte. Der ursprüngliche Audit hatte eine bedingte Codewirkung beschrieben und die fehlende Produktionsprüfung ausdrücklich benannt. Das darf nicht nachträglich als schon belegter Schaden an Gesamtpunkten gelesen werden. Die Fixture ist ein gültiger Test des überlappenden Zweigs; sie war kein Beleg dafür, dass dieser Datenzustand damals im Bestand vorkam. Durch den korrigierten Writer wird Herkunft 2 für neue Shop-Stornos jetzt tatsächlich erzeugt.
- Provisionswirkung: Die Aussagen in Abschnitten 3, 5 und 6 der Bewertung, die Auswirkung auf Provisionen sei null bzw. nur Darstellung betroffen, sind durch den eigenen Nachtrag 9 überholt. Gesamtpunkte allein bestimmen die Shop-Provision nicht;
BusinessUserItemOptimized.php:233–236verwendet die Shop-Nettobasis. Bei Weiterverwendung des Dokuments sollte der gültige Endstand vorne klar stehen. - A16 und A22: Die gelesenen Bestandsgrößen rechtfertigen eine geringere kurzfristige Dringlichkeit. Keine heute aktive der zehn Deaktivierungen ist aber kein Beweis für vollständige historische Richtigkeit sämtlicher Zwischenzustände, Teamwechsel und anderer Schreibpfade. Beim Speicherbefund wurde ursprünglich ein Skalierungsrisiko beschrieben, kein bereits eingetretener Absturz.
- A14: Die Begrenzung auf Englisch stimmt. Schon der ursprüngliche Befund demonstrierte englische Formate; die Empfehlung, alle vier Sprachen zu testen, behauptet keinen Fehler in allen vier Sprachen. Fünf betroffene Nutzer senken die Reichweite, nicht die Fehlerhaftigkeit ihrer Sortierung.
- A05: „Nur ein Tooltip genügt“ ist keine vollständig abgesicherte Schlussfolgerung. Das Juli-Dokument empfiehlt tatsächlich
customerAboCounts(); das neuere September-PDF verlangt aber ausdrücklich nur bereits abgerechnete Abos. Es besteht ein Widerspruch zwischen ursprünglichem Umsetzungsdetail und erneuertem Kundenwunsch. Aktuellen Bestand klar beschriften ist sinnvoll; ob damit die geforderte Anzahl abgegolten ist, bleibt fachlich zu entscheiden. - A01: P1/P2 ist eine Prioritätsentscheidung. Dass nur das eigene Abo betroffen ist, war bereits im ursprünglichen Audit abgegrenzt. Ausfall künftiger Abo-Lieferungen/-Zahlungen durch Umgehung einer Mindestlaufzeit ist trotzdem eine relevante Geschäftsregelverletzung. Ohne Untersuchung von Änderungsereignissen ist „kein aktueller Schaden“ hier nicht belegt.
- A18: Teamdaten sind nicht ausschließlich vom exportierenden Benutzer kontrollierter Input. Namen, Kontaktdaten und weitere Textfelder können von anderen Personen stammen. Die Teamgrenze beseitigt die Formel-Interpretation im Tabellenprogramm nicht. Eine niedrigere organisatorische Priorität kann vereinbart werden; die Begründung „kein fremder Input“ trägt technisch nicht.
- A20: Öffentliche Bilder können eine bewusst akzeptierte Produktentscheidung sein. Ein mit
uniqid()präfigierter Dateiname ist aber keine Team-Zugriffskontrolle. „Teamintern“ muss entweder nur die Listenauswahl oder auch den Bildabruf meinen; das sollte ausdrücklich festgelegt sein. - A07: Die noch offene allgemeine Vereinheitlichung K11 blockiert nicht zwangsläufig die Konsistenz zweier bereits vorhandener Payline-Prognosen. Gemeinsame Filter innerhalb derselben Prognosesicht können gesondert korrigiert werden.
6. Tests und Aussagegrenzen
Alle schreibenden Testaktivitäten liefen in /tmp/mivita-statistics-recheck, einer eigenen Kopie mit separaten vendor-Dateien, isolierter Testkonfiguration und SQLite :memory:. Die neuen/angepassten Dateien wurden aus dem Arbeitsstand übernommen. Die Bestandsdatenbank wurde ausschließlich für die obigen lesenden Abfragen verwendet.
| Lauf | Ergebnis |
|---|---|
| Bestehende Statistik-Kernmodule plus beide neuen Storno-Testdateien | 120 bestanden, 711 Assertions |
| Zusätzliche Gegenproben R01–R04 | 4 bestanden, 12 Assertions; R01–R03 bestätigen ausdrücklich das beanstandete Ist-Verhalten, R04 bestätigt die gelungene reguläre Korrektur |
| Erweiterter Lauf Incentive/Sponsor-Reparatur/Storno/DATEV | 98 bestanden, 3 fehlgeschlagen, 275 Assertions |
| Derselbe erweiterte Filter auf isoliertem Stand vor den jetzigen Korrekturen | 87 bestanden, dieselben 3 fehlgeschlagen, 242 Assertions |
Die drei Fehler betreffen IncentiveTeaserPageTest (Galerie) und zwei Fälle in RegenerateCancellationInvoicePdfCommandTest. Sie treten im gleichen isolierten Umfeld bereits ohne die jetzigen Änderungen auf. Damit sind sie keine nachgewiesenen Regressionen dieser Korrektur. Der Filter Storno erfasst auch Testbeschreibungen und schließt deshalb PDF-Regenerierungstests ein. Die in der Bewertung genannte Zusammenstellung „245 bestanden, 1 Fehler“ wurde ohne deren vollständigen Testbefehl nicht als identischer Lauf behauptet.
Die PHPUnit-Warnung zur veralteten XML-Konfiguration besteht weiterhin. Es wurden kein umfassender Browser-/Lasttest, keine echte Rechnungserstellung mit Versand und kein Zahlungsabgleich ausgeführt.
Die initiale Einrichtung der zusätzlichen Fixtures erforderte das Entfernen doppelter Hilfsfunktionsnamen und das explizite Setzen von status_points=1, bevor ein direkt erzeugtes Eloquent-Objekt an den Writer übergeben wurde. Diese Fixturefehler sind keine Anwendungsbefunde. Die oben genannten finalen Ergebnisse stammen aus dem bereinigten Lauf.
7. Ausgeführte Testbefehle
Im isolierten Nachprüfungsverzeichnis:
php artisan test --compact --filter='StornoClosedMonthTest|StornoPointsCategoryTest|BackofficeStatistics|BackofficeDashboardService|PersonalStatisticsService|HomeMonthlyStats|PaylineForecastService|LiveCommissionService|TeamAboOverview|TeamFirstlinesPage|TopResultsService|AnnualFeeDueService|CustomerAboListView|PointsAboHighlight|TeamStructureAboBadges|TeamCustomerAbosRedirect'
php artisan test --compact --filter=StatisticsRecheckProbeTest
php artisan test --compact --filter='Incentive|SponsorBuyingPointsRepairTest|Storno|DatevExportCancellationTest'
Der letzte Befehl wurde zusätzlich in /tmp/mivita-statistics-audit mit dem Stand vor den jetzigen Korrekturen ausgeführt, um die drei Fehlschläge abzugrenzen. Keine dieser Prüfungen änderte die Testdateien im Repository.
8. Integrität und dauerhafte Nachweise
Vor Beginn der Nachprüfung wurde eine neue SHA-256-Prüfbasis für 10.391 vorhandene Dateien erstellt. Der Abschlussvergleich erfolgt gegen diese Basis einschließlich des Bewertungsdokuments und aller inzwischen erfolgten Implementierungskorrekturen. Neu angelegt wurde nur dieses Nachprüfungsdokument. Die vorangegangenen Auditberichte und die Bewertung wurden nicht redigiert.
Die nachfolgenden Anhänge enthalten die ausgeführten zusätzlichen Gegenproben und anonymen Aggregatergebnisse dauerhaft im Auditordner. Die temporären Dateien werden zum Verständnis des Ergebnisses nicht benötigt.
Anhang A: Zusätzliche isolierte Gegenproben
Die Erwartungen von R01–R03 beschreiben den beobachteten Fehlerzustand. Bei einer späteren Behebung müssen daraus Soll-Regressionen werden; der grüne Gegenproben-Lauf bedeutet keine Freigabe dieser drei Verhaltensweisen. R04 prüft dagegen bereits das gewünschte Ergebnis. Die Beispiele wurden am 09.09.2026 ausgeführt; R03 wechselt die Carbon-Uhr bewusst relativ zum realen Systemmonat.
<?php
use App\Models\{Setting, Product, ShoppingOrder, ShoppingOrderItem, UserAbo, UserAboItem, UserAccount, UserBusiness, UserSalesVolume};
use App\Services\Backoffice\{BackofficeDashboardService, SalesVolumePoints};
use App\Services\BusinessPlan\SalesPointsVolume;
use App\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\{Cache, DB};
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
beforeEach(function () { Cache::flush(); Carbon::setTestNow('2026-09-09 12:00:00'); });
afterEach(fn () => Carbon::setTestNow());
function recheckRecord(string $case, array $data): void {
file_put_contents('/tmp/statistics-recheck-results.jsonl', json_encode(['case'=>$case]+$data)."\n", FILE_APPEND);
}
function recheckUser(?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 recheckVolume(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_points'=>1,'status_turnover'=>null,'points'=>$points,'total_net'=>$points]);
}
function recheckOrder(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']);
}
it('R01 addon split remains wrong after cancellation through the corrected writer', function () {
$u=recheckUser(); $a=recheckUser($u->id); $o=recheckOrder($a);
ShoppingOrderItem::forceCreate(['shopping_order_id'=>$o->id,'product_id'=>0,'qty'=>1,'price'=>20,'points'=>20,'is_abo_addon'=>true]);
$v=recheckVolume($a,100,2,$o);
$cancel=SalesPointsVolume::cancelSalesPointsVolume($v,9991);
recheckVolume($a,100,2,recheckOrder($a));
$r=app(BackofficeDashboardService::class)->overview($u,9,2026)['top_customer_abos']['by_points'][0];
recheckRecord('R01',['original_turnover'=>$v->status_turnover,'cancel_turnover'=>$cancel->status_turnover,'actual'=>$r['points_actual'],'addon'=>$r['points_addon'],'recurring'=>$r['points_recurring']]);
expect($cancel->status_turnover)->toBe(2)->and($r['points_actual'])->toBe(100.0)->and($r['points_addon'])->toBe(20.0)->and($r['points_recurring'])->toBe(80.0);
});
it('R02 cancellation of manual shop-net credit does not reverse its original point categories', function () {
$u=recheckUser(); $v=recheckVolume($u,100,4); $v->update(['status_turnover'=>2,'status_points'=>2]);
SalesPointsVolume::reCalculateSalesPointsVolume($u->id,9,2026);
$before=$v->fresh();
$cancel=SalesPointsVolume::cancelSalesPointsVolume($v,9992)->fresh();
$aggregate=UserSalesVolume::where('user_id',$u->id)->selectRaw(SalesVolumePoints::ownPoints().' as own')->selectRaw(SalesVolumePoints::externalPoints().' as shop')->selectRaw(SalesVolumePoints::nonPaylinePoints().' as nonpayline')->first();
recheckRecord('R02',['before_KP'=>$before->month_KP_points,'before_shop_points'=>$before->month_shop_points,'after_KP'=>$cancel->month_KP_points,'after_shop_points'=>$cancel->month_shop_points,'after_shop_net'=>$cancel->month_shop_total_net,'aggregate_own'=>(float)$aggregate->own,'aggregate_shop'=>(float)$aggregate->shop,'nonpayline'=>(float)$aggregate->nonpayline]);
expect($cancel->month_KP_points)->toBe(100.0)->and($cancel->month_shop_points)->toBe(-100.0)->and((float)$aggregate->nonpayline)->toBe(100.0);
});
it('R03 month check ignores the frozen application clock', function () {
$month=(int)date('m'); $year=(int)date('Y');
Carbon::setTestNow(Carbon::create($year,$month,15)->addMonth());
$u=recheckUser();$v=recheckVolume($u,100,2);$v->update(['month'=>now()->month,'year'=>now()->year]);
$open=SalesPointsVolume::isStornoMonthOpen($v);
recheckRecord('R03',['system_month'=>date('Y-m'),'app_month'=>now()->format('Y-m'),'original_month'=>$v->year.'-'.str_pad($v->month,2,'0',STR_PAD_LEFT),'considered_open'=>$open]);
expect($open)->toBeFalse();
});
it('R04 ordinary customer cancellation now reverses totals and shop basis exactly once', function () {
$u=recheckUser(); $v=recheckVolume($u,100,2); $c=SalesPointsVolume::cancelSalesPointsVolume($v,9993)->fresh();
$total=SalesVolumePoints::totalPointsByUser([$u->id],9,2026)->get($u->id);
recheckRecord('R04',['turnover'=>$c->status_turnover,'KP'=>$c->month_KP_points,'shop_points'=>$c->month_shop_points,'shop_net'=>$c->month_shop_total_net,'total'=>$total]);
expect($c->status_turnover)->toBe(2)->and($c->month_shop_points)->toBe(0.0)->and($c->month_shop_total_net)->toBe(0.0)->and($total)->toBe(0.0);
});
Aufgezeichnete Ergebnisse:
{"case":"R01","original_turnover":null,"cancel_turnover":2,"actual":100,"addon":20,"recurring":80}
{"case":"R02","before_KP":100,"before_shop_points":0,"after_KP":100,"after_shop_points":-100,"after_shop_net":0,"aggregate_own":100,"aggregate_shop":-100,"nonpayline":100}
{"case":"R03","system_month":"2026-09","app_month":"2026-10","original_month":"2026-10","considered_open":false}
{"case":"R04","turnover":2,"KP":0,"shop_points":0,"shop_net":0,"total":0}
Anhang B: Anonyme Datenbankaggregate
Erste Abfragegruppe:
{
"status_counts": [
{
"status": 0,
"status_turnover": null,
"n": 1
},
{
"status": 1,
"status_turnover": null,
"n": 19525
},
{
"status": 2,
"status_turnover": null,
"n": 24151
},
{
"status": 3,
"status_turnover": null,
"n": 8
},
{
"status": 4,
"status_turnover": 1,
"n": 76
},
{
"status": 4,
"status_turnover": 2,
"n": 13
},
{
"status": 5,
"status_turnover": null,
"n": 647
},
{
"status": 6,
"status_turnover": null,
"n": 20
}
],
"users": [
{
"total": 1634,
"vip": "7",
"english": "5"
}
],
"history": [
{
"action": "paused",
"n": 6
},
{
"action": "cancelled",
"n": 44
},
{
"action": "deactivated",
"n": 10
}
],
"deactivated_now": [
{
"deactivated_abos": 10,
"active_history_rows": "0"
}
],
"storno_orders": [
{
"payment_for": 1,
"n": 1
},
{
"payment_for": 6,
"n": 13
},
{
"payment_for": 3,
"n": 6
}
],
"same_month_shop": [
{
"n": 7,
"cancelled_net": "-596.05",
"hypothetical_margin_30": "178.81"
}
],
"turnover_schema": [
{
"COLUMN_TYPE": "tinyint unsigned",
"IS_NULLABLE": "YES",
"COLUMN_DEFAULT": null
}
]
}
Ergänzende Abfragegruppe zur Rundung und gespeicherten Shop-Basis:
{
"users_scope": [
{
"physical_rows": 1634,
"not_deleted": "901"
}
],
"rounding": [
{
"affected_user_months": 6,
"net": "-596.05",
"round_after_sum": "178.82",
"sum_after_round": "178.81"
}
],
"stored_business": [
{
"matches": 6,
"min_margin": "30.00",
"max_margin": "30.00",
"commission_basis_difference": "178.82",
"existing_commission_mismatches": "1"
}
]
}
Wesentliche Abfragen zur Reproduktion; ausschließlich lesend in einer entsprechenden Read-only-Transaktion ausführen. Keine Identitäten ausgeben:
SELECT status, status_turnover, COUNT(*) AS n
FROM user_sales_volumes
GROUP BY status, status_turnover
ORDER BY status, status_turnover;
SELECT COUNT(*) AS physical_rows,
SUM(deleted_at IS NULL) AS not_deleted,
SUM(admin >= 1 AND admin < 4 AND deleted_at IS NULL) AS vip,
SUM(lang = 'en' AND deleted_at IS NULL) AS english
FROM users;
SELECT action, COUNT(*) AS n
FROM user_abo_status_histories
GROUP BY action;
SELECT COUNT(DISTINCT a.id) AS deactivated_abos,
SUM(a.active = 1) AS active_history_rows
FROM user_abo_status_histories h
JOIN user_abos a ON a.id = h.user_abo_id
WHERE h.action = 'deactivated';
SELECT o.payment_for, COUNT(*) AS n
FROM user_sales_volumes v
JOIN shopping_orders o ON o.id = v.shopping_order_id
WHERE v.status = 6
GROUP BY o.payment_for;
SELECT COUNT(*) AS matches,
MIN(b.margin_shop) AS min_margin,
MAX(b.margin_shop) AS max_margin,
SUM(ROUND(b.sales_volume_total_shop * b.margin_shop / 100, 2)
- ROUND((b.sales_volume_total_shop + q.net) * b.margin_shop / 100, 2))
AS commission_basis_difference,
SUM(ABS(b.commission_shop_sales
- ROUND(b.sales_volume_total_shop * b.margin_shop / 100, 2)) > 0.005)
AS existing_commission_mismatches
FROM (
SELECT c.user_id, c.month, c.year, SUM(c.total_net) AS net
FROM user_sales_volumes c
JOIN user_invoices i ON i.cancellation_id = c.user_invoice_id
JOIN user_sales_volumes v ON v.user_invoice_id = i.id
WHERE c.status = 6 AND v.status = 2
AND c.month = v.month AND c.year = v.year
GROUP BY c.user_id, c.month, c.year
) q
JOIN user_businesses b
ON b.user_id = q.user_id AND b.month = q.month AND b.year = q.year;
Anhang C: Geprüfte Korrekturfassungen
SHA-256 der zentralen geänderten Dateien:
| Datei | SHA-256 |
|---|---|
app/Services/BusinessPlan/SalesPointsVolume.php |
4d668b5f91885926b40c8f27b9f501f02837d6775f835d851cef33cb58b5895d |
app/Services/Backoffice/SalesVolumePoints.php |
5dcd93954b37f6562ddf07df0895f7c36b02b8648745b79aac2bb78aca0a8fd5 |
resources/views/admin/settings/index.blade.php |
3b39e30d70cf259e85a45ec0090b08d85ee0e554ec25817d750219d105d9e5e8 |
database/migrations/2026_09_09_140000_make_status_turnover_nullable_on_user_sales_volumes.php |
9effa46911e9edc2b67ad190e45018d9bcff755288a8a5148d1d47170e24cc2f |
tests/Feature/StornoClosedMonthTest.php |
e364eac7144875d847c90555d12c12e6994ae13727bf1100c941c2742b07d3e3 |
tests/Feature/StornoPointsCategoryTest.php |
d92a65fb09befad2b0972f040b6925cbc5edf4043ea025a92f3c1aebaa3747b8 |
Abschluss des Dateivergleichs: 10.391 vorhandene Dateien geprüft, 0 verändert oder entfernt; ausschließlich dieses Nachprüfungsdokument neu angelegt. Anwendungscode, bestehende Tests, Konfiguration und vorhandene Dokumente blieben unverändert.