Application Logic
:::info Source
Sourced from services/billing-service/APPLICATION_LOGIC.md in the documentation repo.
:::
1. Application Services
PaymentService— PaymentIntent creation, capture, refund.InvoiceService— generate, finalize, render PDF, mark paid.SubscriptionService— lifecycle, renewals, cancellation.DunningService— failed payment retry schedule + communication.TaxService— tax calculation via Stripe Tax.PayoutService— compute provider earnings + trigger payouts.WebhookHandler— Stripe webhook signing + dispatch.ReconciliationService— daily reconciliation vs Stripe.
2. Commands
| Command | Trigger |
|---|---|
CreatePaymentIntent | marketplace.order.placed.v1 |
ConfirmPayment | Stripe webhook |
RefundPayment | Admin or auto-refund policy |
CreateInvoice | Subscription renewal, marketplace order |
FinalizeInvoice | Invoice ready to charge |
StartDunning | Payment failed on subscription |
TriggerPayout | Scheduled weekly |
VoidInvoice | Admin |
3. Queries
getSubscription(id),listSubscriptions(tenantId)getInvoice(id),listInvoices(tenantId)getPayment(id),getProviderEarnings(providerTenantId)getPayout(id),listPayouts(providerTenantId)
4. Sagas
- Billing step of Purchase Saga: create PaymentIntent → confirm → emit
payment.succeeded.v1. - Subscription Renewal Saga (nightly).
- Refund Saga (coordinated with marketplace to revoke license).
- GDPR Erasure Saga (legal hold on billing records per tax law; anonymize user PII).
5. Policies
- Idempotency:
Idempotency-Keyon all writes; Stripe API also accepts idempotency keys. - Webhook validation: HMAC signature + 5-min timestamp window + nonce cache.
- Dunning: 3 reminders over 7 days → suspension → 30 days → cancel.
- Refund window: per tenant policy (default 14 days).
- Payout threshold: provider balance ≥ $50 (configurable); weekly Friday.
6. Use Case Flows
6.1 Order Payment
marketplace.order.placed.v1 (amount, currency, orderId)
│
▼
CreatePaymentIntent(amount, currency, metadata={orderId})
│
▼
Return client_secret to frontend → Stripe Elements → 3DS if required
│
▼ Stripe confirms → webhook /webhooks/stripe
│
Validate signature → identify event
│
├─ payment_intent.succeeded → emit billing.payment.succeeded.v1
└─ payment_intent.failed → emit billing.payment.failed.v1
6.2 Subscription Renewal
Nightly cron: SELECT subscriptions WHERE currentPeriod.end < now()
For each:
CreateInvoice(lines from subscription.itemQuantities)
FinalizeInvoice → attempts payment
On success: extend currentPeriod
On fail: StartDunning
6.3 Refund
Admin POST /payments/{id}/refund (reason, amount)
│
▼
Stripe API refund with original Idempotency-Key prefix
│
▼
Emit billing.payment.refunded.v1
│
▼
marketplace consumes → revoke license → revoke enrollment
6.4 Dunning
Payment failed on subscription S:
Day 1: email reminder + in-app banner. Retry payment.
Day 3: stage 2. Retry.
Day 7: stage 3. Retry. Manager notified if org subscription.
Day 10: suspension_pending. Grace period.
Day 14: suspended. Tenant can no longer use paid features.
Day 44: cancel.
Payment anytime → resolve, return to active.
6.5 Payout
Weekly Friday job:
For each provider tenant:
Compute earnings (order totals × (1 - 0.15) for that period)
Subtract refunds
If balance ≥ $50:
CreatePayout(providerConnectAccountId, amount)
Emit billing.payout.initiated.v1
Reconciliation on Monday vs Stripe Connect balance.