Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughThis PR updates worker credit draining, removes Chat organization credit migration, adds credit-accounting tests, and rejects real-credit gifts for non-default organizations. ChangesCredit policy updates
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant batchProcessLogs
participant drainBucket
participant OrganizationCredits
batchProcessLogs->>drainBucket: Drain regular and premium pools
drainBucket-->>batchProcessLogs: Return remaining balances
batchProcessLogs->>OrganizationCredits: Charge overflow or floor regular credits at zero
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cec93f518b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (devPool) { | ||
| // The dev plan is active but its cycle allowance was exhausted | ||
| // by an in-flight burst that raced past the limit. Charge the | ||
| // overflow to the dev plan's virtual credits (pushing | ||
| // devPlanCreditsUsed past the limit) rather than the org's real | ||
| // credits. Dev-plan users typically hold no real credits, so | ||
| // spilling here would drive the balance negative for usage the | ||
| // plan was meant to cover. | ||
| await deductFromPlanPool( | ||
| orgId, | ||
| devPool, | ||
| remainingCost, |
There was a problem hiding this comment.
Bill exhausted-plan overage to real credits
When an active dev plan has no virtual credits left but the org still has regular credits, the gateway still admits requests because it sums organization.credits + devPlanCreditsRemaining + chatPlanCreditsRemaining (checked in apps/gateway/src/chat/chat.ts:4036-4048). This new branch sends every remaining cost to deductFromPlanPool whenever devPool exists, so those admitted requests increase devPlanCreditsUsed instead of reducing the real credits that made the request eligible. A dev-plan org with a top-up/referral balance (or a frozen/exhausted plan plus regular credits) can therefore consume up to that real-credit balance without decrementing it, then get blocked while the balance remains unused.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/worker/src/worker.ts (1)
1068-1075:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPremium overflow can be over-attributed to
devPlanPremiumCreditsUsed.At Line 1068–1074,
remainingPremiumis only reduced when the dev pool is charged. Premium cost covered by chat-plan credits remains inremainingPremium, and Line 1120 can then apply that leftover as premium on unrelated overflow. That can over-incrementdevPlanPremiumCreditsUsedand prematurely trigger premium-cap blocking.Suggested fix
- const premiumTake = - pool.kind === "dev" - ? Decimal.min(remainingPremium, take) - : new Decimal(0); + const coveredPremium = Decimal.min(remainingPremium, take); + const premiumTake = + pool.kind === "dev" ? coveredPremium : new Decimal(0); await deductFromPlanPool(orgId, pool, take, premiumTake); remaining = remaining.minus(take); - remainingPremium = remainingPremium.minus(premiumTake); + remainingPremium = remainingPremium.minus(coveredPremium);Also applies to: 1102-1121
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/worker/src/worker.ts` around lines 1068 - 1075, The code only decreases remainingPremium when charging a dev pool, causing premium covered by chat-plan credits to remain and later be attributed to devPlanPremiumCreditsUsed; fix by computing the actual premium portion (set premiumTake = Decimal.min(remainingPremium, take)) for any pool that can consume premium credits (not only when pool.kind === "dev"), pass that premiumTake into deductFromPlanPool(orgId, pool, take, premiumTake), and always subtract remainingPremium = remainingPremium.minus(premiumTake) after the call (also ensure the later increment of devPlanPremiumCreditsUsed at the dev-cap block uses this same premiumTake) so premium usage is consumed at the time it is charged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/worker/src/log-processing.spec.ts`:
- Around line 609-651: The test seeds real credits as "0.00" which allows a
faulty spill-to-real implementation that floors negatives to zero to pass;
change the seeded credits in the db.update call (the organization update in this
test) to a non-zero value (e.g., "1.00") and then assert that
updatedOrg!.credits remains exactly that original non-zero value after calling
batchProcessLogs(), keeping the other devPlan fields the same so the assertion
on devPlanCreditsUsed still verifies overflow behavior; reference the
organization update block and the expect on updatedOrg!.credits and
batchProcessLogs() to locate where to modify and assert.
---
Outside diff comments:
In `@apps/worker/src/worker.ts`:
- Around line 1068-1075: The code only decreases remainingPremium when charging
a dev pool, causing premium covered by chat-plan credits to remain and later be
attributed to devPlanPremiumCreditsUsed; fix by computing the actual premium
portion (set premiumTake = Decimal.min(remainingPremium, take)) for any pool
that can consume premium credits (not only when pool.kind === "dev"), pass that
premiumTake into deductFromPlanPool(orgId, pool, take, premiumTake), and always
subtract remainingPremium = remainingPremium.minus(premiumTake) after the call
(also ensure the later increment of devPlanPremiumCreditsUsed at the dev-cap
block uses this same premiumTake) so premium usage is consumed at the time it is
charged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 383694ca-a479-454a-a92a-6257b1c60d65
📒 Files selected for processing (2)
apps/worker/src/log-processing.spec.tsapps/worker/src/worker.ts
| test("should charge dev plan overflow to virtual credits, not real credits", async () => { | ||
| // Active dev plan whose cycle allowance is nearly exhausted, with no | ||
| // real credits. An in-flight request that exceeds the remaining dev | ||
| // plan allowance must spill onto the dev plan (over its limit), not | ||
| // drive real credits negative. | ||
| await db | ||
| .update(organization) | ||
| .set({ | ||
| credits: "0.00", | ||
| devPlan: "pro", | ||
| devPlanCreditsLimit: "0.02", | ||
| devPlanCreditsUsed: "0.00", | ||
| }) | ||
| .where(eq(organization.id, testOrg.id)); | ||
|
|
||
| await db.insert(log).values({ | ||
| requestId: "test-request-dev-overflow", | ||
| organizationId: testOrg.id, | ||
| projectId: testProject.id, | ||
| apiKeyId: testApiKey.id, | ||
| cost: 0.05, | ||
| cached: false, | ||
| usedMode: "credits", | ||
| duration: 1000, | ||
| requestedModel: "openai/gpt-4o-mini", | ||
| requestedProvider: "openai", | ||
| usedModel: "gpt-4o-mini", | ||
| usedProvider: "openai", | ||
| responseSize: 100, | ||
| mode: "credits", | ||
| }); | ||
|
|
||
| await batchProcessLogs(); | ||
|
|
||
| const updatedOrg = await db.query.organization.findFirst({ | ||
| where: { id: { eq: testOrg.id } }, | ||
| }); | ||
|
|
||
| // Real credits untouched; the full cost lands on the dev plan even | ||
| // though it pushes usage past the limit. | ||
| expect(Number(updatedOrg!.credits)).toBe(0); | ||
| expect(Number(updatedOrg!.devPlanCreditsUsed)).toBeCloseTo(0.05, 10); | ||
| }); |
There was a problem hiding this comment.
The overflow test can pass even if real-credit spill regresses.
Line 617 seeds real credits at 0.00, and Line 649 asserts 0. A spill-to-real implementation that floors to zero would still pass. Seed with non-zero credits and assert they remain unchanged.
Suggested test tightening
.set({
- credits: "0.00",
+ credits: "1.00",
devPlan: "pro",
devPlanCreditsLimit: "0.02",
devPlanCreditsUsed: "0.00",
})
@@
- expect(Number(updatedOrg!.credits)).toBe(0);
+ expect(Number(updatedOrg!.credits)).toBe(1);
expect(Number(updatedOrg!.devPlanCreditsUsed)).toBeCloseTo(0.05, 10);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/worker/src/log-processing.spec.ts` around lines 609 - 651, The test
seeds real credits as "0.00" which allows a faulty spill-to-real implementation
that floors negatives to zero to pass; change the seeded credits in the
db.update call (the organization update in this test) to a non-zero value (e.g.,
"1.00") and then assert that updatedOrg!.credits remains exactly that original
non-zero value after calling batchProcessLogs(), keeping the other devPlan
fields the same so the assertion on devPlanCreditsUsed still verifies overflow
behavior; reference the organization update block and the expect on
updatedOrg!.credits and batchProcessLogs() to locate where to modify and assert.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 413ef4d39e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } else if (org && org.kind !== "default") { | ||
| // Non-default orgs (devpass/chat) run entirely on virtual plan | ||
| // credits — their `credits` field is unused. Never subtract from | ||
| // it. With no active plan pool to absorb the cost (e.g. usage that | ||
| // drained after the plan was cancelled and its virtual-credit pool | ||
| // was torn down), the residual is written off. | ||
| logger.debug( |
There was a problem hiding this comment.
Debit Chat pay-as-you-go credits instead of writing them off
For a kind: "chat" org with chatPlan === "none" and a positive pay-as-you-go balance, there is no plan pool, so every charged request reaches this new write-off branch. The gateway still admits those requests based on organization.credits (apps/gateway/src/chat/chat.ts:4506-4518), but this branch leaves that balance unchanged, so a Chat user with any positive credit balance can keep making paid requests indefinitely without the worker ever debiting the credits that authorized them. This should still deduct real credits for Chat PAYG balances and reserve the write-off for the cancelled DevPass residual case.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 798d1ab5e3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| logger.debug( | ||
| `Charged ${remainingCost.toString()} overflow to dev plan virtual credits for organization ${orgId}`, | ||
| ); | ||
| } else if (chatPool) { |
There was a problem hiding this comment.
Debit PAYG credits after active Chat plan exhausts
When a Chat org has an active plan plus pay-as-you-go credits, requests are admitted from regularCredits + chatPlanCreditsRemaining, and the Chat pricing UI says PAYG credits remain available after plan credits drain. If one request exceeds the remaining plan allowance, remainingCost is the overage that the PAYG balance made billable, but this branch increments chatPlanCreditsUsed past the limit instead of deducting organization.credits; that overage is then effectively reset at renewal, and before renewal users can be blocked while their PAYG balance is still untouched.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7fbc579657
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| org.kind !== "default" && | ||
| new Decimal(org.credits ?? "0").lessThanOrEqualTo(0) |
There was a problem hiding this comment.
Keep debiting Chat PAYG after balance crosses zero
Fresh scenario not covered by the positive-balance fix: a kind: "chat" / chatPlan: "none" PAYG org can have many already-admitted logs because the gateway authorizes from organization.credits before asynchronous billing (apps/gateway/src/chat/chat.ts:4506-4518) and the worker only processes bounded batches. If the first batch drives credits to <= 0, later queued PAYG logs hit this non-default/no-balance branch and are written off instead of continuing to debit the real credits balance negative, so a burst that spans batches gets partially free usage. Reserve this write-off for cancelled-plan residuals, not Chat PAYG usage that was admitted while the balance was positive.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9486c4cc32
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if ( | ||
| org && | ||
| org.kind === "devpass" && | ||
| new Decimal(org.credits ?? "0").lessThanOrEqualTo(0) |
There was a problem hiding this comment.
Keep debiting devpass real-credit bursts after zero
For a devpass org that did have real credits (the new comment and tests cover referral/top-up balances, and the gateway admits from organization.credits + planRemaining in apps/gateway/src/chat/chat.ts), a burst can be authorized while that balance is positive but processed over multiple worker batches. After the first batch drives credits to <= 0, later already-admitted logs hit this write-off branch and are no longer debited, so part of the real-credit-funded usage becomes free; this needs to distinguish cancelled-plan residuals from devpass PAYG/overage that was admitted on a positive real balance.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d4a8f93640
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Real `credits` is only used by pay-as-you-go (default) orgs. chat/devpass | ||
| // orgs run purely on virtual plan credits, so gifting them real credits would | ||
| // create a balance nothing bills against — reject it here. | ||
| if (org.kind !== "default") { |
There was a problem hiding this comment.
For kind: "chat" orgs this rejects the same credits balance that powers Chat pay-as-you-go: getOrCreateChatOrg documents that the Chat org owns chat plans, PAYG top-ups, and playground billing, and it migrates an existing PAYG balance into organization.credits (apps/api/src/utils/personal-org.ts:57-63, 90-99). Since the admin org detail page still exposes the Gift Credits action for these orgs, support can no longer grant or adjust Chat PAYG credits and will always get a 400; this guard should not block Chat orgs unless there is a replacement path for gifting their real balance.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3e72e0e04b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // org used by the coding product. Chat orgs run purely on virtual chat-plan | ||
| // credits and never hold a real `credits` balance — pay-as-you-go lives on | ||
| // default orgs. |
There was a problem hiding this comment.
Route PAYG media users to their funded org
When PAYG stops being copied into the Chat org, opening /image, /video, or /audio without an orgId but with credits on a default org now selects the freshly-created zero-credit Chat org first (apps/playground/src/app/image/page.tsx:92-95, same pattern in video/audio). Those clients then base the out-of-credits state on the Chat org plan status (image-page-client.tsx:787-795), so funded PAYG users are shown the plan upsell/blocked until they manually switch orgs; the chat shell has a redirect for this case, but the media pages do not.
Useful? React with 👍 / 👎.
3e72e0e to
49ed972
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Dev-plan usage that drained after the plan's cycle limit (or after the plan was cancelled and its virtual-credit pool was torn down) spilled onto organization.credits with no floor, driving dev-plan orgs — who hold no real credits — negative. Now: an active dev plan absorbs overflow into its virtual credits (devPlanCreditsUsed past the limit), and any remaining real-credit deduction is floored at 0 so balances never go negative. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dev-plan usage that drained after the cycle limit (or after the plan was cancelled and its virtual-credit pool was torn down) spilled onto organization.credits with no guard, driving devpass/chat orgs negative — even though those orgs run entirely on virtual plan credits and never use the credits field. Worker fallback now: - active dev/chat plan -> absorb overflow into the plan's virtual credits (used past the limit), not real credits - non-default org (devpass/chat) with no active pool -> write off; never touch the unused credits field - default org -> deduct real credits as before, still allowed to go negative so the balance reconciles on the next top-up Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The write-off branch covered all non-default orgs, but chat orgs use the real credits balance for pay-as-you-go (chatPlan none + topped-up credits). The gateway authorizes those requests against credits, so writing the cost off let a chat PAYG user spend the same balance forever. Restrict the write-off to non-default orgs with no real balance (credits <= 0) — the cancelled-plan/devpass residual case. Chat PAYG and default orgs both debit credits as before (negative allowed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-up: the fallback pushed any leftover after draining a plan pool back onto that plan's virtual counter whenever the pool existed. But the gateway admits requests on credits + devRemaining + chatRemaining, so an exhausted-plan org with a real balance (referral/top-up, or chat PAYG) had its overage charged to devPlanCreditsUsed/chatPlanCreditsUsed instead of the real credits that authorized it — spending the balance silently and resetting at renewal. Drop the overflow-to-virtual branches. Plan pools still absorb everything up to their limit in drainBucket; any leftover is billed to real credits (default + chat PAYG, may go negative, reconciled on top-up) or written off only for a non-default org with no real balance (devpass/cancelled residual). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-up (P1): the write-off branch keyed on kind != default && credits <= 0, which also caught a chat pay-as-you-go org whose balance crossed zero mid-burst — later queued logs were written off instead of continuing to debit negative, giving partially free usage across batches. Restrict the write-off to devpass (the only kind whose real credits field is genuinely unused). chat and default always debit real credits, going negative and reconciling on the next top-up. A devpass org that holds a real balance (e.g. referral earnings) still debits before any write-off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per product model: pay-as-you-go lives on default orgs; chat and devpass orgs run purely on virtual plan credits and must never touch the real credits field. worker: write off leftover for any non-default org with no real balance (was devpass-only), so chat usage with no plan/balance writes off like devpass instead of debiting. A stray/legacy real balance is still debited (credits <= 0 guard) so it can't be spent for free. admin: reject gift-credits for non-default orgs — gifting is only valid for pay-as-you-go (default) organizations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Chat orgs run purely on virtual chat-plan credits and must never hold a real credits balance (pay-as-you-go lives on default orgs). Creating a chat org no longer migrates a personal org's real balance into it, which directly contradicted that model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
49ed972 to
c2b0c46
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Closing in favour of #3705. This branch was built on the premise that chat/DevPass orgs hold no real
The one piece that was still valid — plan overshoot landing on real credits — is fixed in #3705, cut fresh from current |
## Problem Requests are admitted while a plan pool still has room, so a burst can collectively cost more than was left. Today that overshoot falls through to the org's real `credits` balance and drives it negative for two cases `plannedOverflowOnly` doesn't cover: - **Chat plans** — the guard requires `devPlan !== "none"`, so a chat-plan subscriber's overshoot always hits real credits. - **Cancelled plans** — the pool is torn down on cancellation, so usage it had already authorized lands on `credits` afterwards. For chat/devpass orgs that never opted into pay-as-you-go this is a real charge, not just cosmetic bookkeeping. `getAvailableCredits` computes `regularCredits + devPlanRemaining + chatPlanRemaining`, so a negative balance **silently docks the next cycle's plan allowance** — the subscriber ends up paying for overshoot out of the plan they already bought. ## Fix In the residual branch of `batchProcessLogs`, for non-`default` orgs that have not enabled PAYG overflow: - charge only what the real balance can actually cover (a partial balance is still fully spent — no free usage), - route the remainder to an active chat/dev plan pool so the usage stays accounted for, - write it off when the plan is already gone and there is no pool left to hold it. Deliberately unchanged: `default` (pay-as-you-go) orgs and `devPlanPaygEnabled` opt-ins may still go negative and are reconciled on the next top-up. Opting into PAYG is an explicit request to bill real money past the allowance, and auto top-up exists to settle it — flooring those would lose genuinely incurred usage. ## Scope note Once a plan is cancelled its pool is gone, so late-arriving usage it authorized can only be written off rather than billed precisely. Attributing it exactly would require recording which pool admitted each request at log time — a schema change well outside this diff. ## Tests Five tests in `log-processing.spec.ts`; suite is 32/32 green. Verified the tests actually catch the bug: with `worker.ts` reverted, the three bug-targeting tests fail, while the two regression guards (`default` org goes negative, PAYG-enabled devpass goes negative) pass either way — they pin behavior this PR intentionally leaves alone. `pnpm format` clean, `pnpm build` green (17/17). The 9 failures in `sync-models.spec.ts` are pre-existing on `main` and unrelated. ## Supersedes Replaces #2658, which was built on the premise that chat/devpass orgs hold no real credits. That premise no longer holds after #3430/#3473/#3560 — devpass gifting is deliberate and the admin UI exposes it — so that branch is best closed rather than rebased. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Prevented eligible plan-funded accounts without pay-as-you-go billing from reaching negative credit balances. * Usage now deducts available credits first, then applies remaining usage to the appropriate plan allowance. * Cancelled-plan residual usage is written off when no applicable allowance remains. * Planless personal accounts and pay-as-you-go-enabled accounts can retain negative balances after overage. * Default billing behavior remains unchanged. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Problem
A personal devpass org ended up with -$41.67 real credits, despite dev-plan usage being meant to run on virtual credits.
Root cause
Credits are gated softly at request time but billed asynchronously in the worker, which drains plan pools and applies any leftover to
organization.credits. When a dev plan is cancelled,apps/api/src/stripe.tsimmediately zeroes the virtual-credit pool; already-queued logs then drain with no pool and the catch-all subtractedcreditsunconditionally → negative, on a field devpass orgs never use.Billing model (clarified in review)
defaultorgs only. They use the realcreditsbalance.chatanddevpassorgs run purely on virtual plan credits (chat plan / dev plan) and never touch the realcreditsfield.Changes
apps/worker/src/worker.tsdrainBucketdrains the chat/dev plan pools up to their cycle limits (source-aware). For the leftover after draining:credits <= 0) → write off (residual of an exhausted/cancelled plan, nothing real to charge).credits(default PAYG; plus any stray/legacy non-default balance so it can't be spent for free). May go negative; reconciled on next top-up.Plan overage is never pushed back onto a plan's virtual counter — the gateway admits on
credits + devRemaining + chatRemaining, so overage backed by a real balance must hit real credits (else it spends the balance silently and resets at renewal).apps/api/src/routes/admin.tsAdmin gift-credits rejected for non-default orgs (
400). Real credits can only be gifted to pay-as-you-go (default) organizations.apps/api/src/utils/personal-org.tsStopped migrating a real PAYG balance into the chat org on creation. That directly contradicted the model (chat orgs are virtual-only). Chat orgs are now created with a zero balance.
Tests (
apps/worker/src/log-processing.spec.ts, 18 total)All 18 pass; full
pnpm buildgreen.Follow-ups (flagged, not in this PR)
creditsfor chat/devpass orgs; ignoring it for non-default kinds would let us drop thecredits <= 0safety guard entirely. Kept the guard for now so any stray/legacy balance is debited rather than leaked.🤖 Generated with Claude Code
Summary by CodeRabbit