Billing & Subscription Architecture
A Stripe-backed, storage-usage billing engine built around a durable job queue, a safe two-pass preview-then-charge workflow, and idempotency guards that make double-billing structurally impossible.
flowchart TB
A([Admin enqueues run]):::app
Q[(BillingRun queue<br/>Status Queued)]:::data
P[Console poll-queue<br/>ClaimNextQueuedRunAsync<br/>atomic claim - Running]:::svc
PV[Preview pass<br/>read-only - no charges]:::svc
CSV[(CSV export)]:::data
CH[Charge pass<br/>idempotency guard<br/>one charge per account]:::svc
ST[Stripe<br/>tokenized cards<br/>last4 plus brand]:::ext
INV[(Invoices plus DB commit)]:::data
BRA[(BillingRunAccount<br/>Success - Failed<br/>Skipped - NoCharges)]:::data
FPR[(FailedPaymentReconciliation<br/>Stripe ok - DB write failed<br/>manual resolution)]:::sec
DONE([Status Complete]):::app
A --> Q
Q -->|poll| P
P --> PV
PV --> CSV
PV --> CH
CH --> ST
ST -->|charge ok| INV
INV --> BRA
ST -->|charged but DB write fails| FPR
BRA --> DONE
classDef app fill:#1f2630,stroke:#C0894E,color:#F3EFE8;
classDef svc fill:#241c14,stroke:#E2B583,color:#F3EFE8;
classDef data fill:#22201a,stroke:#C0894E,color:#EBD3AE;
classDef ext fill:#1b1f27,stroke:#5A616B,color:#C4C0B8;
classDef sec fill:#1d2733,stroke:#5cc8e0,color:#eafaff;
Stripe & Storage-Based Pricing
The billing engine charges customers through Stripe (Stripe.net v46.2.0) based on how much storage each file consumes over time.
- Per-file storage usage pricing
- Honors coupon-free storage
- Accounts for soft-deleted files
Card Data Never Stored
Cards are tokenized by Stripe. Grade My Investments persists only the last4 digits and card brand for display, never raw card numbers.
Two-Pass Billing
Every billing run is split into two distinct passes so totals can be inspected before a single dollar moves.
Pass 1 — Preview
- Read-only, no charges
- CSV export of computed charges
- Auditable before committing
Pass 2 — Charge
- Commits charges to the database
- Calls Stripe to bill customers
- Records per-account outcomes
Durable Queue
The BillingRun table doubles as a durable job queue, decoupling the admin who triggers a run from the console worker that executes it.
- Admin enqueues a billing run
- Console polls the queue for work
ClaimNextQueuedRunAsyncatomically claims the next run- Status advances Queued Running Complete
Idempotency & Safety
- No double-billing a period.
HasSuccessfulBillingRunForPeriodAsync(year, month)blocks repeat runs for the same period. - One charge per job per account. Each account is charged at most once within a run.
- One-time coupon use. Coupons are consumed a single time.
Failed Payment Reconciliation
The hardest case is a partial success: Stripe charges the customer but the database write that records it fails. The FailedPaymentReconciliation table captures exactly this scenario.
- Stripe success + DB write failure is captured, not lost
- Preserved for manual resolution
- Prevents silent revenue or audit gaps
Per-Account Auditing
BillingRunAccount rows record the outcome of each account within a run, with an invoice id or error message for drill-down.
CLI Commands
| Command | Pass | Purpose |
|---|---|---|
billing-report |
Preview | Read-only preview with CSV export, no charges |
charge |
Charge | Run the monthly charge pass |
charge-account |
Charge | Retry a single account |
poll-queue |
Primary | Poll the BillingRun queue and process claimed runs |
Implemented across Gmi.BillingEngine.Console and the BillingService in Gmi.Services.