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 free-tier and coupon-covered ($0) charges
- 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. Billing applies to paid-tier (card-on-file) accounts only — free-tier usage records a $0 charge carrying internal cost for analytics and is never invoiced.
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.