Skip to content

feat(billing): add fixed-threshold auto top-up API behind a feature flag - #3533

Open
baktun14 wants to merge 2 commits into
mainfrom
feat/billing-fixed-threshold-auto-reload
Open

feat(billing): add fixed-threshold auto top-up API behind a feature flag#3533
baktun14 wants to merge 2 commits into
mainfrom
feat/billing-fixed-threshold-auto-reload

Conversation

@baktun14

@baktun14 baktun14 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Why

Auto reload currently predicts upcoming spend: it projects the cost of all auto-top-up deployments over the next 7 days, triggers when the balance is below 25% of that projection, and charges max(projection − balance, $20). Both charge timing and amount shift as deployments change, and the settings UI can only say "approximately $X per week".

CON-717 replaces this with a fixed, user-configurable rule: when the credit balance is at or below threshold X, charge the default payment method exactly amount Y.

This is the backend half of CON-717, split out from the original combined PR. The frontend is in #3535 (stacked on this branch — it consumes the wallet-settings types regenerated here). This PR is safe to merge on its own: everything is behind a flag that is off by default.

Part of CON-717

What

Everything is gated behind a new feature flag auto_reload_fixed_threshold (off by default). With the flag off, the legacy predicted-spend algorithm is untouched.

  • Re-add auto_reload_threshold / auto_reload_amount columns (NUMERIC NOT NULL, defaults $20 / $100). The additive ADD COLUMN … DEFAULT … NOT NULL migration backfills existing enabled users — that is the backfill story, no script.
  • Split the wallet-settings HTTP schema into input/output: threshold >= 5, amount >= $20, both optional on write (DB defaults on create).
  • WalletBalanceReloadCheckHandler branches on the flag: flag on charges max(autoReloadAmount, $20) when balance <= autoReloadThreshold (inclusive), fires even with no active deployments, and never projects spend; flag off keeps the legacy path.
  • Schedule an immediate reload check when threshold/amount change while enabled (backs the dialog's "top-up runs shortly after saving").
  • Instrumentation reworked to serve both paths; createPaymentIntent unchanged (confirm: true, job-scoped idempotency key, onAmountMismatch: "tolerate").
  • Reload-check README rewritten with the new algorithm as primary.

Behavior change at flag GA (needs a release note): enabled users with a balance ≤ their threshold get charged the fixed amount on their next check (spend-event-driven or the daily safety net). This was product-approved.

Rollout note for ops: in background jobs there is no unleash userId, so the flag acts as a global on/off there (fine as a kill-switch, not per-user rollout). Metric changes (projected_cost_usd, zero_cost reason) are deferred to the flag-cleanup follow-up.

Follow-ups (filed separately): CON-763 — flag cleanup (remove the legacy algorithm, weekly-cost chain + GET /v1/weekly-cost, old UI branch, and legacy metrics); CON-764 — notification on auto-top-up payment failure.

Testing

  • reload-check handler (legacy + fixed-threshold cases incl. boundary, no-deployments, $20 clamp), wallet-settings service (immediate check on value change, enable requires payment method), controller pass-through, instrumentation, seeder ranges, and a functional test asserting PUT /v1/wallet-settings 400s on amount < 20 / threshold < 5. docs.spec snapshot regenerated.
  • npm run lint -- --quiet and npx tsc --noEmit clean for the changed files.

Summary by CodeRabbit

  • New Features
    • Added configurable automatic wallet top-ups using a balance threshold and reload amount.
    • Added validation, defaults, API support, and currency handling for reload settings.
    • Added a feature flag for the fixed-threshold reload strategy.
    • Reload charges now enforce a minimum amount of $20.
  • Bug Fixes
    • Improved reload scheduling when settings are enabled or changed.
    • Added safeguards for missing payment methods and invalid configuration values.
  • Documentation
    • Updated API documentation and automatic reload behavior guidance.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR adds persisted wallet auto-reload threshold and amount settings. It exposes validated API contracts, centralizes the $20 minimum, adds fixed-threshold reload execution behind a feature flag, updates instrumentation, and adjusts reload scheduling.

Changes

Auto top-up configuration

Layer / File(s) Summary
Wallet settings contracts and persistence
apps/api/drizzle/..., apps/api/src/billing/config/..., apps/api/src/billing/http-schemas/..., apps/api/src/billing/model-schemas/..., apps/api/src/billing/repositories/..., apps/api/src/billing/services/wallet-settings/..., apps/api/swagger/openapi.json, packages/console-api-types/src/schema.d.ts
Adds auto-reload fields, USD validation, cent-based persistence, generated API types, and schedule updates.
Fixed-threshold reload execution
apps/api/src/core/services/feature-flags/feature-flags.ts, apps/api/src/billing/services/wallet-balance-reload-check/...
Adds feature-flagged threshold checks, minimum charge clamping, idempotent payment creation, and structured reload outcomes.
Validation and behavior coverage
apps/api/test/functional/wallet-settings.spec.ts, apps/api/src/billing/controllers/wallet-settings/wallet-settings.controller.spec.ts, apps/api/src/billing/services/wallet-balance-reload-check/*.spec.ts, apps/api/src/billing/lib/currency/currency.spec.ts, apps/api/test/seeders/wallet-setting.seeder.ts
Tests API validation, currency conversion, scheduling, threshold boundaries, zero projected cost, and payment failures.
Shared top-up configuration
apps/api/src/billing/services/trial-validation/trial-validation.service.ts
Uses the shared STANDARD_TOP_UP_MIN_AMOUNT_USD value for non-trial validation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: ygrishajev

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/billing-fixed-threshold-auto-reload

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/api/src/billing/lib/currency/currency.spec.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

apps/api/src/billing/lib/currency/currency.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 5 others

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

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.29%. Comparing base (c79291b) to head (9e38eee).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3533      +/-   ##
==========================================
- Coverage   75.08%   74.29%   -0.79%     
==========================================
  Files        1162     1074      -88     
  Lines       30161    27852    -2309     
  Branches     7502     7058     -444     
==========================================
- Hits        22645    20693    -1952     
+ Misses       6644     6320     -324     
+ Partials      872      839      -33     
Flag Coverage Δ *Carryforward flag
api 88.86% <100.00%> (+0.25%) ⬆️
deploy-web 64.66% <ø> (ø)
log-collector ?
notifications 93.84% <ø> (ø) Carriedforward from e8f0db4
provider-console 81.38% <ø> (ø) Carriedforward from e8f0db4
provider-inventory ?
provider-proxy 88.17% <ø> (ø) Carriedforward from e8f0db4
tx-signer ?

*This pull request uses carry forward flags. Click here to find out more.

Files with missing lines Coverage Δ
apps/api/src/billing/config/top-up.config.ts 100.00% <100.00%> (ø)
apps/api/src/billing/lib/currency/currency.ts 100.00% <100.00%> (ø)
...el-schemas/wallet-setting/wallet-setting.schema.ts 60.00% <ø> (ø)
...ries/wallet-settings/wallet-settings.repository.ts 57.14% <ø> (ø)
...vices/trial-validation/trial-validation.service.ts 64.55% <ø> (-0.45%) ⬇️
...et-balance-reload-check-instrumentation.service.ts 68.88% <100.00%> (+27.97%) ⬆️
...eload-check/wallet-balance-reload-check.handler.ts 100.00% <100.00%> (+2.38%) ⬆️
...ervices/wallet-settings/wallet-settings.service.ts 97.05% <100.00%> (+0.76%) ⬆️
...i/src/core/services/feature-flags/feature-flags.ts 100.00% <ø> (ø)

... and 91 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@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: 2

🧹 Nitpick comments (2)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)

6-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Drop #MIN_RELOAD_AMOUNT_IN_USD in favor of the newly imported shared constant.

Line 53 still hardcodes 20 while the fixed-threshold path uses STANDARD_TOP_UP_MIN_AMOUNT_USD imported on line 6 — the two minimums can now drift, which is the exact duplication top-up.config.ts was added to eliminate.

♻️ Proposed change
-  `#MIN_RELOAD_AMOUNT_IN_USD` = 20;
-
-    const reloadAmountInFiat = Math.max(costUntilTargetDateInFiat - resources.balance, this.#MIN_RELOAD_AMOUNT_IN_USD);
+    const reloadAmountInFiat = Math.max(costUntilTargetDateInFiat - resources.balance, STANDARD_TOP_UP_MIN_AMOUNT_USD);

Also applies to: 53-53

🤖 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/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts`
at line 6, Replace the hardcoded 20 in the wallet balance reload check with the
imported STANDARD_TOP_UP_MIN_AMOUNT_USD constant, and remove the obsolete
`#MIN_RELOAD_AMOUNT_IN_USD` definition if present. Keep both threshold paths
aligned through the shared constant.
apps/deploy-web/src/components/billing-usage/AutoTopUpSettingsPopup/AutoTopUpSettingsPopup.spec.tsx (1)

127-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Nested object passed as mock<T>() override.

mock<PaymentMethod>({ card: {...} }) passes a plain nested object as an override. Based on learnings, this pattern can wrap card in a recursive proxy, causing any of its unset sub-properties to resolve to mock functions instead of undefined — assign paymentMethod.card = {...} after creating the mock instead.

🔧 Proposed fix
-    const paymentMethod = mock<PaymentMethod>({
-      card: { brand: "visa", last4: "5720", exp_month: 5, exp_year: 30 } as PaymentMethod["card"]
-    });
+    const paymentMethod = mock<PaymentMethod>();
+    paymentMethod.card = { brand: "visa", last4: "5720", exp_month: 5, exp_year: 30 } as PaymentMethod["card"];
🤖 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/deploy-web/src/components/billing-usage/AutoTopUpSettingsPopup/AutoTopUpSettingsPopup.spec.tsx`
around lines 127 - 129, Update the paymentMethod setup in the test to create the
PaymentMethod mock without the nested card override, then assign the plain card
object directly to paymentMethod.card afterward. Preserve the existing card
brand, last4, exp_month, and exp_year values while avoiding recursive proxying
of nested properties.

Source: Learnings

🤖 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/api/src/billing/http-schemas/wallet.schema.ts`:
- Around line 53-71: Update WalletSettingsInputSchema to add explicit
upper-bound validation to both autoReloadAmount and autoReloadThreshold, using
maximums compatible with the database numeric(20, 2) range and safe charging
behavior in `#tryToReloadOnFixedThreshold`. Keep the existing minimum, precision,
optionality, and OpenAPI descriptions intact so oversized requests are rejected
during schema validation.

In
`@apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts`:
- Around line 181-213: Update `#tryToReloadOnFixedThreshold` to evaluate
AUTO_RELOAD_FIXED_THRESHOLD using the job payload’s real user ID rather than the
worker’s CURRENT_USER value; pass payload.userId into the flag context used by
this reload decision while preserving the existing threshold and reload
behavior.

---

Nitpick comments:
In
`@apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts`:
- Line 6: Replace the hardcoded 20 in the wallet balance reload check with the
imported STANDARD_TOP_UP_MIN_AMOUNT_USD constant, and remove the obsolete
`#MIN_RELOAD_AMOUNT_IN_USD` definition if present. Keep both threshold paths
aligned through the shared constant.

In
`@apps/deploy-web/src/components/billing-usage/AutoTopUpSettingsPopup/AutoTopUpSettingsPopup.spec.tsx`:
- Around line 127-129: Update the paymentMethod setup in the test to create the
PaymentMethod mock without the nested card override, then assign the plain card
object directly to paymentMethod.card afterward. Preserve the existing card
brand, last4, exp_month, and exp_year values while avoiding recursive proxying
of nested properties.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5e886d2b-b272-4a30-b4a3-b9a828cf6f0a

📥 Commits

Reviewing files that changed from the base of the PR and between afb7de0 and df73ecc.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (28)
  • apps/api/drizzle/0034_cheerful_electro.sql
  • apps/api/drizzle/meta/0034_snapshot.json
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/billing/config/index.ts
  • apps/api/src/billing/config/top-up.config.ts
  • apps/api/src/billing/controllers/wallet-settings/wallet-settings.controller.spec.ts
  • apps/api/src/billing/http-schemas/wallet.schema.ts
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts
  • apps/api/src/billing/services/trial-validation/trial-validation.service.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/README.md
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check-instrumentation.service.spec.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check-instrumentation.service.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.integration.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts
  • apps/api/src/core/services/feature-flags/feature-flags.ts
  • apps/api/swagger/openapi.json
  • apps/api/test/functional/wallet-settings.spec.ts
  • apps/api/test/seeders/wallet-setting.seeder.ts
  • apps/deploy-web/src/components/billing-usage/AccountOverview/AccountOverview.spec.tsx
  • apps/deploy-web/src/components/billing-usage/AccountOverview/AccountOverview.tsx
  • apps/deploy-web/src/components/billing-usage/AutoTopUpSettingsPopup/AutoTopUpSettingsPopup.spec.tsx
  • apps/deploy-web/src/components/billing-usage/AutoTopUpSettingsPopup/AutoTopUpSettingsPopup.tsx
  • apps/deploy-web/src/queries/useWalletSettingsQueries.spec.tsx
  • apps/deploy-web/src/types/feature-flags.ts
  • packages/console-api-types/src/schema.d.ts

Comment thread apps/api/src/billing/http-schemas/wallet.schema.ts
Comment thread apps/deploy-web/src/components/billing-usage/AccountOverview/AccountOverview.tsx Outdated
@baktun14
baktun14 force-pushed the feat/billing-fixed-threshold-auto-reload branch from b0def8a to 53725b0 Compare July 28, 2026 23:12
@github-actions github-actions Bot added size: L and removed size: XL labels Jul 28, 2026
@baktun14 baktun14 changed the title feat(billing): add fixed-threshold auto top-up behind a feature flag feat(billing): add fixed-threshold auto top-up API behind a feature flag Jul 28, 2026
Comment thread apps/api/drizzle/0035_cheerful_electro.sql
@@ -112,11 +112,24 @@ export class WalletSettingService {
}

async #arrangeSchedule(prev?: WalletSettingOutput, next?: WalletSettingOutput) {

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.

question: I don't understand the change. when is wallet reload job triggered after this change?

before this it was triggered based on estimation of lifetime for deployments user has or in some particular moments. Does this PR changes this?

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.

@coderabbitai can u explain?

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.

@stalniy, this PR changes both the reload-check scheduling and, behind the feature flag, the reload decision.

WalletSettingService.#arrangeSchedule() now schedules an immediate reload-check job in these cases:

  1. autoReloadEnabled changes from disabled to enabled.
    It calls scheduleForWalletSetting(next, { withCleanup: true }).

  2. autoReloadEnabled stays enabled, and either autoReloadThreshold or autoReloadAmount changes.
    It calls scheduleForWalletSetting(next).

It does not schedule a job when auto reload is disabled or when enabled settings do not change.

The existing event-driven and periodic reload checks still schedule jobs. The change is what the handler does when a job runs:

  • With auto_reload_fixed_threshold disabled, it keeps the existing predicted-spend algorithm based on deployment cost/lifetime.
  • With auto_reload_fixed_threshold enabled, it does not calculate predicted deployment spend. It reloads when balance <= autoReloadThreshold, and charges autoReloadAmount (clamped to at least $20).
  • The fixed-threshold path can reload even when the user has no active deployments.

So, this service adds an immediate check after enabling or changing fixed-threshold settings. The feature flag changes the reload criterion from predicted deployment spend to the configured wallet balance threshold.

You are interacting with an AI system.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The check scheduling is unchanged: the event-driven and periodic reload checks still enqueue jobs the same way. What this PR adds is an immediate check when a user enables auto top-up or edits their threshold or amount. With the auto_reload_fixed_threshold flag off, the predicted-lifetime estimation still runs; with it on, the decision becomes a plain balance-versus-threshold comparison that charges the configured amount even when there are no active deployments. So the cadence stays the same and only the reload criterion changes behind the flag.

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Backend half of CON-717, split out from the combined PR. Adds the
auto_reload_threshold/auto_reload_amount columns, the wallet-settings input/output
schema split, the flag-gated reload-check branch, and regenerated OpenAPI +
shared types. Everything is gated behind the auto_reload_fixed_threshold flag
(off by default), so this is safe to merge ahead of the UI.

Refs CON-717
@baktun14
baktun14 force-pushed the feat/billing-fixed-threshold-auto-reload branch from 53725b0 to e8f0db4 Compare August 4, 2026 22:41
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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

🧹 Nitpick comments (2)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (2)

427-438: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the centralized minimum constant.

The test hardcodes 20 for both the input and the clamped result. The minimum top-up amount now lives in apps/api/src/billing/config/top-up.config.ts. Import STANDARD_TOP_UP_MIN_AMOUNT_USD and derive the values from it, so a change to the constant fails this test instead of silently passing.

🤖 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/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts`
around lines 427 - 438, Update the test around the setup call and
createPaymentIntent assertion to import and use STANDARD_TOP_UP_MIN_AMOUNT_USD
from the top-up configuration, deriving both autoReloadThreshold and the
expected payment amount from that constant instead of hardcoding 20.

520-521: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bind the flag mock to the flag name.

isEnabled returns the same value for every flag key. If the handler reads a second flag later, these tests keep passing for the wrong reason. Assert on the key instead.

♻️ Proposed change
     const featureFlagsService = mock<FeatureFlagsService>();
-    featureFlagsService.isEnabled.mockReturnValue(input?.fixedThresholdEnabled ?? false);
+    featureFlagsService.isEnabled.mockImplementation(flag => flag === FeatureFlagValue.auto_reload_fixed_threshold && (input?.fixedThresholdEnabled ?? false));

Adjust the flag identifier to match the enum exported from apps/api/src/core/services/feature-flags/feature-flags.ts.

🤖 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/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts`
around lines 520 - 521, Update the FeatureFlagsService mock setup around
isEnabled in the wallet balance reload check tests to return the configured
value only when called with the specific enum flag exported from
feature-flags.ts, rather than for every flag key; preserve the existing false
default.
🤖 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/api/src/billing/services/wallet-balance-reload-check/README.md`:
- Around line 21-25: Update the fenced pseudocode block in the README to specify
the text language identifier, changing the opening fence around the balance
auto-reload example to use text while preserving its contents.

---

Nitpick comments:
In
`@apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts`:
- Around line 427-438: Update the test around the setup call and
createPaymentIntent assertion to import and use STANDARD_TOP_UP_MIN_AMOUNT_USD
from the top-up configuration, deriving both autoReloadThreshold and the
expected payment amount from that constant instead of hardcoding 20.
- Around line 520-521: Update the FeatureFlagsService mock setup around
isEnabled in the wallet balance reload check tests to return the configured
value only when called with the specific enum flag exported from
feature-flags.ts, rather than for every flag key; preserve the existing false
default.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 47d4fb9d-c822-4cef-b727-ff1434bbc0a5

📥 Commits

Reviewing files that changed from the base of the PR and between c79291b and e8f0db4.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (22)
  • apps/api/drizzle/0035_cheerful_electro.sql
  • apps/api/drizzle/meta/0035_snapshot.json
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/billing/config/index.ts
  • apps/api/src/billing/config/top-up.config.ts
  • apps/api/src/billing/controllers/wallet-settings/wallet-settings.controller.spec.ts
  • apps/api/src/billing/http-schemas/wallet.schema.ts
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts
  • apps/api/src/billing/services/trial-validation/trial-validation.service.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/README.md
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check-instrumentation.service.spec.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check-instrumentation.service.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.integration.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts
  • apps/api/src/core/services/feature-flags/feature-flags.ts
  • apps/api/swagger/openapi.json
  • apps/api/test/functional/wallet-settings.spec.ts
  • apps/api/test/seeders/wallet-setting.seeder.ts
  • packages/console-api-types/src/schema.d.ts
🚧 Files skipped from review as they are similar to previous changes (17)
  • apps/api/src/billing/config/index.ts
  • apps/api/test/seeders/wallet-setting.seeder.ts
  • apps/api/src/billing/config/top-up.config.ts
  • apps/api/src/billing/services/trial-validation/trial-validation.service.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check-instrumentation.service.spec.ts
  • apps/api/test/functional/wallet-settings.spec.ts
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts
  • apps/api/src/core/services/feature-flags/feature-flags.ts
  • apps/api/swagger/openapi.json
  • apps/api/src/billing/controllers/wallet-settings/wallet-settings.controller.spec.ts
  • apps/api/src/billing/http-schemas/wallet.schema.ts
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts
  • packages/console-api-types/src/schema.d.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.integration.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check-instrumentation.service.ts

Comment thread apps/api/src/billing/services/wallet-balance-reload-check/README.md Outdated
…cents

Persist auto_reload_threshold/auto_reload_amount as integer minor units
instead of numeric(20,2) mapped to a JS float, matching the
stripe_transaction.amount convention and removing any risk of float
precision loss. The service converts dollars to cents on write and back
on read; the reload handler converts to USD before comparing against the
balance and charging. Also add the missing markdown code-fence language.

@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/api/src/billing/services/wallet-settings/wallet-settings.service.ts (1)

122-128: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Make wallet-reload cleanup transaction-bound.

enqueue uses the active transaction, but cancelCreatedBy uses a separate pgBoss connection. If upsertWalletSetting rolls back, the cancellation remains while the new job enqueue rolls back, leaving no reload job. Run cleanup on the same transaction connection or defer it until commit.

🤖 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/api/src/billing/services/wallet-settings/wallet-settings.service.ts`
around lines 122 - 128, Update the wallet reload scheduling flow around
`#hasReloadValuesChanged` and walletReloadJobService calls so cleanup uses the
active transaction connection, matching enqueue behavior. Ensure cancellation is
transaction-bound or deferred until commit, preventing cleanup from persisting
when upsertWalletSetting rolls back and preserving the existing scheduling
behavior.

Source: Path instructions

🤖 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/api/src/billing/services/wallet-settings/wallet-settings.service.ts`:
- Line 61: Update the private `#validate` method in upsertWalletSetting to enforce
the wallet-setting minimums for every provided value, independently of the
autoReloadEnabled branch, before either updateById or create persists settings.
Reuse the existing minimum-validation rules or symbols, and preserve the current
autoReloadEnabled-specific validation.

---

Outside diff comments:
In `@apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts`:
- Around line 122-128: Update the wallet reload scheduling flow around
`#hasReloadValuesChanged` and walletReloadJobService calls so cleanup uses the
active transaction connection, matching enqueue behavior. Ensure cancellation is
transaction-bound or deferred until commit, preventing cleanup from persisting
when upsertWalletSetting rolls back and preserving the existing scheduling
behavior.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b48c8aa0-be94-4c74-9f22-45d284aca5c6

📥 Commits

Reviewing files that changed from the base of the PR and between e8f0db4 and 9e38eee.

📒 Files selected for processing (12)
  • apps/api/drizzle/0035_cheerful_electro.sql
  • apps/api/drizzle/meta/0035_snapshot.json
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/billing/lib/currency/currency.spec.ts
  • apps/api/src/billing/lib/currency/currency.ts
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/README.md
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.integration.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts
  • apps/api/test/seeders/wallet-setting.seeder.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/api/drizzle/0035_cheerful_electro.sql
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/README.md
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts


await this.#validate({ next: settings, userId });
const next = await this.walletSettingRepository.accessibleBy(ability, "update").updateById(prev.id, settings, { returning: true });
const next = await this.walletSettingRepository.accessibleBy(ability, "update").updateById(prev.id, this.#toStoredSettings(settings), { returning: true });

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -a -t f 'CLAUDE.md' . -x sh -c 'printf "\n--- %s ---\n" "$1"; sed -n "1,240p" "$1"' sh || true

rg -n -C 12 \
  'upsertWalletSetting|autoReloadThreshold|autoReloadAmount' \
  apps/api --glob '*.ts' --glob '*.tsx'

Repository: akash-network/console

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- service ---'
sed -n '1,175p' apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts

printf '%s\n' '--- input schema and constants ---'
sed -n '1,115p' apps/api/src/billing/http-schemas/wallet.schema.ts
rg -n -C 3 'AUTO_RELOAD_THRESHOLD_MIN_USD|STANDARD_TOP_UP_MIN_AMOUNT_USD|AUTO_RELOAD_AMOUNT_MAX_USD' apps/api/src

printf '%s\n' '--- all upsert callers ---'
rg -n -C 4 'upsertWalletSetting' apps/api/src apps/api/test --glob '*.ts' --glob '*.tsx'

printf '%s\n' '--- repository write definitions ---'
rg -n -C 8 'class WalletSettingRepository|updateById|create\(' apps/api/src/billing/repositories apps/api/src/core/repositories --glob '*.ts'

Repository: akash-network/console

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- focused service methods ---'
sed -n '35,125p' apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts

printf '%s\n' '--- schema imports and bounds ---'
sed -n '1,90p' apps/api/src/billing/http-schemas/wallet.schema.ts
rg -n -C 5 'AUTO_RELOAD_THRESHOLD_MIN_USD|STANDARD_TOP_UP_MIN_AMOUNT_USD|AUTO_RELOAD_AMOUNT_MAX_USD' apps/api/src apps/api/test --glob '*.ts'

printf '%s\n' '--- compact caller list ---'
rg -l 'upsertWalletSetting' apps/api/src apps/api/test --glob '*.ts' --glob '*.tsx' |
  while read -r f; do
    printf '\n--- %s ---\n' "$f"
    rg -n -C 5 'upsertWalletSetting' "$f"
  done

Repository: akash-network/console

Length of output: 26665


Enforce wallet-setting minimums before persistence.

The current production callers use WalletSettingsInputSchema, but upsertWalletSetting accepts direct WalletSettingInput calls. Add minimum validation for provided values in #validate, outside the autoReloadEnabled branch, before updateById or create.

🤖 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/api/src/billing/services/wallet-settings/wallet-settings.service.ts` at
line 61, Update the private `#validate` method in upsertWalletSetting to enforce
the wallet-setting minimums for every provided value, independently of the
autoReloadEnabled branch, before either updateById or create persists settings.
Reuse the existing minimum-validation rules or symbols, and preserve the current
autoReloadEnabled-specific validation.

Source: Path instructions

@claude claude 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.

Beyond the inline stale-comment nit, I checked the two job-scheduling concerns raised for wallet-settings.service.ts and didn't find them to hold up: the immediate-check call on threshold/amount change omits withCleanup, but the WalletBalanceReloadCheck queue uses policy singleton, which blocks concurrent active execution rather than additional created/scheduled jobs, so it doesn't collide with the pending safety-net job. And the immediate-check dispatch is intentionally flag-agnostic — the actual charge decision is gated by auto_reload_fixed_threshold inside the handler when the job runs, not at schedule time.

Extended reasoning...

I read wallet-reload-job.service.ts and job-queue.service.ts to check the singleton-collision claim: the queue is created with policy: handler.policy ("singleton"), which per pg-boss semantics restricts concurrent active execution per singleton key, not the presence of an already-scheduled/created job, so scheduleForWalletSetting(next) without withCleanup on a value change should enqueue successfully alongside a pending daily safety-net job rather than returning null and throwing. For the flag-gating concern, WalletBalanceReloadCheckHandler#tryToReload branches on FeatureFlags.AUTO_RELOAD_FIXED_THRESHOLD when the job actually runs, so scheduling the immediate check independent of the flag is correct — the flag controls the charge algorithm, not whether a check is worth running. This is informational only, based on examining these two candidates; it isn't a full review of the PR's scheduling logic.

Comment on lines +6 to +9

/** Upper bounds keep oversized input from overflowing the numeric(20, 2) columns (a 500) or being charged verbatim to the card. */
const AUTO_RELOAD_THRESHOLD_MAX_USD = 10_000;
const AUTO_RELOAD_AMOUNT_MAX_USD = 10_000;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The JSDoc above AUTO_RELOAD_THRESHOLD_MAX_USD/AUTO_RELOAD_AMOUNT_MAX_USD says the $10,000 cap prevents overflowing a numeric(20, 2) column, but this same PR's integer-cents refactor (wallet-setting.schema.ts, migration 0035_cheerful_electro.sql) made autoReloadThreshold/autoReloadAmount integer columns, not numeric(20, 2). Integer comfortably holds far more than 1,000,000 cents ($10,000), so the overflow rationale is no longer accurate for these two fields — only the "charged verbatim to the card" half of the comment still holds. Suggest rewording to state the real justification (capping accidental huge charges).

Extended reasoning...

What's wrong: The comment at apps/api/src/billing/http-schemas/wallet.schema.ts:7 reads: Upper bounds keep oversized input from overflowing the numeric(20, 2) columns (a 500) or being charged verbatim to the card. This was accurate when it was written (commit b0def8a, added in response to a CodeRabbit review comment during this PR's review cycle, when auto_reload_threshold/auto_reload_amount were still numeric(20, 2) columns). But the PR's own later commit (9e38eee, "store auto top-up threshold and amount as integer cents") migrated wallet-setting.schema.ts to declare both columns as integer(...) (see migration 0035_cheerful_electro.sql: ADD COLUMN \"auto_reload_threshold\" integer), and the comment was never updated to match.\n\nWhy it's wrong now: With integer columns, the actual ceiling is PostgreSQL's int4 max of ~2.147 billion. The $10,000 cap in USD corresponds to 1,000,000 cents — nowhere close to that ceiling (roughly 2,147x smaller). So the "prevent overflowing the numeric(20, 2) columns" clause no longer describes a real risk for these two fields; there is nothing left to overflow at that value. The only clause that remains true is "being charged verbatim to the card," which is the actual reason a cap is still needed today: without it, a client-submitted value like $500,000 would pass validation and get charged directly to the user's payment method.\n\nWhy nothing else catches this: This is a pure documentation/comment-accuracy issue — TypeScript, tests, and runtime behavior are all unaffected, since the .max(10_000) validation itself is unchanged and still functions correctly regardless of what the comment says. Nothing in CI checks that a JSDoc comment's stated rationale matches the underlying schema.\n\nStep-by-step proof:\n1. Read apps/api/src/billing/http-schemas/wallet.schema.ts:6-9: the JSDoc justifies AUTO_RELOAD_THRESHOLD_MAX_USD/AUTO_RELOAD_AMOUNT_MAX_USD (both 10_000) by citing overflow of a numeric(20, 2) column.\n2. Read apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts: autoReloadThreshold and autoReloadAmount are declared as integer(...), not numeric(20, 2).\n3. Read apps/api/drizzle/0035_cheerful_electro.sql: confirms the migration adds both columns as integer.\n4. Convert the cap to the column's actual unit: $10,000 × 100 = 1,000,000 cents.\n5. Compare against integer's max value (~2,147,483,647): 1,000,000 is far below it, so no overflow is possible at the current cap — the stated risk doesn't exist for these columns anymore.\n6. Conclude: the "numeric(20, 2) overflow" half of the comment is stale; only the "charged verbatim to the card" half is still the operative justification.\n\nImpact and fix: Purely cosmetic — no behavioral or test impact, since the .max() validation logic is correct and unchanged. The fix is a one-line comment update, e.g.: Upper bound keeps oversized input from being charged verbatim to the card. This keeps the JSDoc's stated "why" accurate, per the repo's own clean-code-over-comments guidance that a why-comment should state the correct why.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants