Skip to content

feat(billing): configurable org auto top-up threshold and $5,000 amount - #7612

Merged
iscekic merged 6 commits into
mainfrom
feat/org-auto-top-up-threshold
Oct 16, 2026
Merged

iscekic merged 6 commits into
mainfrom
feat/org-auto-top-up-threshold

Conversation

@emilieschario

@emilieschario emilieschario commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Organization automatic top up always fired when the balance dropped below a fixed $50. A customer asked for this to be configurable.

  • Orgs can now choose the balance that triggers a top up: $50, $100, or $500 (default $50, so existing configs keep their current behavior).
  • Adds $5,000 to the org top-up amounts ($100 / $500 / $1,000 / $5,000).
  • New rule: the top-up amount must be at least the trigger balance. Without it, a $100 top up with a $500 trigger would leave the balance below the trigger after every charge and charge again on the next usage event. The server rejects invalid combinations; the UI disables invalid dropdown options and blocks enabling with an inline error.

Implementation:

  • auto_top_up_configs.threshold_cents — new nullable integer column (migration 0281, additive only). NULL means the default org threshold; personal configs ignore it.
  • autoTopUp.ts — the org trigger check still short-circuits with no DB read when the balance is at or above the maximum option ($500). Below that, it does one read of the org's configured threshold. The post-lock re-check uses the threshold returned by the lock query.
  • Router: getConfig returns thresholdCents; toggle and changePaymentMethod accept it and fall back to the saved config, not hard-coded defaults; new updateThreshold mutation; updateAmount applies the validation rule.
  • Stripe setup checkout passes thresholdCents through PaymentIntent metadata and the org-auto-topup-setup webhook persists it. The product description shows the chosen trigger balance.
  • Personal (non-org) auto top-up is unchanged ($5 trigger; $20 / $50 / $100).

Verification

  • Rendered the enabled settings row and the configure dialog in Storybook with seeded getConfig data and screenshotted them with Playwright (below): $5,000 option, threshold dropdown, invalid amount options disabled, inline error and disabled enable button for an invalid combination.
  • End to end on a local stack (Next.js, Postgres, Stripe test mode, stripe listen forwarding webhooks):
    • Configure dialog: a $500 trigger with a $100 amount shows the inline error and disables the enable button.
    • Setup checkout with a $500 trigger and a $1,000 amount: the Stripe line item reads "drops below $500.00", and the webhook saved threshold_cents = 50000 and credited the org.
    • Usage trigger with a $500 threshold: a balance below $500 charged $1,000 off session, and invoice.paid credited it and released the lock.
    • Changed the amount to $5,000 and the trigger to $100 in the settings row. The $100 amount option was disabled while the trigger was $500.
    • With a $100 threshold, a balance above $100 did not charge; a balance below $100 charged $5,000.
    • Direct tRPC calls: updateAmount below the threshold returns BAD_REQUEST; updateThreshold with an unsupported value fails input validation.
    • Change payment method with a saved $500 amount and $100 trigger: the new checkout reads "drops below $100.00", and after payment the config kept $500 / $100 with the new card.
    • Disable, then re-enable through the dialog with the saved card: the dialog prefilled $100 / $500, enabling with a $500 trigger used the saved card without a checkout or charge, and saved threshold_cents = 50000.
  • Migration renumbered to 0282 after main added its own 0281; regenerated with drizzle-kit generate (same single ADD COLUMN).
  • DB-backed Jest suites pass: autoTopUp.test.ts, organization-auto-top-up-router.test.ts, stripe/index.test.ts.
  • Ran: apps/web typecheck, oxlint and oxfmt on changed files.

Visual Changes

Before: the settings row read "Automatically top up [amount] when balance drops below $50." with only amounts $100 / $500 / $1,000, and the dialog had a single amount dropdown.

Screen After
Enabled settings — threshold is now a dropdown 1-enabled-settings.png
Amount options — $5,000 added; $100 disabled because the threshold is $500 2-amount-options.png
Configure dialog — threshold and amount 3-configure-dialog.png
Configure dialog — invalid combination blocked 4-configure-dialog-invalid.png

Reviewer Notes

  • MCP write exposure: this adds the organizations.autoTopUp.updateThreshold mutation. It only changes when an already-configured automatic top up triggers. It does not create a charge itself. services/kilo-mcp/catalog.json is regenerated by the kilo-mcp-catalog workflow.
  • In-flight setup checkout sessions created before deploy have no thresholdCents metadata. The webhook stores the $50 default for those, which matches current behavior.

@emilieschario emilieschario self-assigned this Oct 15, 2026
@kilo-code-bot

kilo-code-bot Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The only change since the previous review is a correct formatCents fix for the org auto top-up checkout line-item description (so $5,000 renders as $5,000.00 and the threshold is no longer hard-coded) plus three added router tests; no regressions or new defects found.

Files Reviewed (2 files)
  • apps/web/src/lib/organizations/organization-auto-top-up.ts
  • apps/web/src/routers/organizations/organization-auto-top-up-router.test.ts

Incremental scope: only the two files above changed within the PR since f4468b9. The remaining PR files were carried forward from the prior review with no new edits, and the intervening branch merge of main is not this PR's authorship. Verification is static: no tests were executed in this read-only review.

Previous Review Summary (commit f4468b9)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit f4468b9)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Reviewed the configurable org auto top-up threshold feature end to end (DB column + migration 0281, autoTopUpConstants/autoTopUp trigger logic and lock re-check, organization-auto-top-up Stripe checkout metadata, the Stripe webhook persistence, the tRPC router validation, both UI components, tests, and the regenerated MCP catalog) and found no defects; the new organizations.autoTopUp.updateThreshold MCP mutation only changes when an already-enabled top-up triggers and does not itself charge or expose credentials, matching the existing autoTopUp mutations the catalog already publishes.

Files Reviewed (16 files)
  • .kilo/skills/kilo-mcp/SKILL.md
  • apps/web/src/components/organizations/OrganizationAutoTopUpConfigureDialog.tsx
  • apps/web/src/components/organizations/OrganizationAutoTopUpToggle.tsx
  • apps/web/src/lib/organizations/organization-auto-top-up.ts
  • apps/web/src/lib/stripe/index.test.ts
  • apps/web/src/lib/stripe/index.ts
  • apps/web/src/routers/organizations/organization-auto-top-up-router.test.ts
  • apps/web/src/routers/organizations/organization-auto-top-up-router.ts
  • packages/db/src/migrations/0281_volatile_james_howlett.sql
  • packages/db/src/migrations/meta/0281_snapshot.json
  • packages/db/src/migrations/meta/_journal.json
  • packages/db/src/schema.ts
  • packages/web-shared/src/lib/autoTopUp.test.ts
  • packages/web-shared/src/lib/autoTopUp.ts
  • packages/web-shared/src/lib/autoTopUpConstants.ts
  • services/kilo-mcp/catalog.json

Notes: the threshold_cents migration is an additive nullable ADD COLUMN (no table rewrite/lock concern). Verification reliance: DB-backed Jest suites were not run locally per the PR, so runtime confidence rests on read-through analysis plus the updated tests.


Reviewed by deepseek-v4.1-flash · Input: 52.4K · Output: 9.2K · Cached: 574.8K

Review guidance: REVIEW.md from base branch main

@emilieschario

Copy link
Copy Markdown
Contributor Author

re-assigning to @iscekic who is going to run with this.

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