Sitelet https://github.com/simstudioai/sim/pull/7078
Skip to content

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

Open
icecrasher321 wants to merge 14 commits into
stagingfrom
billing-ledger-only-cycle-close
Open

improvement(billing): ledger-only usage + period-advance cycle close#7078
icecrasher321 wants to merge 14 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:11pm

Request Review

@greptile-apps

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR makes attributed usage-ledger rows the sole source of billing usage and replaces invoice-triggered rollover with durable period-advance cycle closing.

  • Removes legacy baseline, membership-snapshot, and departed-usage accounting from usage and administrative surfaces.
  • Adds transactional, replay-safe cycle closing with credit application, final overage collection, bookkeeping, and a durable subscription marker.
  • Adds a six-hour authenticated cron sweep and aligns threshold settlement with the cycle-close marker and lock protocol.

Confidence Score: 5/5

The PR appears safe to merge because the previously reported billing-cycle and refresh failures are resolved and no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/lib/billing/cycle-close.ts Implements durable period-advance closing, stamped-ledger aggregation, transactional overage settlement, bookkeeping, and marker claiming; the reviewed prior issues are resolved at current HEAD.
apps/sim/lib/billing/threshold-billing.ts Defers settlement while an elapsed period remains open and revalidates the expected period under the tracker lock before mutation.
apps/sim/lib/billing/credits/daily-refresh.ts Aligns refresh membership with billing entity and period stamps while clamping late rows into the elapsed period’s final day.
apps/sim/lib/billing/core/usage.ts Removes legacy baseline accounting so attributed ledger entries provide current-period usage.
apps/sim/app/api/cron/billing-cycle-close/route.ts Adds an authenticated, Redis-serialized background entry point for periodic cycle-close sweeps.
packages/db/migrations/0305_add_subscription_last_closed_period_start.sql Adds the nullable durable close marker used to initialize and replay cycle closing safely.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Subscription period advances] --> B[Cron sweep finds lagging close marker]
  B --> C[Read elapsed-period ledger stamps]
  C --> D[Compute refresh-adjusted overage]
  D --> E[Lock tracker and organization state]
  E --> F{Marker already advanced?}
  F -->|Yes| G[No-op]
  F -->|No| H[Apply credits]
  H --> I[Enqueue remaining Stripe overage]
  I --> J[Write last-period bookkeeping]
  J --> K[Reset billed-overage tracker]
  K --> L[Claim close marker]
  M[Threshold settlement] --> N{Close marker current?}
  N -->|No| O[Defer until sweep closes period]
  N -->|Yes| P[Lock tracker and revalidate period]
  P --> Q[Settle current-period threshold overage]
Loading

Reviews (14): Last reviewed commit: "fix(billing): pair the overage tracker w..." | 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
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

Organization threshold billing now reads the period ledger per user and
unions the actors holding org-attributed rows with the current roster
before computing refresh deductions — the same actor set
calculateSubscriptionOverage and the cycle close use — so a departed
member's usage cannot be settled without their daily-refresh offset.

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 38 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/cycle-close.ts Outdated
…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
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.

2 issues found across 40 files

Confidence score: 2/5

  • apps/sim/lib/billing/cycle-close.ts can advance the terminal marker past an intervening rollover and silently skip a billing period, causing incorrect close processing; re-read and validate the marker before claiming the terminal period.
  • apps/sim/lib/billing/webhooks/subscription.ts misclassifies Enterprise reporting subscriptions that use subscription.billingInterval, so deletion bookkeeping can apply Stripe-period behavior incorrectly; handle both interval sources before recording deletion state.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/sim/lib/billing/cycle-close.ts">

<violation number="1" location="apps/sim/lib/billing/cycle-close.ts:232">
P1: When another rollover commits between the elapsed-period close and deletion's terminal claim, `claimTerminalPeriod` jumps the marker to the newest `periodStart` and silently skips the intervening period. Re-read and close every lagging period before claiming the terminal period, or reject the terminal claim unless the marker is current.</violation>
</file>

<file name="apps/sim/lib/billing/webhooks/subscription.ts">

<violation number="1" location="apps/sim/lib/billing/webhooks/subscription.ts:317">
P2: When an Enterprise reporting subscription stores its interval in `subscription.billingInterval` rather than `metadata.reportingPeriodInterval`, deletion bookkeeping treats it as a Stripe-period subscription because these calls omit `billingInterval`. Pass the stored billing interval through to `writeFinalPeriodBookkeeping` so reporting-anchor subscriptions remain bookkeeping no-ops.</violation>
</file>

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

const markerWasCurrent =
!!row.lastClosedPeriodStart &&
row.lastClosedPeriodStart.getTime() >= row.periodStart.getTime()
await claimCloseMarker(tx, subscriptionId, row.periodStart)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When another rollover commits between the elapsed-period close and deletion's terminal claim, claimTerminalPeriod jumps the marker to the newest periodStart and silently skips the intervening period. Re-read and close every lagging period before claiming the terminal period, or reject the terminal claim unless the marker is current.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/billing/cycle-close.ts, line 232:

<comment>When another rollover commits between the elapsed-period close and deletion's terminal claim, `claimTerminalPeriod` jumps the marker to the newest `periodStart` and silently skips the intervening period. Re-read and close every lagging period before claiming the terminal period, or reject the terminal claim unless the marker is current.</comment>

<file context>
@@ -0,0 +1,830 @@
+    const markerWasCurrent =
+      !!row.lastClosedPeriodStart &&
+      row.lastClosedPeriodStart.getTime() >= row.periodStart.getTime()
+    await claimCloseMarker(tx, subscriptionId, row.periodStart)
+    return { periodStart: row.periodStart, periodEnd: row.periodEnd, markerWasCurrent }
+  })
</file context>

periodStart: subscription.periodStart ?? null,
periodEnd: subscription.periodEnd ?? null,
...settlementPeriod,
metadata: subscription.metadata,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When an Enterprise reporting subscription stores its interval in subscription.billingInterval rather than metadata.reportingPeriodInterval, deletion bookkeeping treats it as a Stripe-period subscription because these calls omit billingInterval. Pass the stored billing interval through to writeFinalPeriodBookkeeping so reporting-anchor subscriptions remain bookkeeping no-ops.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/billing/webhooks/subscription.ts, line 317:

<comment>When an Enterprise reporting subscription stores its interval in `subscription.billingInterval` rather than `metadata.reportingPeriodInterval`, deletion bookkeeping treats it as a Stripe-period subscription because these calls omit `billingInterval`. Pass the stored billing interval through to `writeFinalPeriodBookkeeping` so reporting-anchor subscriptions remain bookkeeping no-ops.</comment>

<file context>
@@ -299,15 +284,37 @@ export async function handleSubscriptionDeleted(
-            periodStart: subscription.periodStart ?? null,
-            periodEnd: subscription.periodEnd ?? null,
+            ...settlementPeriod,
+            metadata: subscription.metadata,
           })
 
</file context>

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