Files
HiveGrid-Wireframes/hivegrid-cloud/docs/boilerplate-mapping/M28-subscriptions-billing.md
T

20 KiB
Raw Blame History

Boilerplate-Mapping — M28 Subscriptions / Billing

Modul-ID: M28 Name: Subscriptions / Billing Milestones: M153 (Stripe-Integration EU), M154 (Plan-/Seat-Modell), M155 (Trial + Read-Only), M156 (Metering / Pay-as-you-go) Pflichtenheft-Block: Block 7, Modul 28 (_bmad-output/pflichtenheft/blocks/block7.md §28.1–28.Y) Detail-Spec: _bmad-output/specs/M28-subscriptions-billing.md Reuse-Level: HIGH (Daten-Layer, Stripe-Service-Layer, Pricing-Config-Admin, Pay-as-you-go-Engine fast vollständig vorhanden; Lifecycle-State-Machine, Seat-Modell und Read-Only-Middleware fehlen)

Verifiziert direkt gegen /Users/raffaelsheikh/Documents/GitHub/boilerplate (nicht nur Inventur). Wichtig: Die BP-Modelle bilden den Stripe-Spiegel struktur-getreu ab, weichen aber in mehreren spec-kritischen Punkten ab (kein Status-Enum, kein Seat-Typ, andere Idempotenz-Strategie, sterilere Felder).


Übernehmbar aus Boilerplate

Boilerplate-Pfad Was Wie nutzbar
prisma/schema/subscriptions.prisma Komplettes Subscription-Daten-Modell: SubscriptionProduct, SubscriptionPrice, SubscriptionUsageBasedPrice, SubscriptionUsageBasedTier, SubscriptionFeature, TenantSubscription, TenantSubscriptionProduct, TenantSubscriptionProductPrice, TenantSubscriptionUsageRecord, TenantProductConfiguration, TenantFeatureOverride, CheckoutSessionStatus, Credit anpassen — Modellnamen 1:1 deckungsgleich mit Spec; Felder müssen erweitert werden (siehe Lücken). Als Migrations-Basis übernehmen.
apps/web/app/utils/stripe.server.ts (21 KB) Stripe-Client + ~40 Funktionen: createStripeCheckoutSession, createStripeSetupSession, createStripeSubscription, cancelStripeSubscription, reactivateStripeSubscription, createCustomerPortalSession, getStripeInvoices, getStripeUpcomingInvoice(s), getStripePaymentMethods, createStripeCustomer, Products/Prices CRUD, createStripeUsageBasedPrice, createMeterEvent, listMeterEventSummaries, getStripeCoupon/PromotionCode, Revenue-Aggregation 1:1 — deckt fast alle Stripe-Operationen der Spec (Block C, F, H, I, K) ab. API-Version 2025-10-29.clover. EU-Entity/Tax-Felder konfigurativ.
apps/web/app/utils/services/.server/subscriptionService.ts (23 KB, mit Tests) getPlanFeaturesUsage, getPlanFeatureUsage, getActiveTenantSubscriptions, reportUsage, persistCheckoutSessionStatus, cancelTenantSubscription, getMrr, convertToCurrency, clearTenantSubscriptionCache anpassen — Lifecycle-Helper als Basis; Status-Übergänge (Block B) fehlen und sind zu ergänzen.
apps/web/app/utils/services/.server/subscriptionCache.server.ts Two-Tier-Cache (L1 in-process Map, L2 SharedStore-Pub/Sub) mit TTL-Default 5 min + Cross-Replica-Invalidation anpassen — Spec verlangt 30 s TTL (Block G) statt 5 min; ansonsten 1:1 das geforderte Cache-Invalidate-Pattern.
apps/web/app/utils/services/.server/pricingService.ts (23 KB) Pricing-Berechnung / Plan-Preis-Auflösung anpassen — Basis für Setup-Fee-Differenz-Logik (Block K) und Proration.
apps/web/app/utils/services/.server/tenantPlanLookup.server.ts (+Test) Plan-Auflösung pro Tenant 1:1 / anpassen — direkt für Quota-/Feature-Lookup.
apps/web/app/utils/db/subscriptions/tenantSubscriptionProducts.db.server.ts (+Test) DB-Layer Tenant-Subscription-Products als-vorlage — DB-Zugriffsmuster; in HiveGrid-Layer-Trennung (View-Model) zu überführen.
apps/web/app/utils/db/stripe/checkoutSessions.db.server.ts Checkout-Session-Persistenz als-vorlage
packages/entitlements/ (Paket @toolctrl/entitlements) Plan-/Feature-Gate-Engine: evaluateEntitlement, evaluateProductAccess, featureGateRegistry, featureKeys, getFeatureGate, standardEntitlementPlans, getProductBlueprint (blueprints.ts, aktuell crm-saas) anpassen — direkt nutzbar als Feature-Gating-Querschnitt (Spec SubscriptionFeature/SubscriptionProductFeature); HiveGrid-Plan-Codes/Blueprints einzufüllen. Plan-IDs sind aktuell free/pro/enterprise.
packages/entitlements/src/ai-quotas.ts (+Test) resolveTenantAIQuota, getAIQuotaPlanDefaults, parseTenantAIQuotaOverride, aiQuotaCapabilityKeys 1:1 / anpassen — deckt AI-Credits-Quota (Block J) konzeptionell ab.
packages/entitlements/src/ai-overage.ts (+Test) resolveTenantAIOverage, computeMarkedUpCostCents, roundUpToBlock, standardAIOverageDefaults (Multiplier 1.5, Block-Cents) 1:1 — Pay-as-you-go-Aufschlags-Rechnung (Block I/J + Pflichtenheft 28.8) bereits implementiert.
apps/worker/src/jobs/ai-cost-aggregate*.ts, ai-cost-persist*.ts, ai-cost-stripe-report*.ts, ai-cost-stripe-submitter.ts, ai-cost-reconciliation*.ts, ai-cost-alert.ts Voller AI-Cost-Pipeline-Stack: Aggregation pro Period → Persist → Stripe-Usage-Reporting (createMeterEvent) → Reconciliation → Kosten-Alert (Notbremse-Kandidat 28.8.6) 1:1 / anpassen — deckt Metering-Reporting an Stripe (Block I) + Notbremse/Upsell-Trigger weitgehend ab.
apps/worker/src/jobs/trial-expiring-trigger.ts (+Test) Täglicher Cron: scannt Trials mit Ablauf in Horizon (default 3 Tage), emittiert tenant.trial_expiring-Outbox-Event; idempotenter dispatchKey tenant.trial_expiring.<tenantId>.<YYYY-MM-DD> anpassen — Trial-Ende-3-Tage-/1-Tag-Warnung (Block E / 28.3.5). 1-Tag-Schwelle ergänzen.
apps/api/src/billing/stripe-webhook-route.ts (+ 24 KB Test) Stripe-Webhook-Endpoint (Hono): Signatur-Verify (constructStripeWebhookEvent), Metrics, transaktionaler Sub-Update + Outbox-Publish, Cache-Invalidation. Handhabt: customer.subscription.updated/deleted, subscription_schedule.canceled anpassen — solide Endpoint-/Signatur-/Transaktions-/Outbox-Vorlage. Event-Coverage und Idempotenz-Strategie müssen erweitert werden (siehe Lücken).
apps/web/app/components/core/settings/subscription/* UI: ChangeSubscription, MyInvoices, MyPayments, MyPaymentMethods, MyUpcomingInvoice, MyBillingSession, MyPlanFeatureUsage, MySubscriptionFeatures, MyProducts, Plan(s), PlanChangePreviewModal, ToggleBillingPeriod, CheckPlanFeatureLimit (alle mit i18n-Tests) als-vorlage — Achtung LP-8/Apple-Compliance: Pricing-/Plan-Vergleich/Upgrade-UI darf NICHT in die App. Nur Read-Only-Teile (MyInvoices, MyPaymentMethods, Usage-Anzeigen, Billing-Session-Embed) sind in HiveGrid zulässig; Plan-Wechsel/Toggle-Period gehören in Anti-Liste.
apps/web/app/routes/admin/settings/pricing*.tsx (pricing.tsx, pricing.new.tsx, pricing.edit.$id.tsx, pricing.features.tsx, pricing.ai-overage.tsx) Plattform-Admin-Pricing-Konfiguration (datengetrieben, kein Code-Release) anpassen — erfüllt Spec-Grundprinzip Block A (konfig-getriebene Engine) + Pflichtenheft 28.1.6. Gehört nach admin.hivegrid.de (M37).
apps/web/app/routes/admin/accounts/subscriptions.tsx, subscriptions-revenue.tsx Plattform-Admin: alle Tenant-Subscriptions + Revenue-Sicht anpassen — Spec Block N „Plattform-Admin-Sicht" (M37).
apps/web/app/routes/app.$tenant/settings/subscription.tsx (+Test) Tenant-seitige Subscription-Settings-Route als-vorlage — Route-Pattern + Loader-Muster; HiveGrid-Routes sind /{tenant}/admin/subscription/... (Spec Block M).
packages/shared-store/ (@toolctrl/shared-store) Redis-KV: createIdempotencyStore, createTwoTierCache, Rate-Limiter 1:1 — Idempotenz (AV #8) + Status-Cache (Block G) + Cross-Replica-Invalidation.
prisma/schema/outbox.prisma + @toolctrl/db/outbox (publishOutboxEvent) Transactional-Outbox für zuverlässige Side-Effects (Notifications, Lifecycle-Events) 1:1 — Übergangs-Hooks (Block B) sauber als Outbox-Events.
apps/worker/src/jobs/legacy-event-receiver.ts Outbox→BullMQ-Consumer für subscription.ended/cancelled Lifecycle-Events anpassen — Vorlage für Status-Übergangs-Side-Effects (Audit/Activity/Notification).

Vorhandene Daten-Modelle (konkret, mit Pfad)

Alle in /Users/raffaelsheikh/Documents/GitHub/boilerplate/prisma/schema/subscriptions.prisma:

BP-Modell Spec-Pendant Deckung
SubscriptionProduct SubscriptionProduct Teilweise — hat stripeId, order, title, active, model, public, badge, billingAddressCollection, hasQuantity, canBuyAgain, Relation assignsTenantTypes TenantType[]. Fehlt: code, tier_order, is_self_service_eligible, setup_fee_required, trial_default_days, requires_manual_activation, frozen_mode_price_id, locale-display_name, Markdown-description, created_at/updated_at.
SubscriptionPrice SubscriptionPrice Teilweise — stripeId, type(Int), billingPeriod(Int), price(Decimal), currency, trialDays, active. Fehlt: interval-Enum (month/year/one_time), interval_count, unit_amount_cents, tax_behavior, valid_from/valid_until.
SubscriptionFeature SubscriptionFeature + teilweise SubscriptionProductFeature Abweichend — BP-Feature ist direkt an SubscriptionProduct gehängt (name, type, value, accumulate), kein globaler Feature-Katalog + M:N-Join. Spec verlangt globalen SubscriptionFeature (mit category-Enum) + separate SubscriptionProductFeature (mit quota_value JSON). Umbau nötig.
SubscriptionUsageBasedPrice SubscriptionUsageBasedPrice Gut — unit, usageType, aggregateUsage, tiersMode, billingScheme, Tiers. Fehlt: metric_code-Enum (automation_trigger/ai_token/sms_outbound), included_amount, overage_pricing_strategy.
SubscriptionUsageBasedTier SubscriptionUsageBasedTier 1:1 — from, to?, perUnitPrice, flatFeePrice.
TenantSubscription TenantSubscription Stark abweichend — BP hat nur tenantId, stripeCustomerId, products[]. Fehlt alles Lifecycle-Relevante: status-Enum, subscription_product_id, stripe_subscription_id, Period-Felder, Trial-Felder, cancel_at_period_end, cancelled_at/reason, frozen_since, setup_fee_paid_at, Dunning-Felder, created_by_user_id, created_via. In BP liegen Period/Cancel-Felder auf TenantSubscriptionProduct, nicht auf TenantSubscription.
TenantSubscriptionProduct TenantSubscriptionProduct (Snapshot) Teilweise — hat currentPeriodStart/End, cancelledAt, endsAt, stripeSubscriptionId, quantity, fromCheckoutSessionId, customEntitlementPlanId. Fehlt: snapshot_data (JSONB Preisstabilität), valid_from/valid_until.
TenantSubscriptionProductPrice TenantSubscriptionProductPrice Teilweise — verknüpft Price + UsageBasedPrice + UsageRecords. Fehlt: quantity (liegt eine Ebene höher), is_metered-Flag, valid_from/valid_until.
TenantSubscriptionUsageRecord TenantSubscriptionUsageRecord Abweichend — BP: timestamp(Int), quantity, stripeSubscriptionItemId. Spec: metric_code, period_start/end, usage_amount, reported_to_stripe_at, Composite-Index. Erweitern.
CheckoutSessionStatus CheckoutSessionStatus Teilweise — BP: pending(Bool), email, fromUrl, from/createdUserId, from/createdTenantId. Fehlt: stripe_checkout_session_id, status-Enum, intent-Enum, completed_at/expires_at, error_message, redirect_url_*.
Credit Credit Teilweise — BP: tenantId, userId?, amount(Int), type(String), objectId?. Fehlt: credit_type-Enum, source-Enum, valid_from/valid_until, balance_after, notes, created_by_user_id-Semantik.
TenantProductConfiguration (kein direktes Spec-Pendant) Bonus — blueprintId default crm-saas. Brauchbar als Branchen-/Blueprint-Anker (M29).
TenantFeatureOverride (deckt quota_value-Override-Idee) Bonus — Per-Tenant Feature-Override (featureKey/override/reason). Nutzbar für manuelle Plattform-Admin-Grants.

Verwandt, NICHT subscriptions.prisma:

  • prisma/schema/accounts.prisma → model Tenant hat nur active: Boolean + deactivatedReason: String? — kein Status-Enum (trial/active/past_due/cancelled/frozen). Die in Spec Block B geforderte Tenant.status↔TenantSubscription.status-Synchronisation existiert in der BP NICHT.
  • model TenantType (accounts.prisma) — ist ein Tenant-Typ (von SubscriptionProduct.assignsTenantTypes zugewiesen), nicht das Büro/Monteur-User-Seat-Konzept der Spec (Block D). Kein Seat-Typ pro User vorhanden.
  • prisma/schema/portals.prisma → eigenes gespiegeltes PortalSubscription*-Subsystem (Endkunden-Billing). Nicht für M28-Tenant-Billing, aber als zweites Muster vorhanden.

Lücken / komplett neu zu bauen

  1. Subscription-Lifecycle-State-Machine (Block B) — fehlt komplett.

    • Kein status-Enum auf TenantSubscription und kein Tenant.status-Enum. BP kennt nur Tenant.active (Boolean).
    • Keine Übergangs-Tabelle/Guards (trial→active, active→past_due, →frozen, →cancelled, Reaktivierung). BP-Webhook setzt nur cancelledAt/endsAt/Period.
    • Tenant-Status-Synchronisation (M27 A.5) neu zu bauen.
  2. Seat-Modell Büro/Monteur (Block D) — fehlt komplett.

    • Kein per-User-Seat-Typ, keine Seat-Counter, keine Rolle↔Seat-Validierung, keine Quota-Enforcement (max_buero_seats/max_monteur_seats), kein Hard/Soft-Limit. quantity existiert nur als undifferenzierte Stripe-Item-Menge.
  3. Read-Only-/Downgrade-Middleware (Block G, AV #11) — fehlt komplett.

    • Grep nach read.?only|past.?due|frozen|isWritable|402 in apps/web + apps/api ergibt KEINEN Billing-Status-Gate. Kein is_writable_status()-Helper, kein 402-Response auf Write-Endpoints, keine Worker-Job-Blockade (Automation/Notification/Webhook-Outbound), keine UI-Disable-Logik. Komplett neu, gekoppelt an Lifecycle (#1).
  4. Webhook-Event-Coverage + Idempotenz-Tabelle (Block C) — teilweise.

    • BP-Handler deckt nur customer.subscription.updated/deleted + subscription_schedule.canceled. Fehlen: checkout.session.completed, invoice.created, invoice.payment_succeeded, invoice.payment_failed, customer.subscription.trial_will_end.
    • Spec verlangt StripeWebhookLog-Tabelle mit stripe_event_id-Unique-Constraint (Schreib-Idempotenz). BP nutzt stattdessen Outbox-dispatchKey (stripe:<eventId>:...) — funktional ähnlich, aber NICHT die geforderte Tabelle. Neu anzulegen oder Spec-Abweichung freigeben.
  5. Dunning / Past-Due-Tracking (Block F) — fehlt.

    • Keine last_dunning_attempt_at/dunning_attempt_count-Felder, keine 14-Tage-3-Versuche-Logik, keine gestaffelte Notification-Strategie. invoice.payment_failed-Handling fehlt (siehe #4).
  6. Frozen-Modus / User-Pause (Block H) — fehlt.

    • Kein frozen_since, kein frozen_mode_price_id, kein „PAUSIEREN"-Multi-Step-Flow, kein Stripe-Price-Wechsel auf reduzierte Gebühr, kein frozen→past_due-Pfad.
  7. Trial-Aktivierungs-Flow mit Vertriebs-Freigabe (Block E) — teilweise.

    • trial-expiring-trigger.ts deckt die Warnungen ab. Fehlt: requires_manual_activation-Flow (Vertrieb setzt trial_start/trial_end), „Trial-Anfragen"-Liste in M37, Trial-Ende→deletion_pending-Übergang, 1-Tag-Schwelle.
  8. AI-Credits-Saldo + In-App-Sichten (Block J) — teilweise.

    • Engine-Teile (ai-quotas, ai-overage, Credit-Modell) da. Fehlt: Saldo-Berechnung aus Credit-Sum mit Carry-Over/valid_until-Enforcement, balance_after, Pre-Check-Gate beim LLM-Call mit Klartext-Fehler, In-App-Sichten /{tenant}/admin/usage/ + /usage/ai-credits/ (Listen, Drill-Down, Trend-Chart, CSV-Export, Hochrechnung).
  9. Setup-Fee-Differenz-Logik (Block K) — fehlt.

    • setup_fee_required, setup_fee_paid_at, one_time-Interval-Handling, Differenz-Berechnung bei Up-Grade (effective_setup_fee = neu - bisherig, clamped ≥0).
  10. Layer-Trennung (AGENTS.md DoD A) — kompletter Umbau.

    • BP-Code mischt DB + UI in apps/web/app/components/core/settings/subscription/* und nutzt .db.server.ts/.server.ts direkt. HiveGrid verlangt: View-Models in packages/view-models/, UI in packages/ui-hivegrid/ (kein Prisma-Import), Subframe via Adapter, Routes bauen VM. Alle BP-UI-Komponenten sind daher nur als-vorlage brauchbar, nicht 1:1.
  11. Apple-Compliance-Filter (LP-8 / 28.X.10) — neu durchzusetzen.

    • BP-UI enthält explizit Pricing/Plan-Vergleich/Upgrade/Toggle-Period (Plans.tsx, Plan.tsx, ChangeSubscription.tsx, ToggleBillingPeriod.tsx, PlanChangePreviewModal.tsx). Diese gehören in HiveGrid auf die Anti-Liste; in der App nur Read-Only + externer „Plan-Wechsel über hivegrid.de"-Link (noopener noreferrer).
  12. Querschnitt-Wiring (AGENTS.md DoD B/C) — anzudocken.

    • Sidebar-Eintrag „Subscription / Abo" (Gruppe EINSTELLUNGEN, INTEGRATION-CONTRACTS §1) + Permission-Gate, Breadcrumbs, i18n-Keys, logActivity, notify() statt direkter Mail, withTenantScope()/withPermission(), Permission-Keys billing.read/tenant.admin (Block N) in packages/entitlements/src/permissions.ts. In der BP nicht in dieser Form vorhanden.
  13. Demo-Buchung (Block L) — bewusst extern (Calendly), kein Code nötig; nur created_via=platform_admin-Pfad.


Empfohlenes Vorgehen (Reihenfolge)

M153 — Stripe-Integration (zuerst, Fundament):

  1. prisma/schema/subscriptions.prisma als Migrations-Basis übernehmen; spec-Felder ergänzen (insb. TenantSubscription.status-Enum, fehlende Product/Price/Credit/CheckoutSession-Felder) und Tenant.status-Enum in accounts.prisma einführen.
  2. apps/web/app/utils/stripe.server.ts 1:1 übernehmen (in HiveGrid-Domain-Layer app/domain/billing/); EU-Entity/Tax konfigurieren.
  3. StripeWebhookLog-Tabelle neu + Webhook-Handler aus apps/api/src/billing/stripe-webhook-route.ts als Vorlage erweitern auf alle Pflicht-Events (Block C). Idempotenz über stripe_event_id-Unique. Outbox-/Transaktions-Muster der BP beibehalten.
  4. Idempotenz/Cache aus @toolctrl/shared-store + subscriptionCache.server.ts (TTL auf 30 s).

M154 — Plan-/Seat-Modell: 5. Pricing-Config-Admin (routes/admin/settings/pricing*.tsx) anpassen → admin.hivegrid.de (datengetriebene Engine, Block A). 6. packages/entitlements als Feature-Gating übernehmen; globalen SubscriptionFeature-Katalog + SubscriptionProductFeature-M:N umbauen; HiveGrid-Plan-Codes/Blueprints einfüllen. 7. Seat-Modell (Büro/Monteur) komplett neu — neue Felder/Counter, Rolle↔Seat-Validierung, Quota-Enforcement, Stripe-quantity-Sync. Usage-Sicht /{tenant}/admin/usage/.

M155 — Trial + Read-Only: 8. Lifecycle-State-Machine + Tenant.status-Sync (Block B) neu; Übergangs-Hooks via Outbox (Audit/Activity/notify()). 9. Read-Only-/Downgrade-Middleware (Block G) komplett neu — Route-Guard + Worker-Guard + 402-API + UI-Disable; an Lifecycle gekoppelt. 10. Trial-Aktivierungs-Flow + Dunning (Block E/F): trial-expiring-trigger.ts erweitern (1-Tag), requires_manual_activation-Flow + Dunning-Counter + invoice.payment_failed-Handling. 11. Frozen-Modus (Block H) + Setup-Fee (Block K).

M156 — Metering / Pay-as-you-go (vieles vorhanden): 12. packages/entitlements/ai-overage.ts + ai-quotas.ts 1:1 übernehmen. 13. AI-Cost-Worker-Pipeline (ai-cost-*) übernehmen/anpassen (Stripe-Usage via createMeterEvent, Reconciliation, Notbremse-Alert 28.8.6). 14. AI-Credits-Saldo-Logik (Credit-Sum, Carry-Over, Pre-Check-Gate) + In-App-Sichten (Usage/AI-Credits, CSV-Export) neu bauen.

Querschnitt (alle Milestones): Layer-Trennung (View-Models + ui-hivegrid + Adapter), Sidebar/Permissions/Breadcrumbs/i18n, Apple-Compliance-Filter (keine Pricing/Upgrade-UI in der App).


Notizen

  • Reuse-Grad HIGH trägt v.a. von Daten-Modell, Stripe-Wrapper, Pricing-Admin und Pay-as-you-go-Engine. Die drei spec-kritischsten Bereiche — Lifecycle-State-Machine, Seat-Modell Büro/Monteur, Read-Only-Middleware — sind in der BP NICHT vorhanden und komplett neu (das senkt es vom theoretischen „high+" auf solides high).
  • Idempotenz-Konflikt: Spec fordert StripeWebhookLog-Tabelle; BP nutzt Outbox-dispatchKey. Vor Bau entscheiden (Spec-Abweichung freigeben oder Tabelle bauen).
  • Apple-Compliance ist das größte „Anti"-Risiko: die fertigen BP-UI-Komponenten (Plans.tsx, ChangeSubscription.tsx, ToggleBillingPeriod.tsx) verleiten zur Übernahme verbotener In-App-Pricing-UI. Nur Read-Only-Sichten + externer Link erlaubt.
  • BP-API-Version 2025-10-29.clover (apps/web/app/utils/stripe.server.ts:35 ff.) — beim Übernehmen aktuell halten.