Skip to content

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

PieceRole
BillingEngineOrchestrates install → invoice → payment → activation
GatewayManagerResolves PaymentGatewayInterface drivers (manual, stripe)
ManualGatewaySynchronous settle (admin/manual payments)
StripeGatewayCheckout + webhook normalization to GatewayEvent (only Cashier/Stripe touchpoint)
ProrationCalculatorMid-cycle purchase amount from workspace proration_mode
InvoiceService / PaymentServiceLedger CRUD
PaymentGatewayServiceAdmin gateway management
GatewayModulePriceServicePer-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.

ModeMid-cycle purchase charge
proratedRemaining days in current period ÷ period length × module price
free_until_next0 until next consolidated invoice
noneFull 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 success

Consolidated 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} → driver parseWebhookBillingEngine::handleGatewayEvent
  • POST /stripe/webhook → Cashier handlers + same Billing Engine dispatch

Tenant resolution (provider-agnostic; no Stripe/Cashier columns in the engine):

  1. GatewayEvent.tenantId (set by the driver when the customer map resolves)
  2. providerSubscriptionIdworkspace_module_subscriptions
  3. meta.payment_idpayments (covers payment_failed when no customer → tenant map exists)

Default gateway

default_payment_gateway system setting → payment_gateways.is_defaultmanual (see config/core-platform.php).

Official documentation for the SaleOS SaaS Platform.