Skip to content

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

Merged
icecrasher321 merged 15 commits into
stagingfrom
billing-ledger-only-cycle-close
Aug 25, 2026
Merged

improvement(billing): ledger-only usage + period-advance cycle close#7078
icecrasher321 merged 15 commits into
stagingfrom
billing-ledger-only-cycle-close

Conversation

@icecrasher321

Copy link
Copy Markdown
Collaborator

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

icecrasher321 and others added 2 commits August 25, 2026 13:38
…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>
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 25, 2026 11:27pm

Request Review

@greptile-apps

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR makes the attributed usage ledger authoritative and replaces invoice-triggered billing rollover with a durable period-advance cycle close.

  • Removes legacy usage baselines and membership snapshot accounting from billing, limits, reporting, and administration.
  • Adds transactional, replay-safe cycle closing with final overage collection and durable close markers.
  • Adds authenticated scheduled execution and matching Helm and Docker scheduler configuration.
  • Updates database schema, migration metadata, API compatibility fields, and billing tests.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

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
Loading

Reviews (15): Last reviewed commit: "fix(billing): reject lagging terminal cl..." | Re-trigger Greptile

Comment thread apps/sim/lib/billing/cycle-close.ts
Comment thread apps/sim/lib/billing/cycle-close.ts Outdated
…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>
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown

@cubic-dev-ai review

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

Comment thread apps/sim/lib/billing/threshold-billing.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/sim/lib/billing/core/billing.ts Outdated
Comment thread apps/sim/lib/billing/cycle-close.ts Outdated
Comment thread apps/sim/lib/billing/cycle-close.ts
Comment thread apps/sim/lib/billing/threshold-billing.ts
Comment thread apps/sim/lib/billing/webhooks/subscription.ts
Comment thread apps/sim/lib/billing/calculations/usage-monitor.ts Outdated
Comment thread apps/sim/lib/billing/cycle-close.ts Outdated
…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>
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown

@cubic-dev-ai review

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/sim/lib/billing/cycle-close.ts
Comment thread apps/sim/lib/billing/cycle-close.test.ts Outdated
…-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>
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown

@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>
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown

@cubic-dev-ai review

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/sim/lib/billing/cycle-close.ts
Comment thread apps/sim/lib/billing/cycle-close.ts Outdated
Comment thread apps/sim/lib/billing/cycle-close.ts Outdated
Comment thread apps/sim/lib/billing/cycle-close.test.ts
…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>
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown

@cubic-dev-ai review

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/sim/lib/billing/webhooks/subscription.ts Outdated
Comment thread apps/sim/lib/billing/credits/daily-refresh.ts Outdated
Comment thread apps/sim/lib/billing/calculations/usage-monitor.ts Outdated
Comment thread apps/sim/lib/billing/core/usage.ts Outdated
…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>
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown

@greptile

@cubic-dev-ai review

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

Comment thread apps/sim/lib/billing/credits/daily-refresh.ts Outdated
…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>
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown

@greptile

@cubic-dev-ai review

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/sim/lib/billing/cycle-close.ts Outdated
Comment thread apps/sim/lib/billing/webhooks/subscription.ts
Comment thread apps/sim/lib/billing/cycle-close.test.ts
…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>
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown

@greptile

@cubic-dev-ai review

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

Comment thread apps/sim/lib/billing/threshold-billing.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/sim/lib/billing/cycle-close.ts Outdated
Comment thread apps/sim/lib/billing/webhooks/subscription.ts
…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>
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 25, 2026

Copy link
Copy Markdown

@greptile

@cubic-dev-ai review

@icecrasher321 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@icecrasher321
icecrasher321 merged commit a25d993 into staging Aug 25, 2026
33 checks passed
@waleedlatif1
waleedlatif1 deleted the billing-ledger-only-cycle-close branch August 26, 2026 03:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant