# 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