improvement(billing): ledger-only usage + period-advance cycle close - #7078
Conversation
…iod advance Usage is now the attributed usage_log ledger everywhere: the userStats baselines (currentPeriodCost / currentPeriodCopilotCost), the includeLegacyBaseline compatibility flag, the pro-snapshot join/leave machinery, and departedMemberUsage accrual are removed from all read and write paths. Cycle rollover (final sub-threshold overage collection, billedOverageThisPeriod reset, last-period bookkeeping) moves off the invoice.finalized payload parsing — dead for org subscriptions since May — onto a period-advance sweep with a durable per-subscription close marker (subscription.last_closed_period_start), transaction-enlisted Stripe outbox invoicing, and stamp-matched ledger sums. Enterprise closes are bookkeeping-only; reporting-anchor orgs advance the marker alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Greptile SummaryThe PR makes the attributed usage ledger authoritative and replaces invoice-triggered billing rollover with a durable period-advance cycle close.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/billing/cycle-close.ts | Implements transactional period-advance closing, ledger-based bookkeeping, credit application, outbox billing, and durable marker claims. |
| apps/sim/lib/billing/threshold-billing.ts | Defers settlement while cycle close is pending and revalidates the expected period while holding the shared tracker lock. |
| apps/sim/lib/billing/credits/daily-refresh.ts | Aligns refresh membership with billing entity and period stamps, including departed actors and late-written stamped rows. |
| apps/sim/lib/billing/core/usage.ts | Removes legacy baseline contributions so current usage is derived exclusively from attributed ledger records. |
| packages/db/migrations/0305_add_subscription_last_closed_period_start.sql | Adds the nullable durable period-close marker required for rollout-safe initialization and replay protection. |
| apps/sim/app/api/cron/billing-cycle-close/route.ts | Adds the authenticated, Redis-coordinated scheduler endpoint that launches the close sweep in the background. |
| helm/sim/values.yaml | Adds the Kubernetes cycle-close schedule in parity with the Docker scheduler configuration. |
Sequence Diagram
sequenceDiagram
participant Cron as Billing close cron
participant Close as Cycle-close sweep
participant DB as PostgreSQL
participant Ledger as Usage ledger
participant Outbox as Stripe outbox
Cron->>Close: Start authenticated sweep
Close->>DB: Find subscriptions with lagging close marker
Close->>Ledger: Sum elapsed period by entity stamps
Close->>DB: Lock billing tracker and revalidate period
Close->>DB: Apply credits and write bookkeeping
opt Billable remainder
Close->>Outbox: Enqueue deterministic invoice operation
end
Close->>DB: Reset tracker and advance close marker
Reviews (15): Last reviewed commit: "fix(billing): reject lagging terminal cl..." | Re-trigger Greptile
…hold billing on close currency Cycle close now unions current members with every actor holding org-stamped ledger rows in the closed period, so a departed member's daily-refresh consumption offsets the final overage exactly like their billed usage. Threshold billing defers with a pending-cycle-close no-op while a subscription's close marker lags its current period, so the shared billedOverageThisPeriod tracker can never mix an elapsed period's settlements with the new period's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@cubic-dev-ai review |
@icecrasher321 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 37 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
…lled usage Threshold settlement revalidates the close marker and period under the tracker lock via the same isSubscriptionCycleCloseCurrent predicate the preflight uses, so a rollover between check and transaction aborts as a concurrent-state retry instead of settling against the wrong cycle. Terminal bookkeeping claims the close marker with its tracker reset, so a deletion racing an in-flight sweep close serializes through the one marker and the loser rolls back rather than re-billing settled overage; anchored enterprise deletions only claim the marker since their windows derive from the anchor, not Stripe bounds. A close with overage due but missing Stripe identifiers now defers loudly instead of claiming the marker and silently forgiving the money, the closed window's start derives from the ledger's own period stamps so anchor-day drift cannot misalign the refresh window, calculateSubscriptionOverage unions departed ledger actors into the org refresh deduction like the close does, and blocked accounts report their real ledger usage while staying blocked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@cubic-dev-ai review |
@icecrasher321 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 37 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
…-bearing A close with overage due but no owner-role member now defers loudly like the missing-Stripe-identifier case instead of claiming the marker and silently forgiving the money. The stamp-drift test pins the marker before the stamped boundary so only the ledger-stamp lookup can produce the asserted window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@cubic-dev-ai review |
@icecrasher321 I have started the AI code review. It will take a few minutes to complete. |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@cubic-dev-ai review |
@icecrasher321 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 38 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
…d delete vestigial refresh bounds Subscription deletion now claims the close marker from the fresh subscription row before computing or charging final overage, serializing with the cycle-close sweep so both paths can never bill the same period — an in-flight close fails its guarded claim and rolls back, and the deletion settles against the row's real period instead of a possibly stale webhook payload. The per-user refresh bounds machinery is deleted outright: its only source was proPeriodCostSnapshotAt, which this PR stopped writing, and ledger entity stamps already scope refresh to org-attributed rows — a joiner's pre-join usage is user-stamped and can never enter the org refresh scan, while a departed member's org-stamped rows participate exactly like a current member's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@cubic-dev-ai review |
@icecrasher321 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 40 files
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
…periods before deletion settlement The refresh actor-list threading (userIds + departed-actor unions) violated the entity-stamp principle the ledger sums already follow: org-attributed rows from departed members counted in pooled usage but never consumed refresh on the monitor/resolved-usage paths. Daily refresh now scopes rows by the same write-time entity and period stamps as the ledger — no actor list anywhere — which deletes the unions, the rollup memberIds, and the org-specific refresh variant. Deletion settlement now closes any elapsed-but-unclosed period (grace bypassed — no later sweep revisits a canceled sub) before claiming the terminal period, so a deletion racing the sweep can no longer jump the marker past an unsettled period and silently forgive its final overage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@cubic-dev-ai review |
@icecrasher321 I have started the AI code review. It will take a few minutes to complete. |
…y in the deduction Refresh membership is now the entity/period stamps alone — identical to the ledger sums it offsets. A row written after the rollover but stamped to the elapsed period (attribution frozen at run start) is billed by the stamp-based close, so it must consume refresh too; created-at now only assigns the day bucket, clamped into the period, instead of excluding the row entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@cubic-dev-ai review |
@icecrasher321 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 40 files
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
…in test clocks billedOverageThisPeriod only ever holds collections for the period that began at the close marker — the threshold gate blocks settlement whenever the marker lags. Both consumers now honor that pairing: a close that skipped forgiven periods counts nothing from the tracker against the period it bills, and the deletion settlement ignores the tracker when the marker was still lagging at claim time. Ignoring is provably safe in both cases because a lagging marker means no current-period collections exist. The cycle-close and daily-refresh suites pin the system clock: their grace and window checks compare fixed period fixtures against Date.now(), which made them dependent on the host date. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@cubic-dev-ai review |
@icecrasher321 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 40 files
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
…al into deletion bookkeeping claimTerminalPeriod no longer advances the marker over an unclosed elapsed period: a lagging marker is reported without a write so the deletion handler can run the elapsed close once more (healing a rollover that committed between close and claim) and only then seal the marker explicitly, with an error log, when the period is genuinely unclosable. Sealing preserves the in-flight-sweep abort guarantee. Deletion bookkeeping now passes the subscription's billingInterval through, so an enterprise reporting subscription whose interval lives on the row column (not metadata) still resolves as reporting-anchored and keeps its bookkeeping no-op, matching every other resolver call site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@cubic-dev-ai review |
@icecrasher321 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
No issues found across 40 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
What
Makes the attributed
usage_logledger 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).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.@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.subscription.last_closed_period_start, migration0305, additive) lagsperiod_start→ close the elapsed period. Ledger sums are matched on write-time period stamps.billedOverageThisPeriodreset;lastPeriodCost/lastPeriodCopilotCostwritten from ledger sums; marker claimed. Crash-safe, replay-safe./api/cron/billing-cycle-closeevery 6h (helm + docker/crontab; parity check passes). Subscription deletion useswriteFinalPeriodBookkeeping(final billing unchanged).Ops (already done / separate)
cron-jobs.ts, path/api/cron/billing-cycle-close,0 */6 * * *).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