improvement(billing): ledger-only usage + period-advance cycle close

Merged
IC
2mo ago
+22397-1946

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 / currentPeriodCopilotCost baselines 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 (proPeriodCostSnapshot capture/restore/merge), departedMemberUsage accrual, 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.finalized handler, 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, migration 0305, additive) lags period_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; billedOverageThisPeriod reset; lastPeriodCost / lastPeriodCopilotCost written 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-close every 6h (helm + docker/crontab; parity check passes). Subscription deletion uses writeFinalPeriodBookkeeping (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_stats counters + the two baselines + snapshot pair + departedMemberUsage + workflow_execution_logs.cost).

Verification

  • turbo run type-check: 26/26 · full apps/sim suite: 32,452 passed · new cycle-close.test.ts (10 tests)
  • check:api-validation ✓ · check:cron-parity ✓ · check:migrations (backward-compatible) ✓ · check:boundaries ✓

🤖 Generated with Claude Code