presseportale/docs/user-admin/Billing-und-Rechnungskreise.md
Kevin Adametz c8dc99c3c8 Phase 9E (Abschluss): Checkout-Flows und Plan-Kontingent statt Quota-Stub
- Checkout-Backend: me.checkout.subscription (Tarif-Abo monatlich/jährlich)
  und me.checkout.single-pm (Einzel-PM 19 € netto, pending-Kauf mit
  Webhook-Erfüllung); StripeCheckoutService als mockbarer Stripe-Wrapper;
  Stripe Tax via Cashier::calculateTaxes() (Netto-Preise, USt-ID-Abfrage)
- Slot-Logik: Kontingent aus dem Tarif (plans.press_release_quota) plus
  bezahlte Einmalkäufe; Verbrauch bei Veröffentlichung zuerst aus dem
  Plan-Zähler, danach Einlösung des ältesten Einmalkaufs (consumed +
  PM-Verknüpfung); Grandfathered = unbegrenzt (Entscheidung 12.06.2026,
  Bestandsschutz); Stub-Spalte users.press_release_quota entfernt
- billing:sync-stripe-plans legt zusätzlich das Einzel-PM-Produkt an
  (STRIPE_PRICE_SINGLE_PM); Test-Mode-Sync gelaufen
- Buchungs-Seite: Rückmeldung nach Checkout (erfolg/abbruch/Guard-Hinweis)
- Tests: PressReleaseQuotaTest auf Plan-Semantik neu geschrieben,
  CheckoutFlowTest (8 Tests), Modal-/API-Tests angepasst; Suite 510 passed
- Doku: Billing-und-Rechnungskreise (Kontingent-Tabelle, Checkout-Routen,
  Webhook-Events, Stripe-CLI-Hinweis), PHASE-9-Plan 9E , Checkliste,
  STATUS-ABGLEICH, PROGRESS

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

11 KiB
Raw Blame History

Billing & Rechnungskreise (hybrides Modell)

Stand: 12.06.2026 — Datenmodell, MAN-Kreis, USt-Behandlung (Phase 9D) sowie Stripe-Sync, Webhook-Verarbeitung, Checkout-Flows und Plan-Kontingent (Phase 9E) umgesetzt. Es fehlt die Checkout-UI (Phase 9F).

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.

PM-Kontingent (User::pressReleaseQuotaRemaining(), null = unbegrenzt):

Wer Kontingent
Launch-Schalter billing.enforce_booking aus unbegrenzt (Vor-Launch-Zustand)
Bestandskunde (aktive/grandfathered Legacy-Vereinbarung) unbegrenzt — Entscheidung 12.06.2026: Bestandsschutz gilt, das Alt-Produkt sah unbegrenzte PMs vor; eine Migration auf neue Tarife kommt ggf. später
Abonnent Monatskontingent des Tarifs (plans.press_release_quota) plus offene Einmalkäufe
Nur Einmalkäufe Anzahl bezahlter, noch nicht eingelöster Einzel-/Extra-PM-Käufe

Slot-Verbrauch (erst bei Veröffentlichung, Decision-Update §3.2; Ablehnung kostet nichts; Re-Publish nach Archivierung zählt nicht doppelt): zuerst das Plan-Monatskontingent (Zähler users.press_release_quota_used_this_month, monatlicher Reset), danach wird der älteste bezahlte Einmalkauf eingelöst (single_purchases.status → consumed, verknüpft mit der PM). Die frühere Stub-Spalte users.press_release_quota ist entfernt.

Checkout-Einstiege (Phase 9E; UI-Anbindung folgt in 9F):

Route Zweck
me.checkout.subscription (/admin/me/checkout/abo/{slug}/{monthly|yearly}) Stripe-Checkout für ein Tarif-Abo
me.checkout.single-pm (/admin/me/checkout/einzel-pm) Stripe-Checkout Einzel-PM (legt single_purchases-Eintrag pending an; Webhook setzt paid)

Erfolg/Abbruch landen auf der Buchungs-Seite (?checkout=erfolg|abbruch). Die Steuer ergänzt Stripe Tax automatisch (Cashier::calculateTaxes() im AppServiceProvider, Netto-Preise mit tax_behavior: exclusive) — nach denselben Regeln wie der VatResolver im MAN-Kreis, inkl. USt-ID-Abfrage im Checkout.


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
billing:sync-stripe-plans Tarife + Einzel-PM als Netto-Produkte/Preise nach Stripe synchronisieren (idempotent; --dry-run) manuell
legacy:grandfather-subscriptions Aktive Legacy-Abos aus dem Archiv migrieren manuell (Migrations-Runbook)
press-releases:reset-monthly-quota Monatlicher Reset des Plan-Kontingent-Zählers (press_release_quota_used_this_month) 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:

ENV Bedeutung
STRIPE_KEY / STRIPE_SECRET Publishable/Secret Key (Test-Keys gesetzt)
STRIPE_WEBHOOK_SECRET Signatur-Prüfung des Webhook-Endpoints (gesetzt; Endpoint https://pressekonto.com/stripe/webhook im Dashboard registriert, 12.06.2026)
STRIPE_PRICE_SINGLE_PM Stripe-Price-ID der Einzel-PM (legt billing:sync-stripe-plans an; ohne sie ist der Einzel-PM-Checkout deaktiviert)
CASHIER_CURRENCY / CASHIER_CURRENCY_LOCALE eur / de_DE (gesetzt)

Benötigte Webhook-Events am Stripe-Endpoint: invoice.payment_succeeded (STR-Spiegelung), checkout.session.completed (Einmalkauf-Erfüllung) sowie die Cashier-Standardevents customer.subscription.created/updated/deleted, customer.updated, customer.deleted (Abo-Zustand).

Lokal testen (der registrierte Endpoint zeigt auf die Live-Domain und läuft bis zum Relaunch ins Leere — das ist unkritisch, Stripe versucht die Zustellung nur erneut): Stripe CLI verwenden — stripe listen --forward-to pressekonto.test/stripe/webhook und das von der CLI ausgegebene whsec_… temporär als STRIPE_WEBHOOK_SECRET in die .env.


7. Offene Punkte (Stand 12.06.2026, nach Phase 9E)

  1. 9E erledigt: Stripe-Sync (Tarife + Einzel-PM als Netto-Produkte, Test-Mode), Webhook-Verarbeitung (STR-Spiegelung, Einmalkauf-Erfüllung, Endpoint + Secret eingerichtet), Checkout-Flows (Abo + Einzel-PM, Routen siehe Abschnitt 2), Slot-Logik auf Plan-Kontingent umgestellt (Grandfathered = unbegrenzt, Entscheidung 12.06.2026), Stub-Spalte entfernt, Stripe Tax aktiviert (Cashier::calculateTaxes()).
  2. Phase 9F: Tarif-Seite/Buchungs-UI an die Checkout-Routen anbinden (die Buchungs-Seite ist noch Konzept-Mock mit deaktivierten Buttons); echte Tarif-/Buchungsdaten statt Platzhalter anzeigen.
  3. Stripe Tax im Dashboard aktivieren (Ursprungsadresse/Registrierung hinterlegen) — ohne das schlägt der Checkout mit automatischer Steuer fehl. Im Test-Mode prüfen, dann im Live-Mode wiederholen; dort auch billing:sync-stripe-plans erneut ausführen (Live-Produkt-IDs).
  4. VIES-Validierung der USt-ID (aktuell Formatprüfung; Stripe prüft die im Checkout erfasste USt-ID asynchron selbst).
  5. PDF-Erzeugung für MAN-/STR-Rechnungen (Layout inkl. tax_note); Archiv-PDFs existieren bereits on-demand.
  6. ExpireGrandfatheredSubscriptions: Benachrichtigung zur Umstellung auf neue Tarife am grandfathered_until (D-13-Rest); erst dann wird das unbegrenzte Bestandskontingent ggf. migriert.
  7. Steuerberater-Abnahme der USt-Regeln und Rechnungstexte vor dem ersten produktiven MAN-/STR-Lauf.
  8. Tageslimit (plans.daily_limit) durchsetzen — Phase 9G.