improvement(billing): ledger-only usage + period-advance cycle close
Merged
IC
2mo ago+22397+22397+01234567890123456789012345678901234567890123456789-1946-1946-0123456789012345678901234567890123456789
What
Makes the attributed usage_log ledger the sole source of truth for usage, and replaces the dead invoice-driven cycle rollover with a period-advance close.
Ledger-only usage
currentPeriodCost/currentPeriodCopilotCostbaselines are read and written by nothing: removed from usage math (usage.ts,organization.ts,billing.ts,usage-monitor.ts), threshold billing, admin dashboard aggregates, org member routes, logger email context, and the admin manual-adjust route (now a warned no-op).- Deletes
includeLegacyBaseline(all call sites), the pro-snapshot join/leave machinery (proPeriodCostSnapshotcapture/restore/merge),departedMemberUsageaccrual, and the leave-time baseline wipe — ledger entity/period stamps make all of them redundant. - Fixes the free-user double count (pre-June usage existed in both baseline and ledger under the open one-time-credits window).
- Wire shapes unchanged; dead v1 admin fields kept and marked
@deprecated.
Period-advance cycle close (lib/billing/cycle-close.ts)
invoice.finalizedhandler,resetUsageForSubscription, and the invoice line-period parsing are deleted — that path silently dropped every org subscription's invoice (billing_reason / payload-shape sensitivity), so team cycle closes have not run since May.- The close now keys off subscription period advance: a durable marker (
subscription.last_closed_period_start, migration0305, additive) lagsperiod_start→ close the elapsed period. Ledger sums are matched on write-time period stamps. - Per close, in one transaction: final sub-threshold overage (minus what threshold billing already collected), credits first, remainder invoiced via the tx-enlisted Stripe outbox with deterministic idempotency stems;
billedOverageThisPeriodreset;lastPeriodCost/lastPeriodCopilotCostwritten from ledger sums; marker claimed. Crash-safe, replay-safe. - Null markers initialize without billing — no retroactive closes at rollout; each sub's first close is its next rollover.
- Enterprise: bookkeeping-only (external billing); reporting-anchor orgs marker-only. Sub-$0.50 remainders forgiven.
- Cron:
/api/cron/billing-cycle-closeevery 6h (helm + docker/crontab; parity check passes). Subscription deletion useswriteFinalPeriodBookkeeping(final billing unchanged).
Ops (already done / separate)
- Paid baseline residue ($13.8k across 71 rows) zeroed in prod via console; verified 0 remaining on the replica.
- Hosted-prod cron needs one CDK entry in the infra repo (
cron-jobs.ts, path/api/cron/billing-cycle-close,0 */6 * * *). - Follow-up PR after bake: DROP the dead columns (15 legacy
user_statscounters + the two baselines + snapshot pair +departedMemberUsage+workflow_execution_logs.cost).
Verification
turbo run type-check: 26/26 · fullapps/simsuite: 32,452 passed · newcycle-close.test.ts(10 tests)check:api-validation✓ ·check:cron-parity✓ ·check:migrations(backward-compatible) ✓ ·check:boundaries✓
🤖 Generated with Claude Code