Billing Engine
Gateway-agnostic billing orchestration. Business licensing remains workspace_module_subscriptions; the engine handles purchase settlement, consolidated invoicing, and payment ledger writes.
See also: Payment Gateway Architecture.
Components
| Piece | Role |
|---|---|
BillingEngine | Orchestrates install → invoice → payment → activation |
GatewayManager | Resolves PaymentGatewayInterface drivers (manual, stripe) |
ManualGateway | Synchronous settle (admin/manual payments) |
StripeGateway | Checkout + webhook normalization to GatewayEvent (only Cashier/Stripe touchpoint) |
ProrationCalculator | Mid-cycle purchase amount from workspace proration_mode |
InvoiceService / PaymentService | Ledger CRUD |
PaymentGatewayService | Admin gateway management |
GatewayModulePriceService | Per-gateway module product/price mappings (payment_gateway_module_prices) |
Drivers implement PaymentGatewayInterface. Cashier tables are a Stripe mirror only — not the licensing or invoice SoT. BillingEngine must never import Stripe or Cashier. Modules never store provider price IDs.
Proration modes
Set per workspace (tenants.proration_mode) or default via system setting.
| Mode | Mid-cycle purchase charge |
|---|---|
prorated | Remaining days in current period ÷ period length × module price |
free_until_next | 0 until next consolidated invoice |
none | Full period price immediately |
Purchase sequence (purchaseModule)
POST /tenants/{tenant}/modules → ModuleSubscriptionService::install
├─ non-billable / included → status=active immediately
└─ billable → status=pending
├─ persist payment_gateway_id on the subscription
├─ manual gateway → draft invoice (proration line) → open → mark payment succeeded → activate
└─ other gateway → assert gateway price mapping when required → createCheckout (refs on CheckoutRequest) → redirect; webhook activates on successConsolidated billing sequence
Command: php artisan billing:run-consolidated (scheduled daily in routes/console.php).
For each tenant where next_billing_at <= now:
1. Collect active, billable workspace_module_subscriptions that are platform-managed
(no payment gateway, or gateway does not supportRecurring())
2. Skip invoice creation if none (recurring-gateway renewals stay with the provider)
3. Create draft invoice with one line per subscription (type=module)
4. Open invoice
5. Record payment; auto-succeed on manual gateway only
6. Advance next_billing_at by one month (anchor day 1–28)Webhooks
POST /webhooks/gateways/{code}→ driverparseWebhook→BillingEngine::handleGatewayEventPOST /stripe/webhook→ Cashier handlers + same Billing Engine dispatch
Tenant resolution (provider-agnostic; no Stripe/Cashier columns in the engine):
GatewayEvent.tenantId(set by the driver when the customer map resolves)providerSubscriptionId→workspace_module_subscriptionsmeta.payment_id→payments(coverspayment_failedwhen no customer → tenant map exists)
Default gateway
default_payment_gateway system setting → payment_gateways.is_default → manual (see config/core-platform.php).