mivita/dev/2026-07-17-steuern/UMSETZUNG-fixes.md
Kevin ac3bd6e8ea Statistiken/Abo: Kundenfeedback 2026-07-24 komplett umgesetzt + Steuern: DATEV-Fixes 08+09/2025 (Sammel-Commit Branch-WIP)
Feedback-Umsetzung (dev/2026-07-22/feedback-analyse-2026-07-24.md, alle 10 Punkte):
- VIP-Statistik: eigener Berater ('ich', Linie 0) in Neue-Kundenabos- und beiden Top-10-Listen; Top-10-Umsatz sortiert nach abgerechneten Ist-Punkten + Abo-Anzahl-Spalte; Fußnote nicht-payline-wirksame Punkte (SalesVolumePoints::nonPaylinePoints)
- Führungskräfte-Liste: beide Sichten 'direkt' (an nächster FK abgeschnitten, additiv) und 'gesamt' (komplette Downline), Gesamt-Spalte nach Kundenzählweise
- Team-Abos: je Ebene 'abgerechnet (Monat)' und 'bis zum 25. erwartet'; Payline-Karte mit Teamabo-/Kundenabo-Zählern
- Incentive-Live-Ranking: Qualifizierte zuerst nummeriert (fixt auch isWinner rank<=max_winners)
- Porto: Netto-/Brutto-Kennzeichnung (Berater netto, Endkunden inkl. MwSt., portal_view-Flag; auch CRM-/Shop-Warenkorb + Checkout), Yard::shipping_tax-Fix
- Struktur-Ansicht: Abo-Badges 'A' (eigenes Abo) und '(n)' (Kundenabos) je Teampartner, gebatcht
- Abo-Löschung: Softdelete setzt active=0 (UserAbo deleting-Hook) + Bereinigungs-Migration für gelöschte-aber-aktive Abos
- Doku: technische Analyse + Kundinnen-Feedback-Dokument unter dev/2026-07-22/
- Tests: IncentiveRankingTest, TeamStructureAboBadgesTest, YardShippingTaxTest (neu); BackofficeStatisticsListsTest, TeamAboOverviewTest, PaylineForecastServiceTest, AboAdminDeleteTest, AboOneTimeViewTest erweitert

Außerdem enthalten (Branch-WIP vor dem Feedback): DATEV-Abgleich-Fixes 08+09/2025, USt-ID-Live-Validierung sowie restliche unkommittete Stände (u. a. Abo-Pause-Sprachkeys).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:56:02 +00:00

9.9 KiB

Steuerberater-Abgleich 08+09/2025 — Analyse & Umsetzung der Fixes

Datum: 17.07.2026 Anlass: Korrektur-CSVs des Steuerberaters (Abgleich 08._09.2025-Konto-*.csv in diesem Ordner) zum DATEV-Export aus admin/payments/taxadvisor. Betroffene Exporte: datev_exports #9 (08/2025) und #10 (09/2025), generiert am 08.05.2026.


Lesehilfe zu den Abgleich-CSVs

Die Spalte „Betrag Test" des Steuerberaters ist nicht unser Exportbetrag, sondern der Saldo auf dem Erlöskonto nach DATEV-Steuerautomatik: Wir exportieren Bruttobeträge mit BU-Schlüssel (9 = 19 %, 8 = 7 %), DATEV rechnet die USt automatisch heraus.

Beispiel: Export 277,92 € brutto (BU 9) → 233,55 € netto auf 8400 = „Betrag Test".

Mit diesem Schlüssel lassen sich fast alle gemeldeten Zeilen exakt nachvollziehen.


Befund 1: Beträge ≥ 1.000 € massiv zu niedrig (kritisch)

Gemeldete Fälle: 202536817 (1.434,71 statt 230,28), 202536968 (1.044,69 statt 167,67), 202537532 (1.075,04 statt 172,55)

Ursache: ShoppingOrder::makeTaxSplit() speicherte net_split/tax_split mit number_format($value, 2) — also mit Tausendertrennzeichen ("1,434.70"). DatevExportService::parseNumber() machte daraus per str_replace(',', '.') den String "1.434.70" → floatval() = 1,43 €. Jede Rechnung mit ≥ 1.000 € Netto pro Steuersatz war im Export um den Faktor ~1000 zu niedrig.

Fix:

  • app/Services/DatevExportService.php → parseNumber() erkennt jetzt US-Format (1,434.70), deutsches Format (1.434,70) und reine Tausendertrenner (1,434) korrekt. Nötig, weil die Altdaten in der DB das fehlerhafte Format behalten.
  • app/Models/ShoppingOrder.php → makeTaxSplit() und makeHomepartyTaxSplit() schreiben neu mit number_format($value, 2, '.', '') (ohne Tausendertrenner).

Befund 2: Falsche Kontenzuordnung 8125 (igL) ↔ 8400/8300

Gemeldete Fälle (Auswahl): 202536848, 202536858, 202536910, 202537173, 202537290, 202538011, 202538144, 202538445/446, 202539399 (gehörten auf 8125, standen auf 8400) — umgekehrt 202536841, 202537251, 202537460, 202537543, 202538398 (standen auf 8125, gehörten auf 8400/8300).

Ursachen (mehrere):

  1. Export nutzte den aktuellen Account-Zustand statt des Rechnungszustands. Die betroffenen Berater (z. B. User 781, 850, 974) hatten ihre USt-ID nach dem Kauf wieder gelöscht (reverse_charge_delete leert alle Felder). Die Rechnungen wurden damals korrekt ohne USt (igL) ausgestellt, der Export fand aber keine USt-ID mehr und buchte auf 8400.
  2. Keinerlei Validierung der USt-ID: hasValidUstid prüfte nur „nicht leer". Dadurch lösten auch deutsche USt-IDs (202536841: DE296797691), offensichtliche Dummy-IDs (202538398: DE123456789) und formal ungültige Werte (202537460: 53618009 — gar kein USt-ID-Format) eine igL-Buchung auf 8125 aus.
  3. Abgelaufene/ungültige IDs: Die VIES-Prüfung lief nur einmalig bei der Eingabe (202537251: ESY9284953M, validiert 08/2023, laut StB inzwischen ungültig).

Datenlage als Beleg: In 08+09/2025 hat jede DE-Bestellung ein tax_split; EU-Bestellungen ohne tax_split sind exakt die steuerfrei verkauften (AT: 71, ES: 14).

Fix:

  • app/Services/DatevExportService.php → determineRevenueAccount(): Maßgeblich ist jetzt, wie die Rechnung tatsächlich ausgestellt wurde:
    • EU-Bestellung ohne tax_split (= ohne USt verkauft) → 8125 / BU 1 (igL) — unabhängig vom heutigen Account-Zustand.
    • EU-Bestellung mit tax_split (= mit USt berechnet) → 8400/8300 nach Steuersatz, nie 8125.
    • Drittland unverändert 8120 / BU 11.
  • resolveEuUstid() verwirft deutsche und formal unplausible USt-IDs (neuer zentraler App\Services\VatIdService). Fehlt bei einer igL-Zeile die USt-ID, erscheint wie bisher eine Validierungs-Warnung im Export (validation_summary).

Befund 3: „Betrag fehlerhaft" durch doppelte Rechnungsnummern

Gemeldete Fälle: 202537208, 202537572, 202538182, 202538332 — exakt die vier Nummern, die in 08+09/2025 doppelt vergeben wurden (je zwei verschiedene Rechnungen/Bestellungen). Der StB sah unter einer Belegnummer die Summe zweier Rechnungen.

Ursache: Historisches Race bei der Nummernvergabe (Zähler-Setting invoice-number). Für Rechnungen ist das seit Invoice::makeNextInvoiceNumber() behoben (DB::transaction + lockForUpdate). Gutschriften nutzten aber denselben Zähler weiterhin ungesichert: CreditRepository::create() las die Nummer (plain read) und zählte erst viel später in finishUserCredit() hoch — dazwischen konnten parallele Läufe dieselbe Nummer ziehen. Folge in den Bestandsdaten: 38 Gutschriften teilen sich ihre laufende Nummer mit einer Rechnung.

Fix:

  • app/Services/Credit.php → makeNextCreditNumber() delegiert an Invoice::makeNextInvoiceNumber() (atomar, gelockt).
  • app/Repositories/CreditRepository.php → Nummer wird direkt bei der Erstellung atomar reserviert; das nachträgliche Hochzählen in finishUserCredit() entfällt.

Bestehende Duplikate in den Altdaten bleiben unverändert (nicht rückwirkend korrigierbar).


Befund 4: Gutschriften auf 4764 mit falschem BU-Schlüssel

Gemeldete Fälle: GS202536995 (67,47 statt 56,70), GS202538597 (14,88 statt 12,50)

Ursache: Beide Gutschriften wurden mit taxable = 3 (nicht USt-pflichtig / Ausland) ausgestellt — der Beleg weist keine USt aus. Der Export leitete den BU-Schlüssel aber vom aktuellen Account-Status ab und exportierte BU 9 (19 %) → DATEV rechnete fälschlich 19 % aus dem steuerfreien Betrag heraus.

Fix:

  • app/Services/DatevExportService.php → determineCommissionTaxStatus() nutzt jetzt den taxable-Snapshot der Gutschrift (1 = BU 9, 2 = Kleinunternehmer BU 50, 3 = Ausland → Reverse Charge BU 94 bei EU). Nur Alt-Gutschriften ohne Snapshot (taxable = 0) fallen auf die Account-Logik zurück. Der Brutto-Aufschlag nutzt den in der Gutschrift gespeicherten tax_rate.

Befund 5: USt-ID-Prüfung nur einmalig (Anforderung Kevin)

Anforderung: USt-ID bei jeder Verwendung prüfen (v. a. im Checkout); bei ungültiger ID Hinweis anzeigen und mit USt berechnen; deutsche USt-IDs nie anerkennen.

Umsetzung — neuer zentraler Service app/Services/VatIdService.php:

  • normalize(), isPlausibleFormat() (EU-Länderpräfix + Nummernteil), qualifiesForReverseCharge() (Format ok und nicht DE...), checkVies() (SOAP gegen ec.europa.eu VIES, Timeout konfigurierbar), checkViesCached() (24 h Cache; VIES-Ausfall nur 10 Min. gecacht).
  • Eingabe im Account (UserRepository::reverse_charge_activate): nutzt den Service; deutsche USt-IDs → eigene Fehlermeldung (msg.VATID_german_not_allowed, in de/en/es/fr ergänzt). SSL-Verifikation ist jetzt aktiv (vorher deaktiviert).
  • Checkout (UserService::performUserTaxShippingCountry): Wenn Reverse-Charge greifen würde, wird die USt-ID live (gecacht) geprüft:
    • ungültig / DE / Format falsch → Reverse-Charge-Status wird entzogen (reverse_charge = 0, reverse_charge_valid = null; die USt-ID selbst bleibt zur Neu-Validierung erhalten), Growl-Hinweis msg.VATID_no_longer_valid_taxes_applied, Bestellung läuft mit USt weiter.
    • VIES nicht erreichbar → gespeicherter Status gilt (Checkout wird nie blockiert), Log-Warnung.
  • Konfiguration: services.vies.enabled (VIES_ENABLED, Default true) und services.vies.timeout (VIES_TIMEOUT, Default 10 s). In phpunit.xml deaktiviert.

Nicht im Code fixbar / offene Punkte

Punkt Status
202506145 / 202506147: existieren nicht in user_invoices — stammen aus einem fremden Nummernkreis des Kunden (dort werden teils eigene Rechnungen + Stornorechnungen erstellt) Im Abgleich ignorieren und als „extern" markieren (Vorgabe Kevin)
Stornos 2025 (202536978, 202537144, 202537513 teilweise, 202539501): im System nicht als Storno erfasst — Stornorechnungen (cancellation = 1) existieren erst ab 01/2026 Manuelle Abstimmung mit dem StB / Datenpflege
202535876: Juli-Rechnung, Gutschrift im September — monatsübergreifende Korrekturen kennt der Export konzeptionell nicht Offen, bei Bedarf separates Konzept
202539011: „Rechnung liegt uns nicht vor" — Daten sind im System vorhanden (66,22 € + 285,18 €) PDF an StB nachliefern
Exporte 08+09/2025 neu generieren, sobald die Fixes deployed sind To-do nach Merge

Tests

  • tests/Unit/Services/DatevExportServiceTest.php — erweitert: Tausendertrenner-Parsing, igL-Semantik (soldTaxFree), Ablehnung deutscher/unplausibler USt-IDs, taxable-Snapshot-Logik der Gutschriften.
  • tests/Unit/Services/VatIdServiceTest.php — neu: Normalisierung, Formatprüfung, DE-Ablehnung, VIES-Verhalten (deaktiviert/unplausibel).
  • tests/Feature/CheckoutVatIdRevalidationTest.php — neu: 6 Checkout-Szenarien (VIES ok / ungültig / ausgefallen, DE-ID, Format-Müll, Ländercode-Mismatch).

Alle 63 betroffenen Tests grün; ./vendor/bin/pint sauber. Vorbestehende, unabhängige Testfehler (InvoiceServiceTest, tests/Unit/BusinessPlan/*, makeUser()-Kollision der Gesamtsuite) bestehen unverändert auch auf dem Stand vor diesen Änderungen.

Geänderte Dateien

  • app/Services/DatevExportService.php — parseNumber, Kontenlogik, USt-ID-Filter, Gutschrift-BU
  • app/Services/VatIdService.php — neu, zentrale USt-ID-Prüfung
  • app/Services/UserService.php — Live-Prüfung im Checkout + Entzug + Hinweis
  • app/Services/Credit.php, app/Repositories/CreditRepository.php — atomare Nummernvergabe
  • app/Repositories/UserRepository.php — VIES via Service, DE-Ablehnung bei Eingabe
  • app/Models/ShoppingOrder.php — number_format ohne Tausendertrenner
  • config/services.php, phpunit.xml — VIES-Konfiguration
  • resources/lang/{de,en,es,fr}/msg.php — neue Meldungen