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

7.8 KiB
Raw Blame History

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:


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)

  1. 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.completedsingle_purchases.status = paid). Cashier-Route POST /stripe/webhook ist aktiv.
  2. 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").
  3. VIES-Validierung der USt-ID (aktuell Formatprüfung).
  4. PDF-Erzeugung für MAN-/STR-Rechnungen (Layout inkl. tax_note); Archiv-PDFs existieren bereits on-demand.
  5. ExpireGrandfatheredSubscriptions: Benachrichtigung zur Umstellung auf neue Tarife am grandfathered_until (D-13-Rest).
  6. Steuerberater-Abnahme der USt-Regeln und Rechnungstexte vor dem ersten produktiven MAN-/STR-Lauf.