presseportale/docs/user-admin/Billing-und-Rechnungskreise.md
Kevin Adametz 38fab64e10 Phase 9E (Backbone): Stripe-Produkt-Sync und Webhook-Verarbeitung mit STR-Spiegelung
- billing:sync-stripe-plans: legt die Tarife als Netto-Produkte/Preise
  (tax_behavior exclusive, EUR) in Stripe an und pflegt die IDs zurueck
  nach plans; idempotent, --dry-run. Gegen Stripe Test-Mode ausgefuehrt —
  alle 4 Tiers verknuepft.
- ProcessStripeWebhook (Listener auf Cashier WebhookReceived):
  - invoice.payment_succeeded -> Spiegelung in den lokalen STR-Kreis
    (fortlaufende Nummer via InvoiceNumberGenerator, Adress-Snapshot
    bevorzugt aus dem Stripe-Payload inkl. lokaler USt-ID, Status paid,
    idempotent gegen doppelte Zustellung)
  - checkout.session.completed -> markiert den referenzierten
    single_purchases-Datensatz als bezahlt (Metadata single_purchase_id)
- CASHIER_CURRENCY=eur (+ Locale de_DE); Cashier-Webhook-Route aktiv
- Doku: Billing-Referenz §7 + Phase-9-Plan (9E-Backbone) aktualisiert

Offen fuer 9E-Rest: Checkout-Flows (Abo + Einmalkauf), Webhook-Endpoint
im Stripe-Dashboard + STRIPE_WEBHOOK_SECRET, Slot-Logik auf
Plan-Kontingent (fachliche Frage: Grandfathered = unbegrenzt?).

Tests: StripeWebhookProcessingTest (7, inkl. Event-Wiring).
Suite: 497 passed, 4 skipped. Pint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:07:09 +00:00

154 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Billing & Rechnungskreise (hybrides Modell)
Stand: 12.06.2026 — Datenmodell, MAN-Kreis und USt-Behandlung umgesetzt
(Phase 9D); Stripe-Checkout/Webhooks in Arbeit (Phase 9E).
Dieses Dokument ist die zentrale Referenz für das Abrechnungssystem:
Rechnungskreise, Tarif-Datenmodell, Steuerlogik, Befehle und Konfiguration.
Verwandte Dokumente:
- [`docs/Decision-Update Preisstruktur & Veröffentlichungs-Flow.md`](../Decision-Update%20Preisstruktur%20&%20Ver%C3%B6ffentlichungs-Flow.md) — verbindliche Launch-Entscheidungen (Tarife, Kontingente, Flow, Netto-Preise).
- [`docs/PHASE-9-FLOW-UND-TARIFE-PLAN.md`](../PHASE-9-FLOW-UND-TARIFE-PLAN.md) — Umsetzungsplan mit Päckchen-Status.
- `dev/migration 2026/05-DATABASE-MERGE.md` §5.5/§5.6 — Rechnungsarchiv (D-12) und Grandfathering (D-13).
---
## 1. Die drei Rechnungswelten
| Welt | Präfix | Tabelle | Inhalt |
|---|---|---|---|
| **Stripe-Shop** | `STR-` | `invoices` | Alle **neuen** Abschlüsse (Abos, Einzel-PM, Credits). Abwicklung komplett über Stripe; Rechnungen werden per Webhook in `invoices` gespiegelt und erhalten eine fortlaufende STR-Nummer. *(Spiegelung: Phase 9E)* |
| **Manuell/Legacy** | `MAN-` | `invoices` | Laufende, noch aktive Alt-Zahlungsvereinbarungen ab Relaunch. Fälligkeit wird täglich geprüft, Rechnung wie im Altsystem ausgestellt. |
| **Alt-Archiv** | — | `legacy_invoices` | Read-only Archiv aller importierten Legacy-Rechnungen (D-12). Wird nie verändert; PDFs werden on-demand aus den Archivdaten erzeugt. |
**Rechnungsnummern**: `InvoiceNumberGenerator` vergibt atomar (Row-Lock auf
`invoice_number_sequences`) fortlaufende, lückenlose Nummern pro Kreis:
`STR-00001`, `MAN-00001`, … (Padding: `billing.invoice_number_padding`).
---
## 2. Tarif-Datenmodell
| Tabelle | Zweck |
|---|---|
| `plans` | Tarif-Katalog (Starter/Business/Pro/Agency): Monats-/Jahrespreis **netto**, PM-Kontingent/Monat, Tageslimit, Stripe-Produkt-/Preis-IDs. Seeder: `PlanSeeder` (idempotent). |
| `subscriptions`, `subscription_items` | Laravel-Cashier-Tabellen — Zustand der Stripe-Abos. `User` ist `Billable`. |
| `single_purchases` | Einmalkäufe: Einzel-PM (19 €), Extra-PM, Boost, Veröffentlichungsnachweis-PDF. Status: pending → paid → consumed (oder refunded). |
| `payment_options` / `user_payment_options` | Legacy-Zahlungsvereinbarungen. Grandfathered-Einträge tragen die Netto-Vertragsbasis in `legacy_conditions`; versteckte Katalog-Platzhalter `LEGACY-{PE\|BP}-{Artikel}`. |
| `invoice_number_sequences` | Fortlaufende Nummern pro Rechnungskreis. |
**Submit-Gate** (`User::hasActiveBooking()`, hinter `billing.enforce_booking`):
Eine aktive Buchung ist ein Cashier-Abo **oder** ein bezahlter, noch nicht
eingelöster Einzel-/Extra-PM-Kauf **oder** eine aktive/grandfathered
Legacy-Vereinbarung. Bestandskunden behalten damit nach Gate-Aktivierung
volle Einreichungsrechte.
---
## 3. MAN-Kreis: Fälligkeitslauf für Legacy-Zahlungen
Täglicher Scheduler-Lauf (04:30): `billing:generate-manual-invoices`
1. Findet `user_payment_options` mit Status `active`/`grandfathered`,
**ohne** `stripe_subscription_id`, deren `current_period_end` erreicht ist.
2. Friert die Rechnungsadresse als Snapshot ein (`invoice_billing_addresses`,
inkl. `vat_id`).
3. Stellt die Rechnung aus: Netto-Basis × USt-Regel (Abschnitt 4),
MAN-Nummer, Zahlungsziel `billing.manual_due_days` (Default 14 Tage).
4. Schaltet die Periode weiter (`monthly`/`yearly` aus `legacy_conditions`
bzw. `payment_options.interval`).
Nicht abrechenbare Fälle (fehlende Rechnungsadresse, kein Intervall) werden
geloggt, **die Periode bleibt stehen** — der nächste Lauf versucht es erneut.
Optionen: `--dry-run`, `--limit=50`.
**Befüllung**: `legacy:grandfather-subscriptions` (Migrations-Runbook, nach
`legacy:archive-invoices`) leitet die aktiven jährlichen Vereinbarungen aus
dem Rechnungsarchiv ab — Replay-fähig für den Lauf kurz vor Relaunch.
Details: `dev/migration 2026/05-DATABASE-MERGE.md` §5.6.
---
## 4. USt-Behandlung (Entscheidung 12.06.2026)
**Alle neuen Preise sind Netto-Preise.** Die Steuer wird zur
Rechnungsstellung über `App\Services\Billing\VatResolver` aus der
Rechnungsadresse bestimmt:
| Fall | Behandlung | Rechnung |
|---|---|---|
| Deutschland | immer mit Steuer (`billing.vat_rate`, Default 19 %) | Netto + USt ausgewiesen |
| EU mit gültiger USt-ID | befreit (Reverse Charge) | `is_netto`, Pflichthinweis in `tax_note` |
| EU ohne USt-ID | mit Steuer | Netto + USt ausgewiesen |
| Drittland | grundsätzlich befreit | `is_netto`, Hinweis „nicht im Inland steuerbar" |
- Die USt-ID wird im Profil gepflegt (bestehendes Feld) und zusätzlich an
der Rechnungsadresse (`billing_addresses.vat_id`) gespeichert; jede
Rechnung friert sie im Adress-Snapshot ein.
- „Gültig" = vorhanden + formal plausibel (Länder-Präfix, EL für
Griechenland). **Offen: echte VIES-Validierung** — vor Aktivierung von
Gate/Checkout umsetzen.
- **Legacy-Umrechnung**: Das Altsystem fakturierte brutto (199 € inkl.
Steuer; Befreite mit Netto-Ausweis 167,23 €). Die Grandfather-Migration
leitet daraus die Netto-Basis ab (`legacy_conditions.net_cents`) — für
deutsche Bestandskunden bleibt der Bruttobetrag unverändert, die Steuer
wird künftig nur sauber ausgewiesen.
---
## 5. Befehle & Scheduler
| Befehl | Zweck | Scheduler |
|---|---|---|
| `billing:generate-manual-invoices` | MAN-Fälligkeitslauf (Abschnitt 3) | täglich 04:30 |
| `legacy:grandfather-subscriptions` | Aktive Legacy-Abos aus dem Archiv migrieren | manuell (Migrations-Runbook) |
| `press-releases:reset-monthly-quota` | Quota-Stub-Reset (entfällt mit Plan-Kontingent, 9E) | monatlich, 1. um 00:05 |
---
## 6. Konfiguration
`config/billing.php`:
| Schlüssel | ENV | Default | Bedeutung |
|---|---|---|---|
| `enforce_booking` | `BILLING_ENFORCE_BOOKING` | `false` | Submit-Gate scharf schalten (Launch-Schalter) |
| `invoice_number_padding` | — | `5` | Stellen der laufenden Nummer |
| `manual_due_days` | `BILLING_MANUAL_DUE_DAYS` | `14` | Zahlungsziel MAN-Rechnungen |
| `vat_rate` | `BILLING_VAT_RATE` | `0.19` | USt-Satz für steuerpflichtige Fälle |
| `eu_country_codes` | — | EU-27 ohne DE | Basis der Drittland-/EU-Unterscheidung |
Stripe/Cashier (`config/cashier.php`):
| ENV | Bedeutung |
|---|---|
| `STRIPE_KEY` / `STRIPE_SECRET` | Publishable/Secret Key (Test-Keys gesetzt) |
| `STRIPE_WEBHOOK_SECRET` | Signatur-Prüfung des Webhook-Endpoints — wird beim Einrichten des Endpoints gesetzt (9E) |
| `CASHIER_CURRENCY` | Default `usd` → für uns `eur` setzen (9E) |
---
## 7. Offene Punkte (Stand 12.06.2026, nach 9E-Backbone)
0. **9E-Backbone erledigt**: Tarife liegen als Netto-Produkte/Preise in
Stripe (Test-Mode, `billing:sync-stripe-plans`, IDs in `plans`);
Webhook-Listener `ProcessStripeWebhook` spiegelt bezahlte
Stripe-Rechnungen in den STR-Kreis (fortlaufende Nummer,
Adress-Snapshot aus dem Stripe-Payload, idempotent) und erfüllt
Einmalkäufe (`checkout.session.completed`
`single_purchases.status = paid`). Cashier-Route `POST /stripe/webhook`
ist aktiv.
1. **Phase 9E (Rest)**: Checkout-Flows (Abo + Einmalkauf) inkl.
Buchungs-Seite, Webhook-Endpoint im Stripe-Dashboard registrieren +
`STRIPE_WEBHOOK_SECRET` setzen, Slot-Logik von
`users.press_release_quota`-Stub auf Plan-Kontingent umstellen
(fachlich zu klären: Kontingent-Semantik für Grandfathered —
Legacy-Produkt war „unbegrenzte PMs pro Pressemappe").
2. **VIES-Validierung** der USt-ID (aktuell Formatprüfung).
3. **PDF-Erzeugung** für MAN-/STR-Rechnungen (Layout inkl. `tax_note`);
Archiv-PDFs existieren bereits on-demand.
4. **`ExpireGrandfatheredSubscriptions`**: Benachrichtigung zur Umstellung
auf neue Tarife am `grandfathered_until` (D-13-Rest).
5. **Steuerberater-Abnahme** der USt-Regeln und Rechnungstexte vor dem
ersten produktiven MAN-/STR-Lauf.