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>
181 lines
9.9 KiB
Markdown
181 lines
9.9 KiB
Markdown
# 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
|