Skip to content

fix: keep plan usage off real credits (PAYG=default only) - #2658

Closed
steebchen wants to merge 8 commits into
mainfrom
fix/devpass-no-negative-credits
Closed

steebchen wants to merge 8 commits into
mainfrom
fix/devpass-no-negative-credits

Conversation

@steebchen

@steebchen steebchen commented Jun 12, 2026 •

Copy link
Copy Markdown
Member

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.ts immediately zeroes the virtual-credit pool; already-queued logs then drain with no pool and the catch-all subtracted credits unconditionally → negative, on a field devpass orgs never use.

Billing model (clarified in review)

  • Pay-as-you-go lives on default orgs only. They use the real credits balance.
  • chat and devpass orgs run purely on virtual plan credits (chat plan / dev plan) and never touch the real credits field.

Changes

apps/worker/src/worker.ts

drainBucket drains the chat/dev plan pools up to their cycle limits (source-aware). For the leftover after draining:

  • Non-default org (chat/devpass) with no real balance (credits <= 0) → write off (residual of an exhausted/cancelled plan, nothing real to charge).
  • Everything else → deduct real 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.ts

Admin 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.ts

Stopped 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)

  • default org → goes negative on overage
  • devpass residual (cancelled plan, no balance) → written off
  • chat usage, no plan, no balance → written off (same as devpass)
  • dev plan exhausted, no balance → drained to limit, overage written off
  • chat plan exhausted, no balance → drained to limit, overage written off
  • non-default org with a stray real balance → still debited (safety)
  • plan overage with a stray balance → debited from real credits, not over-counted on the virtual counter

All 18 pass; full pnpm build green.

Follow-ups (flagged, not in this PR)

  • Gateway admission still counts real credits for chat/devpass orgs; ignoring it for non-default kinds would let us drop the credits <= 0 safety guard entirely. Kept the guard for now so any stray/legacy balance is debited rather than leaked.
  • Existing legacy chat orgs that already received a migrated balance need a data cleanup (move back to a default org / refund).
  • The original -$41.67 org is stale data this fix doesn't retroactively correct — happy to add a one-off reset.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Prevented credit balances from going negative when billed costs exceed available credits.
    • Improved dev-plan overflow handling by charging eligible excess costs to virtual credits while preserving real credits.
    • Prevented real-credit gifts to non-default organization types.
    • Ensured new Chat organizations use virtual chat-plan credits without inheriting personal credit balances.
  • Tests
    • Added coverage for credit flooring and dev-plan overflow accounting.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6723777a-f63a-4fd4-b2e0-a10aaeaa998a

📥 Commits

Reviewing files that changed from the base of the PR and between 49ed972 and a1784da.

📒 Files selected for processing (2)
  • apps/api/src/routes/admin.ts
  • apps/api/src/utils/personal-org.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/src/routes/admin.ts
  • apps/api/src/utils/personal-org.ts

Walkthrough

This PR updates worker credit draining, removes Chat organization credit migration, adds credit-accounting tests, and rejects real-credit gifts for non-default organizations.

Changes

Credit policy updates

Layer / File(s) Summary
Worker credit drain and overflow handling
apps/worker/src/worker.ts
drainBucket returns regular and premium remaining credits. batchProcessLogs uses both values for dev-plan overflow and floors regular credits at zero.
Worker credit accounting tests
apps/worker/src/log-processing.spec.ts
Two tests cover zero-flooring of real credits and charging dev-plan overflow to virtual credits without reducing real credits.
Chat org credit migration removal
apps/api/src/utils/personal-org.ts
Chat organization creation no longer reads or migrates credits from the DevPass personal organization.
Admin gift credits kind check
apps/api/src/routes/admin.ts
giftCreditsRoute adds a 400 response and rejects real-credit gifts for non-default organizations.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: preventing plan usage from reducing real credits except for default PAYG organizations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/devpass-no-negative-credits

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread apps/worker/src/worker.ts Outdated
Comment on lines +1108 to +1119
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,

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 Badge 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 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 win

Premium overflow can be over-attributed to devPlanPremiumCreditsUsed.

At Line 1068–1074, remainingPremium is only reduced when the dev pool is charged. Premium cost covered by chat-plan credits remains in remainingPremium, and Line 1120 can then apply that leftover as premium on unrelated overflow. That can over-increment devPlanPremiumCreditsUsed and 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3ed24a3 and cec93f5.

📒 Files selected for processing (2)
  • apps/worker/src/log-processing.spec.ts
  • apps/worker/src/worker.ts

Comment thread apps/worker/src/log-processing.spec.ts Outdated
Comment on lines +609 to +651
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);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

@steebchen steebchen changed the title fix(worker): keep dev plan overflow off real credits fix(worker): never charge plan usage to org credits Jun 21, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread apps/worker/src/worker.ts Outdated
Comment on lines +1264 to +1270
} 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(

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 Badge 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 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread apps/worker/src/worker.ts Outdated
logger.debug(
`Charged ${remainingCost.toString()} overflow to dev plan virtual credits for organization ${orgId}`,
);
} else if (chatPool) {

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 Badge 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 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread apps/worker/src/worker.ts
Comment on lines +1230 to +1231
org.kind !== "default" &&
new Decimal(org.credits ?? "0").lessThanOrEqualTo(0)

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 Badge 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 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread apps/worker/src/worker.ts
Comment on lines +1228 to +1231
if (
org &&
org.kind === "devpass" &&
new Decimal(org.credits ?? "0").lessThanOrEqualTo(0)

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 Badge 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 👍 / 👎.

@steebchen steebchen changed the title fix(worker): never charge plan usage to org credits fix: keep plan usage off real credits (PAYG=default only) Jun 24, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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") {

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 Badge Allow gifts to Chat PAYG orgs

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 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment on lines +59 to +61
// 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.

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 Badge 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 👍 / 👎.

Copilot AI review requested due to automatic review settings July 9, 2026 19:00
@steebchen
steebchen force-pushed the fix/devpass-no-negative-credits branch from 3e72e0e to 49ed972 Compare July 9, 2026 19:00
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

steebchen and others added 7 commits July 28, 2026 11:40
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>
@steebchen
steebchen force-pushed the fix/devpass-no-negative-credits branch from 49ed972 to c2b0c46 Compare July 28, 2026 11:02
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@steebchen

Copy link
Copy Markdown
Member Author

Closing in favour of #3705.

This branch was built on the premise that chat/DevPass orgs hold no real credits and run purely on virtual plan credits. That is no longer true after #3430, #3473 and #3560:

  • getAvailableCredits zeroes the real balance only for devPlan !== "none" && !devPlanPaygEnabled; a chat org counts its full balance.
  • Gifting credits to DevPass orgs is deliberate and supported — the admin UI renders GiftCreditsDialog on the DevPass org page — so this branch's 400 on non-default kinds would break a shipped button.
  • Chat orgs are the playground's billing home for pay-as-you-go top-ups, so removing the balance migration would strand a user's balance on the DevPass org.

The one piece that was still valid — plan overshoot landing on real credits — is fixed in #3705, cut fresh from current main and scoped to the worker.

@steebchen steebchen closed this Aug 21, 2026
steebchen added a commit that referenced this pull request Aug 21, 2026
## 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>
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.

2 participants