Skip to content

feat(billing): adds balance check job handler#2274

Merged
ygrishajev merged 9 commits intomainfrom
feature/auto-reload-handler
Dec 3, 2025
Merged

feat(billing): adds balance check job handler#2274
ygrishajev merged 9 commits intomainfrom
feature/auto-reload-handler

Conversation

@ygrishajev
Copy link
Contributor

@ygrishajev ygrishajev commented Nov 25, 2025

refs #1779

Summary by CodeRabbit

  • New Features

    • Endpoints to mark/get a user’s default payment method; Stripe webhook route; Wallet reload scheduling service and immediate/singleton scheduling.
  • Improvements

    • Local+remote default-payment-method sync, idempotent payment intent support, memoized fiat balance view, richer auto-reload validation and scheduling, handlers now receive job metadata, transactional helpers and type utilities.
  • Bug Fixes / DB

    • Enforced not-null and new is_default column with unique partial index; removed obsolete wallet auto-reload fields.
  • Tests & Docs

    • Extensive unit tests for reload flows and README for wallet reload logic.

✏️ Tip: You can customize this high-level summary in your review settings.

@ygrishajev ygrishajev requested a review from a team as a code owner November 25, 2025 10:22
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 25, 2025

Walkthrough

Implements wallet auto-reload scheduling and handler, adds payment-method defaulting (DB, repo, service, controller, webhook), introduces paying-user utilities, fiat conversion and memoized balances, transactional/job-meta plumbing, schema migration snapshot, many repo/service API changes, tests, and docs.

Changes

Cohort / File(s) Change Summary
Database migration & snapshot
apps/api/drizzle/meta/0023_snapshot.json, apps/api/drizzle/meta/_journal.json, apps/api/drizzle/0023_clumsy_vertigo.sql
New migration/snapshot: make user_wallets.user_id NOT NULL; add payment_methods.is_default NOT NULL default false with partial unique index; drop wallet_settings.auto_reload_threshold and auto_reload_amount; append journal entry.
Payment-method HTTP, DB & seeders
apps/api/src/billing/http-schemas/stripe.schema.ts, apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts, apps/api/test/seeders/database-payment-method.seeder.ts, apps/api/test/seeders/payment-method.seeder.ts
Add mark/get-default schemas/types; add isDefault column and partial unique index; update seeders and add merged payment-method seeder with validated flag.
Payment-method repo & Stripe surface
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts, apps/api/src/billing/services/stripe/stripe.service.ts, apps/api/src/billing/controllers/stripe/stripe.controller.ts, apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts, apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
Export repo types; add repo methods (countByUserId, findDefaultByUserId, markAsDefault, createAsDefault, private unmark helper); Stripe service adds local PaymentMethod type, defaulting/has/remote sync methods and idempotencyKey support; controller/router add mark/get-default endpoints; webhook marks first-attached method as default.
Wallet reload job & handler
apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.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.handler.spec.ts, apps/api/src/billing/services/wallet-balance-reload-check/README.md
Add WalletReloadJobService to schedule/cancel singleton reload jobs and persist autoReloadJobId; implement WalletBalanceReloadCheckHandler with validation, balance checks, Stripe PaymentIntent creation (idempotency), next-check scheduling; add comprehensive tests and README.
Wallet settings service & repo
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts, apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts, apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts, apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
Refactor WalletSettingService into private flows (#create/#update/#validate/#arrangeSchedule), integrate StripeService and WalletReloadJobService for validation/scheduling, replace duplicate-error handling with isUniqueViolation retry, remove numeric auto-reload fields, add WalletSettingRelations; repository returns relations and updated findByUserId signature.
Balances & fiat conversion
apps/api/src/billing/services/balances/balances.service.ts, apps/api/src/dashboard/services/stats/stats.service.ts, apps/api/test/seeders/balance.seeder.ts, apps/api/src/billing/controllers/wallet/wallet.controller.ts
Add getFullBalanceInFiat and memoized helpers in BalancesService (inject StatsService); StatsService adds convertToFiatAmount; add generateBalance test seeder; controller switched to memoized balance retrieval.
Paying-user & auth
apps/api/src/billing/services/paying-user/paying-user.ts, apps/api/src/auth/services/auth.service.ts, apps/api/src/auth/services/ability/ability.service.ts
Add PayingUser type plus isPayingUser/assertIsPayingUser; AuthService gains getCurrentPayingUser overloads (strict/non-strict); add ability rule allowing manage on PaymentMethod scoped to user.
User wallets & repo
apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts, apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts
Enforce user_id NOT NULL on UserWallets schema; repository input types adjusted (DbCreateUserWalletInput / DbUserWalletInput) and create signature tightened.
Managed signer & reload trigger
apps/api/src/billing/services/managed-signer/managed-signer.service.ts, apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts
Inject WalletReloadJobService; after executing derived txs detect spending messages (MsgCreateDeployment, MsgAccountDeposit) and call walletReloadJobService.scheduleImmediate(userId); tests updated.
Core transactions & job infra
apps/api/src/core/services/tx/tx.service.ts, apps/api/src/core/repositories/base.repository.ts, apps/api/src/core/services/job-queue/job-queue.service.ts, apps/api/src/core/services/job-queue/job-queue.service.spec.ts, apps/api/src/core/types/require.type.ts
Add ApiTransaction type and transaction short-circuit in TxService; BaseRepository adds protected ensureTransaction helper; JobQueue returns jobId, introduces JobMeta type and passes job meta to handlers, adds complete method; add generic Require<T,K> type.
Draining deployments & repo
apps/api/src/deployment/services/draining-deployment/draining-deployment.service.ts, apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts, apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts
Add calculateAllDeploymentCostUntilDate to compute predicted cost until a target date; tests refactored with setup helpers; paginateAutoTopUpDeployments accepts optional address filter and conditionally joins UserWallets.
Misc / infra / config / tests
apps/api/src/billing/services/refill/refill.service.ts, packages/http-sdk/src/stripe/stripe.service.ts, apps/api/drizzle.config.ts, apps/api/src/billing/routes/stripe-webhook/stripe-webhook.router.ts, various spec updates
Tidy imports, remove comment headers, add POSTGRES_DB_URI runtime guard in drizzle.config, add Stripe webhook route validating stripe-signature; multiple test/spec adjustments to support new services and job flows.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant WalletSettings as WalletSettingService
    participant WalletJob as WalletReloadJobService
    participant JobQueue as JobQueueService
    participant Worker as WalletBalanceReloadCheckHandler
    participant Balances as BalancesService
    participant StripeSvc as StripeService
    participant StripeAPI as Stripe API

    WalletSettings->>WalletJob: scheduleForWalletSetting(walletSetting)
    WalletJob->>JobQueue: enqueue(WalletBalanceReloadCheck, singletonKey, startAfter)
    JobQueue-->>WalletJob: jobId
    WalletJob->>WalletSettings: persist autoReloadJobId(jobId)
    JobQueue->>Worker: execute(job.payload, { id: jobId })
    Worker->>WalletSettings: fetch walletSetting & user
    Worker->>Balances: getFullBalanceInFiat(address, isOldWallet?)
    Worker->>StripeSvc: getDefaultPaymentMethod(user, ability)
    alt balance < threshold
      Worker->>StripeSvc: createPaymentIntent(..., idempotencyKey)
      StripeSvc->>StripeAPI: paymentIntents.create(...)
      StripeAPI-->>StripeSvc: PaymentIntent
      Worker->>WalletJob: scheduleForWalletSetting(next startAfter)
    else balance >= threshold
      Worker->>WalletJob: scheduleForWalletSetting(next startAfter)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus areas:
    • WalletBalanceReloadCheckHandler orchestration, amount calculations, idempotency, and tests.
    • WalletReloadJobService transactional behavior and persistence of autoReloadJobId.
    • Payment-method partial unique index and mark-as-default flow across repo/service/webhook/controller.
    • TxService short-circuit and BaseRepository.ensureTransaction interactions.
    • DB migration SQL and potential data/constraint impacts.

Possibly related PRs

Suggested reviewers

  • baktun14
  • stalniy

Poem

🐇 I hopped through ledgers, marked a default true,
I nudged the job queue when small balances grew.
I counted coins to dollars with a tiny twitch,
I scheduled checks and logged each careful switch.
A rabbit's hop — wallets hum, refreshed and new.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(billing): adds balance check job handler' directly and clearly describes the main change - introducing a new balance check job handler in the billing module.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/auto-reload-handler

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

@ygrishajev ygrishajev changed the base branch from feature/wallet-setting to main November 25, 2025 10:31
@ygrishajev ygrishajev changed the base branch from main to feature/wallet-setting November 25, 2025 11:02
Base automatically changed from feature/wallet-setting to main November 26, 2025 18:39
@ygrishajev ygrishajev force-pushed the feature/auto-reload-handler branch from d8f2557 to 3852a68 Compare November 27, 2025 16:47
@codecov
Copy link

codecov bot commented Nov 27, 2025

Codecov Report

❌ Patch coverage is 58.72093% with 142 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.71%. Comparing base (21553a8) to head (bc659a9).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
.../api/src/billing/services/stripe/stripe.service.ts 27.50% 28 Missing and 1 partial ⚠️
...tories/payment-method/payment-method.repository.ts 15.00% 16 Missing and 1 partial ⚠️
...ces/wallet-reload-job/wallet-reload-job.service.ts 43.33% 15 Missing and 2 partials ⚠️
...rc/billing/controllers/stripe/stripe.controller.ts 15.78% 15 Missing and 1 partial ⚠️
.../src/billing/services/balances/balances.service.ts 31.57% 13 Missing ⚠️
apps/api/src/auth/services/auth.service.ts 10.00% 8 Missing and 1 partial ⚠️
...eployment-setting/deployment-setting.repository.ts 10.00% 9 Missing ⚠️
...ries/wallet-settings/wallet-settings.repository.ts 16.66% 4 Missing and 1 partial ⚠️
.../services/stripe-webhook/stripe-webhook.service.ts 16.66% 5 Missing ⚠️
apps/api/src/core/repositories/base.repository.ts 0.00% 4 Missing and 1 partial ⚠️
... and 9 more

❌ Your patch status has failed because the patch coverage (58.72%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2274      +/-   ##
==========================================
+ Coverage   47.61%   47.71%   +0.10%     
==========================================
  Files        1036     1038       +2     
  Lines       29350    29600     +250     
  Branches     7630     7651      +21     
==========================================
+ Hits        13976    14125     +149     
- Misses      14877    15060     +183     
+ Partials      497      415      -82     
Flag Coverage Δ *Carryforward flag
api 81.17% <58.72%> (-0.68%) ⬇️
deploy-web 26.44% <ø> (+0.01%) ⬆️
log-collector 75.35% <ø> (ø)
notifications 87.94% <ø> (ø)
provider-console 81.48% <ø> (ø) Carriedforward from de560e9
provider-proxy 84.35% <ø> (ø) Carriedforward from de560e9

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

Files with missing lines Coverage Δ
...s/api/src/auth/services/ability/ability.service.ts 96.15% <ø> (ø)
...rc/billing/controllers/wallet/wallet.controller.ts 98.38% <100.00%> (ø)
apps/api/src/billing/http-schemas/stripe.schema.ts 100.00% <100.00%> (ø)
apps/api/src/billing/http-schemas/wallet.schema.ts 60.00% <ø> (-1.91%) ⬇️
...el-schemas/payment-method/payment-method.schema.ts 85.71% <ø> (ø)
...ng/model-schemas/user-wallet/user-wallet.schema.ts 83.33% <ø> (ø)
...el-schemas/wallet-setting/wallet-setting.schema.ts 77.77% <100.00%> (+2.77%) ⬆️
...repositories/user-wallet/user-wallet.repository.ts 85.96% <100.00%> (ø)
...ing/routes/stripe-webhook/stripe-webhook.router.ts 92.85% <ø> (ø)
.../services/managed-signer/managed-signer.service.ts 97.59% <100.00%> (+0.29%) ⬆️
... and 20 more

... and 60 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.

@ygrishajev ygrishajev force-pushed the feature/auto-reload-handler branch 2 times, most recently from a34a233 to b31182e Compare November 27, 2025 16:51
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (10)
apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts (1)

19-33: Consider adding an index for (userId, isDefault) lookups

The new isDefault flag is likely to be used in queries like “get default payment method for user X”. To keep those queries efficient as the table grows, consider adding a composite index on (userId, isDefault) alongside the existing userIdIsValidatedIdx.

   table => ({
@@
-    userIdIsValidatedIdx: index("payment_methods_user_id_is_validated_idx").on(table.userId, table.isValidated),
+    userIdIsValidatedIdx: index("payment_methods_user_id_is_validated_idx").on(table.userId, table.isValidated),
+    userIdIsDefaultIdx: index("payment_methods_user_id_is_default_idx").on(table.userId, table.isDefault),
apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (1)

44-69: Route wiring is correct; consider a no-content response

The new POST /v1/stripe/payment-methods/default route is wired correctly (validation, security, controller call). Since this is a side-effect-only operation with no response body, you might consider using 204 No Content (or returning {}) instead of c.json(undefined, 200), but this is purely a semantics/UX nit.

-stripePaymentMethodsRouter.openapi(markAsDefaultRoute, async function markAsDefault(c) {
-  await container.resolve(StripeController).markAsDefault(c.req.valid("json"));
-  return c.json(undefined, 200);
-});
+stripePaymentMethodsRouter.openapi(markAsDefaultRoute, async function markAsDefault(c) {
+  await container.resolve(StripeController).markAsDefault(c.req.valid("json"));
+  return c.body(null, 204);
+});
apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (1)

144-157: Make “first default payment method” selection atomic to avoid races

The countByUserIdisDefault = count === 0 logic works in the single-event case, but under concurrent payment_method.attached events for the same user, both transactions could see count === 0 and insert records with isDefault = true. That can leave multiple local defaults, even if Stripe ultimately has only one.

To harden this:

  • Prefer a repository-level atomic operation (e.g., createAsDefault(userId, fingerprint, paymentMethodId)) that:
    • Inserts the first method as default if none exist, and/or
    • Ensures only one default per user (e.g., via a transaction that clears previous defaults or a partial unique index).
  • Keep the Stripe markRemotePaymentMethodAsDefault call coupled to that atomic path so local and remote defaults stay in sync.

The current behavior is acceptable for most cases but may produce subtle inconsistencies under rare concurrent attach scenarios.

Also applies to: 159-165

apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)

48-69: Consider reusing toOutput to avoid duplication.

The parseFloat conversion logic on lines 66-67 duplicates the same logic in toOutput (lines 75-76). Consider extracting or reusing toOutput for consistency and maintainability.

Also, the method lacks an explicit return type annotation. Consider adding one for better type safety:

-  async findInternalByUserIdWithRelations(userId: WalletSettingOutput["userId"]) {
+  async findInternalByUserIdWithRelations(userId: WalletSettingOutput["userId"]): Promise<{
+    wallet: { address: string | null; isOldWallet: boolean | null };
+    user: /* UserOutput type */;
+    autoReloadThreshold: number | undefined;
+    autoReloadAmount: number | undefined;
+    // ... other fields
+  } | undefined> {
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)

3-3: Consider using the standard uuid package instead of unleash-client.

Importing uuidv4 from unleash-client/lib/uuidv4 is unconventional. The codebase already uses uuid (as seen in wallet-settings.service.ts with import { v4 as uuidv4 } from "uuid"). Using a consistent UUID source improves maintainability.

-import { uuidv4 } from "unleash-client/lib/uuidv4";
+import { v4 as uuidv4 } from "uuid";
apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (1)

20-21: Consider using dependency injection instead of jest.mock("uuid").

Per coding guidelines, jest.mock() should be avoided in favor of jest-mock-extended and dependency injection. The uuidv4 function could be injected as a dependency into WalletSettingService to enable cleaner testing without module mocking.

However, since this is pre-existing code and refactoring the service may be out of scope for this PR, this is a nice-to-have improvement for future consideration.

apps/api/src/billing/services/stripe/stripe.service.ts (2)

32-33: Potential type naming conflict with existing PaymentMethod types.

This exported PaymentMethod type may conflict with PaymentMethod defined in apps/api/src/billing/http-schemas/stripe.schema.ts (line 236) and packages/http-sdk/src/stripe/stripe.types.ts (lines 29-40). Consider using a more specific name like StripePaymentMethodWithValidation or ValidatedPaymentMethod to avoid ambiguity when both types are imported in the same file.

-export type PaymentMethod = Stripe.PaymentMethod & { validated: boolean };
+export type ValidatedPaymentMethod = Stripe.PaymentMethod & { validated: boolean };

189-210: Consider precision-safe currency conversion.

Math.round(params.amount * 100) may produce unexpected results due to floating-point arithmetic (e.g., 19.99 * 100 yields 1998.9999999999998). While Math.round mitigates this in most cases, consider using a more robust approach for financial calculations.

-const amountInCents = Math.round(params.amount * 100);
+const amountInCents = Math.round((params.amount * 100 + Number.EPSILON) * 100) / 100;

Alternatively, require the caller to pass amountInCents directly to avoid conversion ambiguity.

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

20-22: Duplicate utility type definition.

The Require type utility is already defined in apps/api/src/billing/services/paying-user/paying-user.ts at line 5. Consider importing it from there to avoid duplication and maintain consistency.

-type Require<T, K extends keyof T> = Omit<T, K> & {
-  [P in K]-?: NonNullable<T[P]>;
-};
+import { Require } from "../paying-user/paying-user";

Note: You may need to export Require from paying-user.ts if it's not already exported.


226-234: Placeholder implementation for next check date calculation.

The method currently returns a fixed 1-day delay with a note indicating "Implementation pending...". This is acceptable for initial development but should be tracked for completion before production use, as the scheduling logic likely needs to be based on wallet activity or balance burn rate.

Would you like me to open an issue to track the implementation of proper next-check-date calculation logic?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6512106 and b31182e.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (33)
  • apps/api/drizzle/0023_sad_adam_warlock.sql (1 hunks)
  • apps/api/drizzle/0024_thankful_stick.sql (1 hunks)
  • apps/api/drizzle/meta/0023_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/0024_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/_journal.json (1 hunks)
  • apps/api/src/auth/services/auth.service.ts (2 hunks)
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts (3 hunks)
  • apps/api/src/billing/http-schemas/stripe.schema.ts (2 hunks)
  • apps/api/src/billing/http-schemas/wallet.schema.ts (0 hunks)
  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts (1 hunks)
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1 hunks)
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (2 hunks)
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2 hunks)
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (2 hunks)
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1 hunks)
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (5 hunks)
  • apps/api/src/billing/services/balances/balances.service.ts (3 hunks)
  • apps/api/src/billing/services/paying-user/paying-user.ts (1 hunks)
  • apps/api/src/billing/services/refill/refill.service.ts (1 hunks)
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (2 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (6 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2 hunks)
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts (2 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (4 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (5 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts (9 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.ts (3 hunks)
  • apps/api/src/core/types/require.type.ts (1 hunks)
  • apps/api/test/seeders/balance.seeder.ts (2 hunks)
  • apps/api/test/seeders/database-payment-method.seeder.ts (1 hunks)
  • apps/api/test/seeders/payment-method.seeder.ts (2 hunks)
  • packages/http-sdk/src/stripe/stripe.service.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • packages/http-sdk/src/stripe/stripe.service.ts
  • apps/api/src/billing/http-schemas/wallet.schema.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
  • apps/api/test/seeders/database-payment-method.seeder.ts
  • apps/api/test/seeders/balance.seeder.ts
  • apps/api/src/billing/services/refill/refill.service.ts
  • apps/api/src/core/types/require.type.ts
  • apps/api/src/billing/services/paying-user/paying-user.ts
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts
  • apps/api/test/seeders/payment-method.seeder.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts
  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/auth/services/auth.service.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/billing/services/balances/balances.service.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
  • apps/api/src/billing/http-schemas/stripe.schema.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test

Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

**/*.spec.{ts,tsx}: Use <Subject>.name in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references
Use either a method name or a condition starting with 'when' for nested suite descriptions in tests
Use present simple, 3rd person singular for test descriptions without prepending 'should'

Files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
🧠 Learnings (9)
📚 Learning: 2025-07-30T10:18:24.469Z
Learnt from: baktun14
Repo: akash-network/console PR: 1750
File: apps/api/drizzle/meta/0017_snapshot.json:17-49
Timestamp: 2025-07-30T10:18:24.469Z
Learning: In the Akash Network console billing system, the `user_wallets.user_id` column can be NULL to support anonymous user wallets, allowing users to interact with the system before authentication. Multiple anonymous wallets with NULL user_id values are permitted by design.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
  • apps/api/drizzle/0023_sad_adam_warlock.sql
📚 Learning: 2025-09-29T02:40:22.090Z
Learnt from: stalniy
Repo: akash-network/console PR: 1951
File: apps/api/src/user/services/user/user.service.ts:48-63
Timestamp: 2025-09-29T02:40:22.090Z
Learning: In the Akash Network Console codebase, the users table was originally created by Sequelize and later migrated to Drizzle. Due to this migration history, the unique constraint on the userId field is named "userSetting_userId_unique" rather than following the typical "users_user_id_unique" pattern.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
📚 Learning: 2025-09-25T14:31:44.914Z
Learnt from: baktun14
Repo: akash-network/console PR: 1969
File: apps/deploy-web/src/pages/payment.tsx:179-191
Timestamp: 2025-09-25T14:31:44.914Z
Learning: The payment confirmation endpoint in apps/api/src/billing/http-schemas/stripe.schema.ts uses zod schema validation with `amount: z.number().gte(20, "Amount must be greater or equal to $20")` to ensure all payment requests meet the minimum amount requirement, preventing zero-amount or invalid payments from reaching Stripe.

Applied to files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/billing/http-schemas/stripe.schema.ts
📚 Learning: 2025-10-15T16:39:55.348Z
Learnt from: jzsfkzm
Repo: akash-network/console PR: 2039
File: apps/deploy-web/tests/ui/change-wallets.spec.ts:4-10
Timestamp: 2025-10-15T16:39:55.348Z
Learning: In the Akash Console E2E tests using the context-with-extension fixture, the first wallet is automatically created during fixture setup via `importWalletToLeap` in `apps/deploy-web/tests/ui/fixture/wallet-setup.ts`, so tests that call `frontPage.createWallet()` are creating a second wallet to test wallet switching functionality.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:52.965Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/setup-instead-of-before-each.mdc:0-0
Timestamp: 2025-11-25T17:45:52.965Z
Learning: Applies to **/*.spec.{ts,tsx} : Use `setup` function instead of `beforeEach` in test files. The `setup` function must be at the bottom of the root `describe` block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: The apps/api codebase consistently uses .toISOString() for startAfter values when scheduling jobs with JobQueueService to ensure UTC timezone consistency. This pattern is used throughout trial-started.handler.ts, trial-deployment-lease-created.handler.ts and other job scheduling code. Do not suggest changing to Date objects as ISO strings provide explicit UTC representation for database storage.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: In apps/api job scheduling with JobQueueService, use .toISOString() for startAfter values instead of Date objects to ensure UTC timezone consistency when dates are persisted to the database. Date objects can be ambiguous about timezone during serialization, while ISO strings with .toISOString() explicitly represent UTC.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-11-25T17:45:44.790Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/no-jest-mock.mdc:0-0
Timestamp: 2025-11-25T17:45:44.790Z
Learning: Applies to **/*.spec.{ts,tsx} : Don't use `jest.mock()` in test files. Instead, use `jest-mock-extended` to create mocks and pass mocks as dependencies to the service under test

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
🧬 Code graph analysis (13)
apps/api/test/seeders/database-payment-method.seeder.ts (1)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)
  • PaymentMethodOutput (11-11)
apps/api/test/seeders/balance.seeder.ts (2)
apps/api/src/billing/http-schemas/balance.schema.ts (1)
  • GetBalancesResponseOutput (17-17)
apps/deploy-web/src/utils/apiUtils.ts (1)
  • balance (44-46)
apps/api/src/billing/services/paying-user/paying-user.ts (2)
apps/api/src/core/types/require.type.ts (1)
  • Require (1-3)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • user (144-169)
apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (2)
apps/api/src/user/model-schemas/user/user.schema.ts (1)
  • Users (8-28)
apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1)
  • UserWallets (5-18)
apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (1)
apps/api/src/core/providers/postgres.provider.ts (1)
  • ApiPgTables (80-80)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (3)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (2)
  • WalletSettingInput (12-17)
  • WalletSettingOutput (19-22)
apps/api/src/core/repositories/base.repository.ts (1)
  • isUniqueViolation (227-229)
apps/api/src/billing/events/wallet-balance-reload-check.ts (1)
  • WalletBalanceReloadCheck (5-15)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (3)
  • userId (59-76)
  • userId (78-106)
  • walletSetting (144-160)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (2)
apps/api/src/auth/services/auth.service.ts (5)
  • Protected (62-78)
  • currentUser (13-15)
  • currentUser (17-24)
  • ability (44-46)
  • ability (48-50)
apps/api/src/billing/http-schemas/stripe.schema.ts (2)
  • PaymentMethodMarkAsDefaultInput (236-236)
  • PaymentMethodsResponse (238-238)
apps/api/src/auth/services/auth.service.ts (1)
apps/api/src/billing/services/paying-user/paying-user.ts (3)
  • PayingUser (6-6)
  • assertIsPayingUser (8-10)
  • isPayingUser (12-14)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (4)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (1)
  • walletSetting (144-160)
apps/api/test/seeders/payment-method.seeder.ts (1)
  • generatePaymentMethod (11-56)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (237-237)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (2)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • user (144-169)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • assertIsPayingUser (8-10)
apps/api/src/billing/services/stripe/stripe.service.ts (5)
apps/api/src/billing/http-schemas/stripe.schema.ts (2)
  • PaymentMethod (237-237)
  • PaymentIntentResult (240-240)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • PaymentMethod (30-41)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • PayingUser (6-6)
apps/api/src/user/repositories/user/user.repository.ts (1)
  • UserOutput (10-12)
apps/api/src/core/services/tx/tx.service.ts (1)
  • WithTransaction (33-45)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)
apps/api/src/core/providers/postgres.provider.ts (1)
  • ApiPgTables (80-80)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
🔇 Additional comments (50)
apps/api/drizzle/0024_thankful_stick.sql (1)

1-1: LGTM!

The migration safely adds the is_default column with a sensible default value (false) and NOT NULL constraint, ensuring existing rows are handled correctly.

apps/api/drizzle/meta/_journal.json (1)

166-179: LGTM!

Migration journal entries are correctly formatted and follow the existing pattern. The sequential indices and tags align with the new migration files.

apps/api/src/core/types/require.type.ts (1)

1-3: LGTM!

The Require utility type is correctly implemented. It makes the specified properties both required and non-nullable while preserving the rest of the type.

apps/api/drizzle/meta/0024_snapshot.json (1)

1-798: Schema snapshot acknowledged.

This is a generated snapshot file documenting the database schema state after migration 0024. The snapshot correctly reflects the addition of the is_default column to payment_methods and the NOT NULL constraint on user_wallets.user_id.

apps/api/drizzle/meta/0023_snapshot.json (1)

1-791: Schema snapshot acknowledged.

This is a generated snapshot file documenting the database schema state after migration 0023, reflecting the NOT NULL constraint on user_wallets.user_id.

apps/api/src/billing/services/refill/refill.service.ts (1)

10-11: LGTM!

The import path changes are a standard refactoring from barrel exports to explicit module paths. No logic or behavior changes.

apps/api/src/core/services/job-queue/job-queue.service.ts (1)

208-215: JobMeta and handler signature extension look good

Exposing only job.id to handlers via JobMeta keeps handlers decoupled from the queue implementation while still enabling better logging and tracing. The updated handle(payload, job) signature is clear and focused.

apps/api/test/seeders/balance.seeder.ts (1)

21-34: generateBalance helper is well-typed and consistent

The generateBalance utility cleanly models GetBalancesResponseOutput["data"], computes total from balance + deployments with proper rounding, and still allows overriding any field. This is a solid, reusable test helper.

apps/api/test/seeders/payment-method.seeder.ts (1)

58-61: generateMergedPaymentMethod correctly composes Stripe data with validated flag

The helper cleanly layers an optional validated flag on top of a generated Stripe.PaymentMethod, returning the project-level PaymentMethod type. The destructuring of validated from overrides and the !!validated coercion both look good.

apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts (1)

53-63: initialize(userId: string) implementation is consistent with existing wallet flows

The simplified initialize(userId: string) method correctly creates a wallet record, provisions a wallet via walletManager, and persists the resulting address. The sequence mirrors the trial-initialization flow and looks coherent.

apps/api/src/billing/services/paying-user/paying-user.ts (1)

1-14: PayingUser type guard and assertion are well-designed

The PayingUser alias plus isPayingUser/assertIsPayingUser pair provide a clear, type-safe way to gate payment-specific flows. Using Require<..., "stripeCustomerId"> and a 402 status for “payments not set up” is a reasonable and consistent approach.

apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)

38-46: LGTM!

The explicit undefined guard and toOutput conversion are correctly implemented.

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

39-48: LGTM!

The Drizzle ORM relations are correctly defined with proper field references and will enable efficient joined loading for the wallet balance reload check handler.

apps/api/src/billing/http-schemas/stripe.schema.ts (2)

10-14: LGTM!

The schema follows the established pattern with nested data object structure, consistent with other request schemas in this file.


236-236: LGTM!

Type export correctly infers from the schema.

apps/api/test/seeders/database-payment-method.seeder.ts (1)

3-15: LGTM!

Good refactor to use PaymentMethodOutput from the repository as the single source of truth, and the addition of isDefault: false aligns with the new schema column.

apps/api/src/auth/services/auth.service.ts (1)

26-42: LGTM!

Well-designed method overloads providing type-safe access to paying users with flexible strictness options. The implementation correctly:

  • Always validates user presence (401)
  • In strict mode, asserts paying user status via assertIsPayingUser (402)
  • In non-strict mode, returns undefined for non-paying users
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (4)

19-20: LGTM!

Good use of WalletBalanceReloadCheckHandler.name for the describe suite and "handle" for the nested describe, following the coding guidelines.


256-305: LGTM!

The setup function correctly follows the coding guidelines:

  • Placed at the bottom of the root describe block
  • Creates and returns the object under test with all dependencies
  • Uses jest-mock-extended for mocks
  • Accepts optional overrides via inline type (implicitly via spread)
  • Avoids shared state

21-62: LGTM!

Comprehensive test coverage for the reload trigger scenario with proper assertions on the Stripe payment intent parameters including the idempotency key.


145-253: LGTM!

Excellent coverage of all validation error paths with proper event logging assertions.

apps/api/src/billing/controllers/stripe/stripe.controller.ts (2)

45-51: LGTM!

The new markAsDefault method correctly:

  • Uses the update action on StripePayment subject for authorization
  • Retrieves the paying user (strict mode ensures 402 if not configured)
  • Delegates to the service layer with proper ability context

54-63: LGTM!

Good defensive pattern using getCurrentPayingUser({ strict: false }) to gracefully handle non-paying users by returning an empty array instead of throwing.

apps/api/src/billing/services/balances/balances.service.ts (1)

112-130: I'll help you verify this review comment. Let me start by checking the DEPLOYMENT_GRANT_DENOM usage and understanding the coin mapping logic.
<function_calls>


#!/bin/bash

Check for DEPLOYMENT_GRANT_DENOM usage and possible values

rg -n "DEPLOYMENT_GRANT_DENOM" --type ts -C 3

</function_calls>

Now let me also check the structure of the balances service to understand the context better:
<function_calls>

#!/bin/bash

Get the file and check its overall structure

wc -l apps/api/src/billing/services/balances/balances.service.ts

</function_calls>

<function_calls>

#!/bin/bash

Read the balances service file to understand the context

head -150 apps/api/src/billing/services/balances/balances.service.ts | tail -60

</function_calls>

apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (2)

9-10: LGTM!

The new type definitions properly separate the full insert input type (DbCreateUserWalletInput) from the partial update input type (DbUserWalletInput). This improves type safety and clarifies the intended usage of each type.


72-83: LGTM!

The updated create method signature using Pick<DbCreateUserWalletInput, "userId" | "address"> enforces that only the required insert fields are accepted, aligning with the new type structure and improving API clarity.

apps/api/src/core/services/job-queue/job-queue.service.spec.ts (6)

22-25: LGTM!

Queue configuration expectations updated to match new defaults: 5-minute retry delay max and explicit policy field.


67-76: LGTM!

Good test coverage for the new validation that prevents multiple handlers from registering for the same queue. The test correctly verifies the error message.


95-100: LGTM!

Log expectation updated to include jobId in the JOB_ENQUEUED event, improving observability.


151-156: LGTM!

Mock implementation updated to use a single job object pattern with id field, matching the updated service behavior.


174-179: LGTM!

Handler invocation now correctly passes the job context { id: job.id } as a second argument, enabling handlers to access job metadata.


284-313: LGTM!

The setup function is correctly placed at the bottom of the root describe block, creates the service under test, avoids shared state, and returns all necessary mocks. The runWithContext mock type is properly cast to ExecutionContextService["runWithContext"]. As per coding guidelines, this follows the recommended test setup pattern.

apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2)

10-11: LGTM!

Exporting PaymentMethodInput and PaymentMethodOutput types enables type-safe usage across the codebase.


70-77: LGTM!

The countByUserId method correctly uses count() with whereAccessibleBy for proper access control and handles the null case with a fallback to 0.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (7)

8-11: LGTM!

New dependencies (StripeService, UserRepository, isUniqueViolation) are properly imported to support the enhanced validation and error handling logic.


13-17: LGTM!

The WalletSettingInput interface clearly defines the expected input shape for wallet settings, improving API contract clarity.


45-57: LGTM!

The refactored upsertWalletSetting cleanly delegates to private methods (#update, #create, #arrangeSchedule), improving readability and separation of concerns.


59-76: LGTM!

The #update method correctly handles the case where no previous setting exists by returning early, and properly validates before updating.


78-106: LGTM!

The #create method properly validates input, handles the race condition on unique constraint violation by retrying via #update, and uses the isUniqueViolation helper for clean error detection.


108-131: LGTM!

Comprehensive validation ensures that auto-reload can only be enabled when: (1) threshold and amount are set, (2) user exists, (3) Stripe customer is set up, and (4) a default payment method exists. This prevents configuration errors.


134-142: LGTM!

The #arrangeSchedule method correctly handles state transitions: schedules a job when auto-reload is newly enabled, and cancels the existing job when disabled.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (1)

279-322: LGTM!

The setup function correctly:

  • Creates userWithStripe with stripeCustomerId for Stripe integration tests
  • Mocks UserRepository.findById to return the user with Stripe ID
  • Mocks StripeService.getDefaultPaymentMethod with a generated payment method
  • Instantiates WalletSettingService with all new dependencies
  • Returns all necessary test fixtures

The function is properly placed at the bottom of the root describe block as per coding guidelines.

apps/api/src/billing/services/stripe/stripe.service.ts (4)

122-138: LGTM!

The method correctly fetches both Stripe customer data and local payment method in parallel, with proper assertions for each failure case. The merged return type preserves the validated status from the local database.


140-145: LGTM!

Simple and correct ownership verification that handles both string and object forms of the customer reference.


174-178: LGTM!

Simple and focused method for updating the Stripe customer's default payment method.


388-390: LGTM!

Clean simplification of the method.

apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (4)

55-64: LGTM!

Clean orchestration of the handler flow with proper Result-based error handling. The separation of concerns between resource collection, reload attempt, and scheduling is well-structured.


86-142: LGTM!

Comprehensive validation with clear error messages for each failure case. The use of type narrowing through conditional checks properly ensures the returned Resources type is fully initialized.


178-213: LGTM!

The reload logic correctly triggers when balance drops to or below the threshold. Good use of the job ID for idempotency to prevent duplicate charges, and proper error logging before re-throwing.


144-156: Verify intentional bypass of user permissions.

The ability is hardcoded with createMongoAbility([{ action: "read", subject: "PaymentMethod" }]) rather than deriving it from the actual user's permissions. Since this is a background job handler, this may be intentional (system-level access), but please confirm this is the expected behavior and not a security gap.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (13)
apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (1)

72-83: create signature now correctly uses DB insert type; consider small type-level cleanups

Switching create to Pick<DbCreateUserWalletInput, "userId" | "address"> is a nice tightening that matches the NOT NULL DB constraint and avoids nullable userIds at the boundary. Two optional follow-ups you might consider:

  • Extract a shared type UserWalletId = DbCreateUserWalletInput["userId"]; and reuse it both here and in getOrCreate instead of Exclude<UserWalletInput["userId"], undefined | null>, to keep the “valid userId” notion in one place.
  • Add an explicit return type, e.g. async create(...): Promise<UserWalletOutput>, to lock the external contract of this repository method and make refactors safer.

These are non-blocking polish items; the current change is sound.

apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts (1)

19-19: Consider adding an index on (user_id, is_default) for query performance.

The new isDefault field enables marking a payment method as the default for a user. If the application frequently queries for the default payment method per user, consider adding a composite index on (userId, isDefault) to optimize these lookups.

Apply this diff to add the index:

   table => ({
     fingerprintPaymentMethodIdUnique: uniqueIndex("payment_methods_fingerprint_payment_method_id_unique").on(table.fingerprint, table.paymentMethodId),
     fingerprintIdx: index("payment_methods_fingerprint_idx").on(table.fingerprint),
     userIdIdx: index("payment_methods_user_id_idx").on(table.userId),
     userIdIsValidatedIdx: index("payment_methods_user_id_is_validated_idx").on(table.userId, table.isValidated),
+    userIdIsDefaultIdx: index("payment_methods_user_id_is_default_idx").on(table.userId, table.isDefault),
     userIdFingerprintPaymentMethodIdIdx: index("payment_methods_user_id_fingerprint_payment_method_id_idx").on(
       table.userId,
       table.fingerprint,
       table.paymentMethodId
     )
   })
apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (1)

44-69: LGTM!

The new route for marking a payment method as default is correctly implemented:

  • Proper schema validation with PaymentMethodMarkAsDefaultInputSchema
  • Security middleware applied via SECURITY_BEARER_OR_API_KEY
  • Handler delegates to the controller appropriately
  • Follows existing patterns in the file

Optionally, consider using a 204 No Content response instead of 200 with undefined body:

 stripePaymentMethodsRouter.openapi(markAsDefaultRoute, async function markAsDefault(c) {
   await container.resolve(StripeController).markAsDefault(c.req.valid("json"));
-  return c.json(undefined, 200);
+  return c.body(null, 204);
 });

And update the route definition:

   responses: {
-    200: {
-      description: "Payment method is marked as the default successfully."
+    204: {
+      description: "Payment method marked as default successfully"
     }
   }
apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (1)

6-6: Default-payment-method flow is sound but has consistency/race edge cases

The logic to treat the first payment method as default and to assert a paying user before calling Stripe is good. Two follow-ups to consider:

  1. Concurrency on first default
    If multiple payment_method.attached events for the same user are processed concurrently, countByUserId can return 0 in both handlers, and both inserts may attempt isDefault = true. If the repository/DB does not enforce “at most one default per user”, this can leave you with multiple defaults. If it does enforce this, one insert will fail—verify that this is intentional and that failures are logged/handled the way you want. A repository-level createAsDefault that encapsulates this invariant might be safer.

  2. External Stripe call inside the transaction + Promise.all
    Running the Stripe markRemotePaymentMethodAsDefault call in parallel with the DB insert under @WithTransaction() means:

    • You hold DB locks while waiting on the network.
    • On partial failure you can end up with Stripe state changed but the DB rolled back (or vice versa), depending on which promise fails first.
      If stronger consistency is needed, consider:
    • Committing the DB transaction first, then updating Stripe (possibly via an out-of-band job), or
    • Making the Stripe call idempotent and adding compensating logic for retry/error paths.

Also applies to: 144-157, 163-165

apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)

38-46: Repository behaviour is correct; consider de-duplicating numeric parsing

The new findByUserId return type and explicit undefined early return look good and are clearer for callers. The relation-loading helper also does the right conversions on autoReloadThreshold/autoReloadAmount, but that logic is now duplicated with toOutput. Extracting a small internal helper like parseAutoReloadFields(...) that both toOutput and findInternalByUserIdWithRelations can use would keep these conversions in one place and reduce drift if the shape ever changes.

Also applies to: 48-69

apps/api/src/billing/services/balances/balances.service.ts (1)

9-9: Fiat conversion math is fine; naming and rounding could be clearer

The overall calculation (micro-denom → base units → fiat using stats.price) is correct, and memoizing on averageBlockTime makes sense. Two small nits:

  • #toFiatAmount(uTokenAmount: number) is currently called with fullBalance.* * stats.price, i.e. a value already scaled by price. Either change the helper to accept pure uTokens and multiply by stats.price outside, or rename the parameter to better reflect that it’s “micro-value to be divided by 1e6”, not strictly a uToken count.
  • You round to 2 decimals inside #toFiatAmount and then round total again. If you ever care about minimizing rounding error, you might prefer to keep full precision until the very end and only round once.

These are stylistic/precision improvements; current behaviour is acceptable.

Also applies to: 19-21, 112-130

apps/api/src/billing/http-schemas/stripe.schema.ts (1)

10-14: New mark-as-default schema is fine; check naming consistency with other endpoints

The PaymentMethodMarkAsDefaultInputSchema + inferred type are straightforward and follow your { data: ... } convention. One thing to double-check is the field name: this schema uses data.id while RemovePaymentMethodParamsSchema uses paymentMethodId. If both refer to the same logical identifier in your public API, consider aligning the name (paymentMethodId or id) for consistency, or confirm that the difference is intentional and documented.

Also applies to: 236-236

apps/api/src/auth/services/auth.service.ts (1)

5-5: getCurrentPayingUser overloads and implementation look sound

The overload set and implementation correctly model the “strict vs non-strict” contract, and using assertIsPayingUser / isPayingUser gives you proper narrowing at call sites. Behaviour-wise, the method consistently returns undefined only for authenticated but non-paying users and throws 401 when there is no current user, which is a sensible default. If you ever want to keep the 401 logic in a single place, you could implement this in terms of this.currentUser, but that’s optional.

Also applies to: 26-42

apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1)

19-305: Comprehensive coverage; consider optional setup parameter to match house style.

The test suite exercises the handler’s main branches and error paths very well (balance thresholds, enqueue behavior, and validation logging). To fully align with the stated guideline for setup in *.spec.ts, you could later evolve setup() to accept a single (optional) config parameter with an inline type (e.g., for overrides), but this is not blocking.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (1)

279-321: Stripe/user mocks wiring for WalletSettingService looks correct; legacy as any casts can be cleaned up later.

The updated setup() correctly constructs userWithStripe, userRepository, and stripeService so the new validation logic (user lookup + default payment method check) is exercised via mocks. This matches the updated WalletSettingService constructor signature.

There are some older ... as any casts on mocked updateById results elsewhere in this file; they aren’t new in this PR but do violate the “no any cast” guideline and would be good candidates for a future typed helper or explicit mock type.

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

178-205: Auto‑reload treats non-throwing createPaymentIntent outcomes as success.

#tryToReload only distinguishes between “no error thrown” and “error thrown” from createPaymentIntent, but createPaymentIntent can return { success: false, requiresAction: true, ... } without throwing. In that case the handler will log WALLET_BALANCE_RELOADED even though the payment actually requires 3DS and may not complete.

If that distinction matters for monitoring or UX, consider checking the returned PaymentIntentResult.success and logging a different event (or treating it as a failure) when success is false.

apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)

70-122: Default-payment-method repository helpers look consistent; watch count() return type.

The new helpers (countByUserId, findDefaultByUserId, markAsDefault, createAsDefault, #unmarkAsDefaultExcluding) form a coherent default-management layer:

  • markAsDefault sets the target’s isDefault and unmarks all other defaults via #unmarkAsDefaultExcluding.
  • createAsDefault atomically creates a new default and unmarks others, which is especially safe when called inside a transaction (as done in StripeService.markPaymentMethodAsDefault).

One minor point: countByUserId returns result?.count ?? 0, and depending on the driver/schema count() may yield a bigint. If this value is later used in arithmetic or serialized, you may want to normalize it to number explicitly to avoid surprises in consumers.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (1)

45-57: upsertWalletSetting refactor and Stripe-based validation look good; consider tightening typing around PayingUser.

The refactor to upsertWalletSetting via #update / #create plus #arrangeSchedule is clear and keeps scheduling concerns separate from persistence. The new #validate logic for the “enable auto-reload” case (requiring both threshold and amount, confirming the user exists with a Stripe customer ID, and checking that a default payment method exists via stripeService.getDefaultPaymentMethod) matches the business intent.

One type-level improvement you could make (non-blocking):

  • getDefaultPaymentMethod expects a PayingUser, but #validate currently passes { ...user, stripeCustomerId } where stripeCustomerId is not narrowed to string in TypeScript by the preceding assert. If you want stricter typing, you can explicitly construct a PayingUser after the assert:
const stripeCustomerId = user.stripeCustomerId;
assert(stripeCustomerId, 404, "User payments not set up");

const payingUser: PayingUser = { ...user, stripeCustomerId };
await this.stripeService.getDefaultPaymentMethod(payingUser, ability);

Functionally this doesn’t change behavior (since getDefaultPaymentMethod already throws 404s for missing defaults), but it keeps the types aligned with the PayingUser helper.

Also applies to: 78-106, 108-131

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6512106 and b31182e.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (33)
  • apps/api/drizzle/0023_sad_adam_warlock.sql (1 hunks)
  • apps/api/drizzle/0024_thankful_stick.sql (1 hunks)
  • apps/api/drizzle/meta/0023_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/0024_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/_journal.json (1 hunks)
  • apps/api/src/auth/services/auth.service.ts (2 hunks)
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts (3 hunks)
  • apps/api/src/billing/http-schemas/stripe.schema.ts (2 hunks)
  • apps/api/src/billing/http-schemas/wallet.schema.ts (0 hunks)
  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts (1 hunks)
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1 hunks)
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (2 hunks)
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2 hunks)
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (2 hunks)
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1 hunks)
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (5 hunks)
  • apps/api/src/billing/services/balances/balances.service.ts (3 hunks)
  • apps/api/src/billing/services/paying-user/paying-user.ts (1 hunks)
  • apps/api/src/billing/services/refill/refill.service.ts (1 hunks)
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (2 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (6 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2 hunks)
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts (2 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (4 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (5 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts (9 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.ts (3 hunks)
  • apps/api/src/core/types/require.type.ts (1 hunks)
  • apps/api/test/seeders/balance.seeder.ts (2 hunks)
  • apps/api/test/seeders/database-payment-method.seeder.ts (1 hunks)
  • apps/api/test/seeders/payment-method.seeder.ts (2 hunks)
  • packages/http-sdk/src/stripe/stripe.service.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • packages/http-sdk/src/stripe/stripe.service.ts
  • apps/api/src/billing/http-schemas/wallet.schema.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/test/seeders/balance.seeder.ts
  • apps/api/test/seeders/database-payment-method.seeder.ts
  • apps/api/test/seeders/payment-method.seeder.ts
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts
  • apps/api/src/billing/services/paying-user/paying-user.ts
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/http-schemas/stripe.schema.ts
  • apps/api/src/auth/services/auth.service.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts
  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
  • apps/api/src/billing/services/refill/refill.service.ts
  • apps/api/src/billing/services/balances/balances.service.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts
  • apps/api/src/core/types/require.type.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test

Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

**/*.spec.{ts,tsx}: Use <Subject>.name in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references
Use either a method name or a condition starting with 'when' for nested suite descriptions in tests
Use present simple, 3rd person singular for test descriptions without prepending 'should'

Files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
🧠 Learnings (9)
📚 Learning: 2025-09-25T14:31:44.914Z
Learnt from: baktun14
Repo: akash-network/console PR: 1969
File: apps/deploy-web/src/pages/payment.tsx:179-191
Timestamp: 2025-09-25T14:31:44.914Z
Learning: The payment confirmation endpoint in apps/api/src/billing/http-schemas/stripe.schema.ts uses zod schema validation with `amount: z.number().gte(20, "Amount must be greater or equal to $20")` to ensure all payment requests meet the minimum amount requirement, preventing zero-amount or invalid payments from reaching Stripe.

Applied to files:

  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/http-schemas/stripe.schema.ts
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-10-15T16:39:55.348Z
Learnt from: jzsfkzm
Repo: akash-network/console PR: 2039
File: apps/deploy-web/tests/ui/change-wallets.spec.ts:4-10
Timestamp: 2025-10-15T16:39:55.348Z
Learning: In the Akash Console E2E tests using the context-with-extension fixture, the first wallet is automatically created during fixture setup via `importWalletToLeap` in `apps/deploy-web/tests/ui/fixture/wallet-setup.ts`, so tests that call `frontPage.createWallet()` are creating a second wallet to test wallet switching functionality.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
📚 Learning: 2025-11-25T17:45:52.965Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/setup-instead-of-before-each.mdc:0-0
Timestamp: 2025-11-25T17:45:52.965Z
Learning: Applies to **/*.spec.{ts,tsx} : Use `setup` function instead of `beforeEach` in test files. The `setup` function must be at the bottom of the root `describe` block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: In apps/api job scheduling with JobQueueService, use .toISOString() for startAfter values instead of Date objects to ensure UTC timezone consistency when dates are persisted to the database. Date objects can be ambiguous about timezone during serialization, while ISO strings with .toISOString() explicitly represent UTC.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: The apps/api codebase consistently uses .toISOString() for startAfter values when scheduling jobs with JobQueueService to ensure UTC timezone consistency. This pattern is used throughout trial-started.handler.ts, trial-deployment-lease-created.handler.ts and other job scheduling code. Do not suggest changing to Date objects as ISO strings provide explicit UTC representation for database storage.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-11-25T17:45:44.790Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/no-jest-mock.mdc:0-0
Timestamp: 2025-11-25T17:45:44.790Z
Learning: Applies to **/*.spec.{ts,tsx} : Don't use `jest.mock()` in test files. Instead, use `jest-mock-extended` to create mocks and pass mocks as dependencies to the service under test

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-07-30T10:18:24.469Z
Learnt from: baktun14
Repo: akash-network/console PR: 1750
File: apps/api/drizzle/meta/0017_snapshot.json:17-49
Timestamp: 2025-07-30T10:18:24.469Z
Learning: In the Akash Network console billing system, the `user_wallets.user_id` column can be NULL to support anonymous user wallets, allowing users to interact with the system before authentication. Multiple anonymous wallets with NULL user_id values are permitted by design.

Applied to files:

  • apps/api/drizzle/0023_sad_adam_warlock.sql
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
📚 Learning: 2025-09-29T02:40:22.090Z
Learnt from: stalniy
Repo: akash-network/console PR: 1951
File: apps/api/src/user/services/user/user.service.ts:48-63
Timestamp: 2025-09-29T02:40:22.090Z
Learning: In the Akash Network Console codebase, the users table was originally created by Sequelize and later migrated to Drizzle. Due to this migration history, the unique constraint on the userId field is named "userSetting_userId_unique" rather than following the typical "users_user_id_unique" pattern.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
🧬 Code graph analysis (16)
apps/api/test/seeders/balance.seeder.ts (1)
apps/api/src/billing/http-schemas/balance.schema.ts (1)
  • GetBalancesResponseOutput (17-17)
apps/api/test/seeders/database-payment-method.seeder.ts (1)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)
  • PaymentMethodOutput (11-11)
apps/api/test/seeders/payment-method.seeder.ts (3)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (237-237)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • PaymentMethod (30-41)
apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (4)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (1)
  • markAsDefault (46-51)
apps/api/src/core/services/openapi-docs/openapi-security.ts (1)
  • SECURITY_BEARER_OR_API_KEY (5-5)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethodMarkAsDefaultInputSchema (10-14)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)
  • markAsDefault (83-98)
apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (1)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • assertIsPayingUser (8-10)
apps/api/src/billing/services/paying-user/paying-user.ts (2)
apps/api/src/core/types/require.type.ts (1)
  • Require (1-3)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • user (144-169)
apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (1)
apps/api/src/core/providers/postgres.provider.ts (1)
  • ApiPgTables (80-80)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (5)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (1)
  • walletSetting (144-160)
apps/api/test/seeders/user-wallet.seeder.ts (1)
  • UserWalletSeeder (6-30)
apps/api/test/seeders/payment-method.seeder.ts (1)
  • generatePaymentMethod (11-56)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (237-237)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (8)
apps/api/test/seeders/payment-method.seeder.ts (1)
  • generatePaymentMethod (11-56)
apps/api/test/seeders/balance.seeder.ts (1)
  • generateBalance (21-34)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (4)
  • job (66-84)
  • wallet (227-234)
  • error (171-176)
  • user (144-169)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (1)
  • walletSetting (144-160)
apps/api/src/billing/events/wallet-balance-reload-check.ts (1)
  • WalletBalanceReloadCheck (5-15)
apps/api/test/seeders/user-wallet.seeder.ts (1)
  • UserWalletSeeder (6-30)
apps/api/test/seeders/user.seeder.ts (1)
  • UserSeeder (5-45)
apps/api/test/seeders/wallet-setting.seeder.ts (1)
  • generateWalletSetting (5-18)
apps/api/src/core/services/job-queue/job-queue.service.ts (1)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • job (66-84)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (2)
apps/api/src/auth/services/auth.service.ts (5)
  • Protected (62-78)
  • currentUser (13-15)
  • currentUser (17-24)
  • ability (44-46)
  • ability (48-50)
apps/api/src/billing/http-schemas/stripe.schema.ts (2)
  • PaymentMethodMarkAsDefaultInput (236-236)
  • PaymentMethodsResponse (238-238)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)
apps/api/src/core/providers/postgres.provider.ts (1)
  • ApiPgTables (80-80)
apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (2)
apps/api/src/user/model-schemas/user/user.schema.ts (1)
  • Users (8-28)
apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1)
  • UserWallets (5-18)
apps/api/src/billing/services/stripe/stripe.service.ts (5)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • PaymentMethod (30-41)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • PayingUser (6-6)
apps/api/src/auth/services/auth.service.ts (2)
  • ability (44-46)
  • ability (48-50)
apps/api/src/user/repositories/user/user.repository.ts (1)
  • UserOutput (10-12)
apps/api/src/core/services/tx/tx.service.ts (1)
  • WithTransaction (33-45)
apps/api/src/billing/services/balances/balances.service.ts (2)
apps/api/src/caching/helpers.ts (1)
  • Memoize (21-37)
apps/api/src/utils/constants.ts (1)
  • averageBlockTime (4-4)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (3)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (2)
  • WalletSettingInput (12-17)
  • WalletSettingOutput (19-22)
apps/api/src/auth/services/auth.service.ts (2)
  • ability (44-46)
  • ability (48-50)
apps/api/src/core/repositories/base.repository.ts (1)
  • isUniqueViolation (227-229)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: validate / validate-app
  • GitHub Check: validate / validate-app
🔇 Additional comments (24)
apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (1)

9-10: Good move to base wallet input types on Drizzle’s inferred insert type

Using ApiPgTables["UserWallets"]["$inferInsert"] for DbCreateUserWalletInput and deriving DbUserWalletInput from it keeps repository types aligned with the actual schema and avoids hand-maintained duplication when columns change.

apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts (2)

5-5: Import cleanup and reduced coupling look good

Dropping UserWalletInput from the imports and keeping only UserWalletPublicOutput and UserWalletRepository keeps this service decoupled from repository input types and focused on the minimal surface it actually needs. No issues here.


53-63: Add explicit return type to initialize method for API clarity.

The parameter change to string is correct—all callers including RefillService.getOrCreateUserWallet (line 95 of refill.service.ts) pass string (specifically UserWalletOutput["userId"]), not nullable values. However, the method lacks an explicit return type annotation. Since updateById(..., { returning: true }) returns Promise<UserWalletOutput>, add the annotation:

async initialize(userId: string): Promise<UserWalletOutput> {
apps/api/src/billing/services/refill/refill.service.ts (2)

10-11: Explicit service imports instead of barrel are an improvement

Importing ManagedUserWalletService and WalletInitializerService from their concrete paths instead of the @src/billing/services barrel makes dependencies clearer and reduces coupling to the barrel module. No functional concerns here.


64-75: The review comment is incorrect – no mismatch exists between the JSDoc and code.

Verification confirms the parameter, documentation, and implementation are all consistent:

  • JSDoc is correct: amountUsd is documented as "in USD cents" with example "10000 = $100" ✓
  • All callers pass amounts in cents:
    • stripe.service.ts passes coupon.amount_off (Stripe API, always cents)
    • stripe-webhook.service.ts passes checkoutSession.amount_subtotal (Stripe API, always cents)
    • stripe-webhook.service.ts passes paymentIntent.amount (Stripe API, always cents)
  • Code multiplication is correct: The * 10000 factor converts from cents to microunits (the blockchain's internal unit), where 1 USD = 1,000,000 microunits, thus 1 cent = 10,000 microunits
  • Test validation confirms: With amountUsd = 100 cents and currentLimit = 5000 microunits, the expected result is 1,005,000 microunits (5000 + 100 × 10000) ✓

The unit system is consistent throughout: Stripe amounts (cents) → topUpWallet → blockchain limits (microunits).

apps/api/test/seeders/balance.seeder.ts (1)

21-34: LGTM!

The generateBalance helper follows the established seeder pattern and correctly handles default value computation with proper precision for the total field.

apps/api/src/core/services/job-queue/job-queue.service.ts (3)

82-96: LGTM!

The enqueue method now properly captures and returns the jobId, enabling callers to track enqueued jobs. Logging includes both the job payload and assigned ID for improved traceability.


145-145: LGTM!

Passing job metadata to handlers enables them to access the job ID for logging, scheduling follow-up jobs, or cancellation purposes.


208-214: LGTM!

The JobMeta type and updated JobHandler signature provide a clean, minimal interface for job context. Using Pick<PgBoss.Job, "id"> ensures type safety with the underlying library.

apps/api/src/core/services/job-queue/job-queue.service.spec.ts (4)

68-76: LGTM!

Good addition testing the guard against duplicate handler registration. This ensures the error message is properly thrown.


95-100: LGTM!

Correctly verifies the updated JOB_ENQUEUED log structure including both job and jobId.


151-179: LGTM!

The test properly verifies that handlers receive the job metadata { id: job.id } as the second parameter, aligning with the updated JobHandler interface.


284-313: LGTM!

The setup function follows the coding guidelines: placed at the bottom of the describe block, creates the object under test, returns it with dependencies, uses inline parameter typing, and avoids shared state.

apps/api/drizzle/0024_thankful_stick.sql (1)

1-1: LGTM!

The migration is safe—the DEFAULT false ensures existing rows receive a valid value without constraint violations.

Consider whether business logic requires exactly one default payment method per user. If so, a partial unique index may be beneficial:

CREATE UNIQUE INDEX payment_methods_user_id_is_default_unique 
ON payment_methods (user_id) WHERE is_default = true;

This would prevent multiple default payment methods for the same user at the database level.

apps/api/drizzle/meta/_journal.json (1)

166-179: LGTM!

The migration journal entries are correctly formatted and sequentially ordered. The new entries (idx 23 and 24) properly reference the corresponding migration files for user_wallets user_id constraint and payment_methods is_default column.

apps/api/src/core/types/require.type.ts (1)

1-3: LGTM!

The Require utility type is correctly implemented. It properly combines Omit<T, K> to preserve non-required properties with a mapped type using the -? modifier and NonNullable to make the specified keys required and non-nullable.

apps/api/drizzle/meta/0023_snapshot.json (1)

1-791: LGTM!

The snapshot file is correctly generated and reflects the schema state after migration 0023 (user_id NOT NULL constraint on user_wallets). The structure is consistent with Drizzle ORM's snapshot format.

apps/api/test/seeders/payment-method.seeder.ts (1)

58-61: LGTM!

The generateMergedPaymentMethod helper is correctly implemented. It properly:

  • Destructures the validated flag from overrides
  • Coerces it to a boolean with !!
  • Merges it with the base payment method from generatePaymentMethod
  • Returns the correct PaymentMethod type
apps/api/drizzle/meta/0024_snapshot.json (1)

1-798: LGTM!

The snapshot file correctly reflects the schema state after migration 0024, including the new is_default field on the payment_methods table. The structure is consistent with Drizzle ORM's snapshot format.

apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1)

7-10: I'll help you verify this review comment about the userId NOT NULL constraint change. Let me investigate the codebase to confirm the concerns raised.
<function_calls>


#!/bin/bash

First, let's verify the current state of the user-wallet schema file

echo "=== Checking user-wallet schema file ==="
cat -n apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts

echo ""
echo "=== Line count for reference ==="
wc -l apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts


</function_calls>

#!/bin/bash

# Search for the migration file mentioned in the review
echo "=== Searching for migration file 0023_sad_adam_warlock.sql ==="
find . -name "*0023*" -o -name "*sad_adam_warlock*" 2>/dev/null | head -20

echo ""
echo "=== Listing all migration files to locate the relevant one ==="
fd "\.sql$" apps/api/drizzle/migrations/ 2>/dev/null | sort | tail -10
</parameter>
</invoke>
</function_calls>

```shell
#!/bin/bash

# Search for patterns where wallets are created with NULL user_id (anonymous wallets)
echo "=== Searching for anonymous wallet creation patterns ==="
rg -nP --type=ts "user_id.*null|userId.*null|anonymous.*wallet" -C2 -g '!**/test/**' -g '!**/*.spec.ts' apps/api/src/

echo ""
echo "=== Searching for UserWallets insert/create operations ==="
rg -nP --type=ts "UserWallets|user_wallets" -C2 -g '!**/test/**' -g '!**/*.spec.ts' apps/api/src/ | head -50
</parameter>
</invoke>
</function_calls>

```shell
#!/bin/bash

# Search for any code that handles NULL user_id in wallet context
echo "=== Searching for NULL user_id handling ==="
rg -nP --type=ts "isNull|notNull.*user" -C3 -g '!**/test/**' apps/api/src/billing/

echo ""
echo "=== Check wallet repository for insert/create methods ==="
fd -e ".ts" -e ".tsx" wallet apps/api/src/ 2>/dev/null | xargs rg -l "insert|create" 2>/dev/null | head -5
</parameter>
</invoke>
</function_calls>

```shell
#!/bin/bash

# Look at the billing directory structure
echo "=== Billing directory structure ==="
find apps/api/src/billing -type f -name "*.ts" | head -20

echo ""
echo "=== Check for wallet-related repositories/services ==="
rg -l "UserWallets\|UserWallet" apps/api/src/billing/ --type=ts 2>/dev/null
</parameter>
</invoke>
</function_calls>

</blockquote></details>
<details>
<summary>apps/api/src/billing/services/paying-user/paying-user.ts (1)</summary><blockquote>

`6-13`: **PayingUser type + guards look correct and consistent**

The Require-based PayingUser type and the `isPayingUser` / `assertIsPayingUser` helpers line up well with the existing `UserOutput` shape and give you clean narrowing at call sites; no issues spotted here.

</blockquote></details>
<details>
<summary>apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (1)</summary><blockquote>

`1-1`: **WalletSetting relations match the schema and FKs**

The new `WalletSettingRelations` correctly wires `userId``Users.id` and `walletId``UserWallets.id`, aligning with the table’s foreign keys and the repository’s `with: { wallet, user }` usage. No issues here.



Also applies to: 39-48

</blockquote></details>
<details>
<summary>apps/api/test/seeders/database-payment-method.seeder.ts (1)</summary><blockquote>

`3-13`: **Seeder update to PaymentMethodOutput and isDefault looks correct**

Referencing `PaymentMethodOutput` directly and seeding `isDefault: false` by default keeps tests aligned with the actual DB schema while still allowing overrides via the `overrides` argument. This is a clean, type-safe change.

</blockquote></details>
<details>
<summary>apps/api/src/billing/services/stripe/stripe.service.ts (1)</summary><blockquote>

`33-34`: **New payment-method APIs and idempotent `createPaymentIntent` integration look coherent.**

- The exported `PaymentMethod` type and `getPaymentMethods` implementation correctly combine Stripe data with local validation state and sort by recency.
- `getDefaultPaymentMethod` and `markPaymentMethodAsDefault` align with the repository’s default-handling helpers and use CASL abilities appropriately; the transactional wrapper plus remote default update cover the typical “local/remote in sync” case.
- `hasPaymentMethod` is a simple, correct ownership check used by the controller once the missing `await` is added.
- The `createPaymentIntent` refactor to accept an optional `idempotencyKey` and to centralize the Stripe call arguments is clean and matches how you use idempotency from the wallet balance reload handler.




Also applies to: 106-138, 140-178, 187-211

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

@ygrishajev ygrishajev force-pushed the feature/auto-reload-handler branch 2 times, most recently from 71539db to 90b339f Compare November 28, 2025 10:03
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (1)

80-101: Align enqueue log expectations with narrowed, non-PII payload

This test currently expects logger.info to receive the entire job object in JOB_ENQUEUED, which mirrors the PII-risky logging in the service. Once the service is updated to log only metadata (e.g. jobName, version, jobId), this expectation should be adjusted to assert the narrower payload instead of the full job object.

For example:

-      expect(logger.info).toHaveBeenCalledWith({
-        event: "JOB_ENQUEUED",
-        job,
-        jobId: "job-id-123"
-      });
+      expect(logger.info).toHaveBeenCalledWith({
+        event: "JOB_ENQUEUED",
+        jobName: job.name,
+        version: job.version,
+        jobId: "job-id-123"
+      });
apps/api/src/core/services/job-queue/job-queue.service.ts (1)

83-95: Avoid logging entire job payload in JOB_ENQUEUED (PII risk)

logger.info currently logs the full job object, which for many jobs will contain user identifiers or message content. Prefer logging only non-sensitive metadata (e.g. job name, version, and jobId) to avoid leaking PII at info level.

A minimal change would look like:

-    this.logger.info({
-      event: "JOB_ENQUEUED",
-      job,
-      jobId
-    });
+    this.logger.info({
+      event: "JOB_ENQUEUED",
+      jobName: job.name,
+      version: job.version,
+      jobId
+    });

Tests expecting the log payload should be updated accordingly.

🧹 Nitpick comments (6)
apps/api/src/core/repositories/base.repository.ts (1)

8-67: Consider delegating new transactions to TxService to keep cursor‑based code transactional

ensureTransaction correctly reuses an existing tx from txManager.getPgTx(), but in the fallback branch it starts a raw this.pg.transaction without updating TxService/AsyncLocalStorage. That means any repository calls inside cb that rely on this.cursor (or other code using TxService.getPgTx()) won’t run inside the same transaction when ensureTransaction has to create one.

To make this helper safer and future‑proof, you can delegate transaction creation to TxService.transaction, then read the tx back from getPgTx():

-  protected async ensureTransaction<T>(cb: (tx: ApiTransaction) => Promise<T>) {
-    const txCursor = this.txManager.getPgTx();
-
-    if (txCursor) {
-      return await cb(txCursor);
-    }
-
-    return await this.pg.transaction(async tx => await cb(tx));
-  }
+  protected async ensureTransaction<T>(cb: (tx: ApiTransaction) => Promise<T>) {
+    const existingTx = this.txManager.getPgTx();
+    if (existingTx) {
+      return cb(existingTx);
+    }
+
+    return this.txManager.transaction(async () => {
+      const tx = this.txManager.getPgTx();
+      if (!tx) {
+        throw new Error("Transaction was not initialized");
+      }
+      return cb(tx);
+    });
+  }

This way, both explicit tx usage and this.cursor/TxService consumers share the same transaction context.

apps/api/src/billing/services/balances/balances.service.ts (1)

9-21: Fiat balance computation looks correct; consider including isOldWallet in memoization key

The new getFullBalanceInFiat flow—reusing getFullBalance, pulling market data, then normalizing via / 1_000_000 and rounding to 2 decimals—produces the expected fiat values for u‑denominated balances. The StatsService integration and return type (GetBalancesResponseOutput["data"]) also line up.

One nuance: @Memoize currently only keys on string/number arguments, so the isOldWallet boolean parameter is ignored for both getFullBalance and getFullBalanceInFiat. If the same address can be queried with different isOldWallet values, those calls will share a cache entry. If that’s not intended, consider customizing the memoization key to incorporate isOldWallet (e.g., pass a key option or stringify the boolean in a wrapper).

Also applies to: 96-130

apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (1)

6-6: Default payment method handling and paying-user guard are well integrated (watch concurrency edge cases)

The updated handlePaymentMethodAttached flow looks solid:

  • It derives isDefault from countByUserId, making the first method default.
  • It asserts a paying user before persisting anything, so non‑paying users can’t accumulate payment methods.
  • It performs DB creation and (when applicable) remote default promotion in parallel, and logs fingerprint/isDefault for better observability.

One thing to keep in mind is concurrent payment_method.attached events for the same user: multiple in‑flight transactions could each see count === 0 and try to mark different methods as default. If this becomes an issue in practice, adding a DB‑level uniqueness constraint for the default flag or a serialized “make default” path would close that gap.

Also applies to: 144-157, 163-165

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

21-23: Avoid duplicating the Require helper and consider toning down the “pending” log

You re‑implement a generic Require type locally and use it for InitializedWallet/ActionableWalletSetting. Prefer importing the shared helper (e.g. from @src/core/types/require.type) to keep this consistent and avoid type drift. Also, the CALCULATING_NEXT_CHECK_DATE log with "Implementation pending..." will fire on every scheduling; once the heuristic is decided, consider removing or updating that note to avoid long‑term noise.

Also applies to: 25-36, 230-237

apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)

83-100: Consider simplifying createAsDefault flow inside the transaction

Within ensureTransaction, createAsDefault runs this.create and #unmarkAsDefaultExcluding via Promise.all. Given you don’t depend on parallelism here and both operate on the same logical record set, a simple sequential update (unmark others, then create the new default, or vice versa) would make the intent clearer and easier to reason about, while still benefiting from the transaction wrapper.

Also applies to: 102-116, 118-126

apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (1)

13-17: Minor service-level type and transaction-usage refinements

Two small maintainability tweaks you might consider:

  • The local WalletSettingInput interface here overlaps in name with the repository‑level WalletSettingInput type but has a different shape. Renaming this service‑input (or importing and building on the repo type) would reduce confusion between the two layers.
  • upsertWalletSetting is decorated with @WithTransaction() and #schedule also wraps work in this.txService.transaction. If TxService already collapses nested transactions onto a single underlying one (which is likely), you could potentially rely on just one of these mechanisms to simplify mental model and avoid redundant nesting.

Both are non‑blocking and can be revisited later.

Also applies to: 19-29, 145-163

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b31182e and 90b339f.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (35)
  • apps/api/drizzle/0023_sad_adam_warlock.sql (1 hunks)
  • apps/api/drizzle/0024_thankful_stick.sql (1 hunks)
  • apps/api/drizzle/meta/0023_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/0024_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/_journal.json (1 hunks)
  • apps/api/src/auth/services/auth.service.ts (2 hunks)
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts (2 hunks)
  • apps/api/src/billing/http-schemas/stripe.schema.ts (2 hunks)
  • apps/api/src/billing/http-schemas/wallet.schema.ts (0 hunks)
  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts (1 hunks)
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1 hunks)
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (2 hunks)
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2 hunks)
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (2 hunks)
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1 hunks)
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (5 hunks)
  • apps/api/src/billing/services/balances/balances.service.ts (3 hunks)
  • apps/api/src/billing/services/paying-user/paying-user.ts (1 hunks)
  • apps/api/src/billing/services/refill/refill.service.ts (1 hunks)
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (2 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (6 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2 hunks)
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts (2 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (4 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (5 hunks)
  • apps/api/src/core/repositories/base.repository.ts (2 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts (9 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.ts (3 hunks)
  • apps/api/src/core/services/tx/tx.service.ts (1 hunks)
  • apps/api/src/core/types/require.type.ts (1 hunks)
  • apps/api/test/seeders/balance.seeder.ts (2 hunks)
  • apps/api/test/seeders/database-payment-method.seeder.ts (1 hunks)
  • apps/api/test/seeders/payment-method.seeder.ts (2 hunks)
  • packages/http-sdk/src/stripe/stripe.service.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • packages/http-sdk/src/stripe/stripe.service.ts
  • apps/api/src/billing/http-schemas/wallet.schema.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/api/drizzle/meta/0023_snapshot.json
🚧 Files skipped from review as they are similar to previous changes (15)
  • apps/api/src/billing/http-schemas/stripe.schema.ts
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/core/types/require.type.ts
  • apps/api/test/seeders/balance.seeder.ts
  • apps/api/test/seeders/payment-method.seeder.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/drizzle/0024_thankful_stick.sql
  • 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/drizzle/0023_sad_adam_warlock.sql
  • apps/api/src/billing/services/refill/refill.service.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/src/core/services/tx/tx.service.ts
  • apps/api/src/core/repositories/base.repository.ts
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
  • apps/api/src/billing/services/balances/balances.service.ts
  • apps/api/src/billing/services/paying-user/paying-user.ts
  • apps/api/test/seeders/database-payment-method.seeder.ts
  • apps/api/src/auth/services/auth.service.ts
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts
  • apps/api/src/core/services/job-queue/job-queue.service.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.spec.ts
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test

Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

**/*.spec.{ts,tsx}: Use <Subject>.name in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references
Use either a method name or a condition starting with 'when' for nested suite descriptions in tests
Use present simple, 3rd person singular for test descriptions without prepending 'should'

Files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
🧠 Learnings (11)
📚 Learning: 2025-11-28T09:17:28.821Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2274
File: apps/api/drizzle/0023_sad_adam_warlock.sql:1-1
Timestamp: 2025-11-28T09:17:28.821Z
Learning: In the Akash Network console billing system, the `user_wallets.user_id` column is required (NOT NULL). All user wallets must be associated with a user ID, and there are no anonymous wallets without a user_id in production.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
📚 Learning: 2025-11-28T09:21:45.580Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2274
File: apps/api/drizzle/0023_sad_adam_warlock.sql:1-1
Timestamp: 2025-11-28T09:21:45.580Z
Learning: In the Akash Network console, `userSettings.userId` can be NULL for anonymous users, but `user_wallets.user_id` is always required (NOT NULL). Anonymous users are identified by userId=null in the userSettings table, not in the user_wallets table.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
📚 Learning: 2025-09-29T02:40:22.090Z
Learnt from: stalniy
Repo: akash-network/console PR: 1951
File: apps/api/src/user/services/user/user.service.ts:48-63
Timestamp: 2025-09-29T02:40:22.090Z
Learning: In the Akash Network Console codebase, the users table was originally created by Sequelize and later migrated to Drizzle. Due to this migration history, the unique constraint on the userId field is named "userSetting_userId_unique" rather than following the typical "users_user_id_unique" pattern.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: In apps/api job scheduling with JobQueueService, use .toISOString() for startAfter values instead of Date objects to ensure UTC timezone consistency when dates are persisted to the database. Date objects can be ambiguous about timezone during serialization, while ISO strings with .toISOString() explicitly represent UTC.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: The apps/api codebase consistently uses .toISOString() for startAfter values when scheduling jobs with JobQueueService to ensure UTC timezone consistency. This pattern is used throughout trial-started.handler.ts, trial-deployment-lease-created.handler.ts and other job scheduling code. Do not suggest changing to Date objects as ISO strings provide explicit UTC representation for database storage.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-11-25T17:45:44.790Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/no-jest-mock.mdc:0-0
Timestamp: 2025-11-25T17:45:44.790Z
Learning: Applies to **/*.spec.{ts,tsx} : Don't use `jest.mock()` in test files. Instead, use `jest-mock-extended` to create mocks and pass mocks as dependencies to the service under test

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-10-31T11:26:42.138Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.138Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-09-25T14:31:44.914Z
Learnt from: baktun14
Repo: akash-network/console PR: 1969
File: apps/deploy-web/src/pages/payment.tsx:179-191
Timestamp: 2025-09-25T14:31:44.914Z
Learning: The payment confirmation endpoint in apps/api/src/billing/http-schemas/stripe.schema.ts uses zod schema validation with `amount: z.number().gte(20, "Amount must be greater or equal to $20")` to ensure all payment requests meet the minimum amount requirement, preventing zero-amount or invalid payments from reaching Stripe.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
📚 Learning: 2025-10-15T16:39:55.348Z
Learnt from: jzsfkzm
Repo: akash-network/console PR: 2039
File: apps/deploy-web/tests/ui/change-wallets.spec.ts:4-10
Timestamp: 2025-10-15T16:39:55.348Z
Learning: In the Akash Console E2E tests using the context-with-extension fixture, the first wallet is automatically created during fixture setup via `importWalletToLeap` in `apps/deploy-web/tests/ui/fixture/wallet-setup.ts`, so tests that call `frontPage.createWallet()` are creating a second wallet to test wallet switching functionality.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:52.965Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/setup-instead-of-before-each.mdc:0-0
Timestamp: 2025-11-25T17:45:52.965Z
Learning: Applies to **/*.spec.{ts,tsx} : Use `setup` function instead of `beforeEach` in test files. The `setup` function must be at the bottom of the root `describe` block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
🧬 Code graph analysis (13)
apps/api/src/core/repositories/base.repository.ts (1)
apps/api/src/core/services/tx/tx.service.ts (1)
  • ApiTransaction (11-11)
apps/api/src/billing/services/balances/balances.service.ts (3)
apps/api/src/caching/helpers.ts (1)
  • Memoize (21-37)
apps/api/src/utils/constants.ts (1)
  • averageBlockTime (4-4)
apps/api/src/billing/http-schemas/balance.schema.ts (1)
  • GetBalancesResponseOutput (17-17)
apps/api/src/billing/services/paying-user/paying-user.ts (2)
apps/api/src/core/types/require.type.ts (1)
  • Require (1-3)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • user (145-169)
apps/api/test/seeders/database-payment-method.seeder.ts (1)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)
  • PaymentMethodOutput (11-11)
apps/api/src/auth/services/auth.service.ts (1)
apps/api/src/billing/services/paying-user/paying-user.ts (3)
  • PayingUser (6-6)
  • assertIsPayingUser (8-10)
  • isPayingUser (12-14)
apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (1)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • assertIsPayingUser (8-10)
apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (1)
apps/api/src/core/providers/postgres.provider.ts (1)
  • ApiPgTables (80-80)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (1)
apps/api/src/core/services/job-queue/job-queue.service.ts (2)
  • setup (175-182)
  • Job (189-197)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (8)
apps/api/src/core/types/require.type.ts (1)
  • Require (1-3)
apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (1)
  • UserWalletOutput (18-22)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)
  • WalletSettingOutput (19-22)
apps/api/src/billing/services/paying-user/paying-user.ts (2)
  • PayingUser (6-6)
  • isPayingUser (12-14)
apps/api/src/billing/http-schemas/balance.schema.ts (1)
  • GetBalancesResponseOutput (17-17)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (237-237)
packages/http-sdk/src/stripe/stripe.service.ts (1)
  • StripeService (18-94)
apps/api/src/billing/events/wallet-balance-reload-check.ts (1)
  • WalletBalanceReloadCheck (5-15)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (4)
apps/api/test/seeders/payment-method.seeder.ts (1)
  • generatePaymentMethod (11-56)
packages/http-sdk/src/stripe/stripe.service.ts (1)
  • StripeService (18-94)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (237-237)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2)
apps/api/src/core/providers/postgres.provider.ts (1)
  • ApiPgTables (80-80)
apps/api/src/core/services/tx/tx.service.ts (1)
  • ApiTransaction (11-11)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (3)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (2)
  • WalletSettingInput (12-17)
  • WalletSettingOutput (19-22)
apps/api/src/auth/services/auth.service.ts (2)
  • ability (44-46)
  • ability (48-50)
apps/api/src/core/repositories/base.repository.ts (1)
  • isUniqueViolation (237-239)
apps/api/src/billing/services/stripe/stripe.service.ts (7)
apps/api/src/billing/http-schemas/stripe.schema.ts (2)
  • PaymentMethod (237-237)
  • PaymentIntentResult (240-240)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • PaymentMethod (30-41)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • user (145-169)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • PayingUser (6-6)
apps/api/src/auth/services/auth.service.ts (2)
  • ability (44-46)
  • ability (48-50)
apps/api/src/user/repositories/user/user.repository.ts (1)
  • UserOutput (10-12)
apps/api/src/core/services/tx/tx.service.ts (1)
  • WithTransaction (39-51)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: Validate local packages
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (19)
apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1)

9-10: Schema change aligns with confirmed architecture.

Based on retrieved learnings, the user_wallets.user_id NOT NULL constraint is intentional and consistent with the system's design where all wallets must be associated with a user. Anonymous users (identified by userId=null in userSettings) do not have wallet records in user_wallets.

The change is properly reflected in migration 0023 and repository methods, as verified in the previous review.

apps/api/drizzle/meta/0024_snapshot.json (1)

1-798: Generated snapshot file is consistent with migrations.

The snapshot correctly reflects:

  • user_wallets.user_id with notNull: true (migration 0023)
  • payment_methods.is_default with boolean type, NOT NULL constraint, and false default (migration 0024)
  • Proper foreign key relationships with cascade deletes
  • wallet_settings table supporting the auto-reload feature with appropriate nullable fields for optional configuration

The metadata structure is valid and consistent with the schema changes in this PR.

apps/api/src/core/services/job-queue/job-queue.service.ts (2)

119-158: Passing JobMeta into handlers is a solid extension of the API

Wiring handler.handle(job.data, { id: job.id }) through the worker callback cleanly exposes the job id to handlers without expanding the payload type, and remains runtime-backwards compatible for handlers that ignore the second parameter.


208-215: JobMeta alias keeps handler context minimal and well-typed

Defining JobMeta as a minimal { id } projection and updating JobHandler.handle to accept (payload, job: JobMeta) provides enough context for logging/diagnostics while keeping the handler contract small and focused.

apps/api/src/core/services/job-queue/job-queue.service.spec.ts (5)

22-25: Queue registration tests now robustly cover config and duplicate handlers

The expectations for retryBackoff, retryDelayMax, retryLimit, and policy plus the new test that rejects when two handlers register the same queue give good protection around queue setup behavior and prevent accidental multiple-handler registration for a single queue.

Also applies to: 55-58, 68-76


122-137: Cancel logging expectations match updated log shape

The cancel test correctly asserts the JOB_CANCELLED log with id and name fields, in line with the service change; nothing further needed here.


146-207: Worker tests give solid coverage of success and failure paths with job meta

The startWorkers tests:

  • Simulate processing a single job object via the mocked work handler.
  • Verify JOB_STARTED, JOB_DONE, and JOB_FAILED logs use job.id.
  • Assert that handlers are invoked with the payload plus { id: job.id }.
  • Check concurrency wiring by asserting work call counts.

This provides good confidence in the worker orchestration and the new JobMeta-aware handler contract.


209-227: Default worker options test correctly validates implicit concurrency

The "uses default options when none provided" test cleanly verifies that two workers are started (default concurrency) and that each invokes the handler with payload + job meta once, which accurately captures the intended default behavior.


284-303: Typed runWithContext stub accurately simulates execution context behavior

Stubbing runWithContext as async (cb: () => Promise<unknown>) => await cb() and typing it as ExecutionContextService["runWithContext"] both matches the real signature and ensures the callback is actually executed in tests, which keeps behavior realistic without introducing shared state.

apps/api/src/core/services/tx/tx.service.ts (1)

11-24: ApiTransaction alias and nested-transaction reuse look good

Centralizing the PgTransaction typing into ApiTransaction and short‑circuiting when a tx is already in AsyncLocalStorage keeps nested WithTransaction calls cheap and type‑safe. No issues here.

apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (1)

9-11: Stronger insert typing for user wallets is aligned with the schema

Basing DbCreateUserWalletInput on $inferInsert and tightening create to require userId and address matches the new NOT NULL constraint on user_id and keeps the repo API close to the actual table shape. The UserWalletInputDbUserWalletInput mapping still looks coherent.

Also applies to: 72-83

apps/api/src/billing/services/paying-user/paying-user.ts (1)

1-14: PayingUser type and guards are clean and composable

The Require<UserOutput, "stripeCustomerId"> alias plus isPayingUser/assertIsPayingUser give a nice, reusable way to enforce “paying user” semantics both at runtime and in types. The 402 status/message also clearly expresses the billing prerequisite.

apps/api/test/seeders/database-payment-method.seeder.ts (1)

3-6: Seeder now matches the real payment method output shape

Typing the seeder against PaymentMethodOutput and seeding isDefault: false keeps test fixtures in sync with the repository’s output type and the new isDefault column. This should reduce drift between tests and the actual DB model.

Also applies to: 12-12

apps/api/src/auth/services/auth.service.ts (1)

5-5: Paying-user accessor overloads are sound and preserve auth semantics

The getCurrentPayingUser overloads make the “strict by default, optional non‑strict” behavior clear, and the implementation correctly:

  • 401s when there is no current user, regardless of strict.
  • Uses assertIsPayingUser when strict is true.
  • Falls back to undefined only for non‑paying users when strict is false.

This keeps error semantics predictable while giving callers a flexible way to gate on paying users.

Also applies to: 26-42

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

56-85: Wallet balance reload orchestration and error handling look solid

The end‑to‑end flow (resource collection, validation, reload attempt, logging, and rescheduling) is cohesive and well‑guarded with typed Result/ValidationError paths, idempotent Stripe charge creation, and structured logging for all decision branches. No functional issues spotted here.

Also applies to: 179-217

apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (1)

10-12: Test setup correctly reflects new Stripe and user dependencies

The updated setup wiring (paying user via userWithStripe, userRepository.findById, stripeService.getDefaultPaymentMethod, and txService.transaction passthrough) aligns with the new service constructor and #validate logic, so specs now properly cover the “enabled auto‑reload requires a default payment method” behavior without leaking real Stripe calls.

Also applies to: 15-16, 279-332

apps/api/src/billing/services/stripe/stripe.service.ts (2)

147-174: Improved default-payment-method update ordering looks correct

markPaymentMethodAsDefault now performs local repo updates inside a transaction and only calls markRemotePaymentMethodAsDefault after local state is established, avoiding the previous pattern of mixing the external Stripe update into the initial Promise.all. This reduces the window where Stripe and the DB can diverge due to mid-flight failures while keeping the API surface clean.

Also applies to: 176-176


182-213: I'll verify the concerns about createPaymentIntent callers passing amounts in the correct units. Let me start by finding all usages of this method and analyzing how amounts are computed.
<function_calls>
<function_calls_item>
<function_calls_invoke name="shell">
#!/bin/bash

Find all createPaymentIntent usages and their context

rg -n "createPaymentIntent\s*(" apps -A 5 -B 2 --type ts --type tsx


</function_calls_item>
</function_calls>
</function_calls_invoke>

apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (1)

45-58: Auto-reload validation and scheduling logic are cohesive and consistent

The upsertWalletSetting flow (#update/#create + #validate + #arrangeSchedule) looks well-structured:

  • Enforces autoReloadThreshold/autoReloadAmount presence only when enabling auto‑reload.
  • Ensures the user exists, has a stripeCustomerId, and has a default payment method via StripeService.getDefaultPaymentMethod before allowing auto‑reload.
  • Handles race-y create vs. existing row via isUniqueViolation and a retry through #update.
  • Schedules/cancels the WalletBalanceReloadCheck job in a single place based on autoReloadEnabled transitions, keeping autoReloadJobId in sync.

No functional issues stand out in this path.

Also applies to: 60-77, 79-107, 109-133, 135-143, 145-163

@ygrishajev ygrishajev force-pushed the feature/auto-reload-handler branch from 90b339f to 47469f8 Compare November 28, 2025 10:27
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
apps/api/src/core/services/job-queue/job-queue.service.ts (1)

83-95: Avoid logging full job payload; log non‑sensitive metadata only

JOB_ENQUEUED is still logging the entire job object, which can include PII or other sensitive payload fields at info level. This repeats the concern from the earlier review and should be tightened.

Recommend logging just minimal metadata (e.g., name, id, version) instead of the full payload:

   async enqueue(job: Job, options?: EnqueueOptions): Promise<string | null> {
     const jobId = await this.pgBoss.send({
       name: job.name,
       data: { ...job.data, version: job.version },
       options
     });

-    this.logger.info({
-      event: "JOB_ENQUEUED",
-      job,
-      jobId
-    });
+    this.logger.info({
+      event: "JOB_ENQUEUED",
+      jobName: job.name,
+      jobId,
+      version: job.version
+    });

     return jobId;
   }
🧹 Nitpick comments (3)
apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts (1)

53-63: Consider adding an explicit return type and aligning with public DTOs

initialize(userId: string) currently has an implicit return type derived from updateById(..., { returning: true }), which is likely the internal entity, while initializeAndGrantTrialLimits returns UserWalletPublicOutput. For API clarity and consistency, consider:

  • Declaring an explicit return type for initialize (either the entity or UserWalletPublicOutput), and
  • Potentially returning toPublic(...) here as well if this method is used at the API boundary.

This is not a blocker but would make the service contract clearer and more robust to repository-level changes.

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

15-23: Reuse shared Require utility instead of redefining locally

You’re re‑defining a Require helper that already exists in apps/api/src/core/types/require.type.ts. Importing and reusing the shared type would avoid duplication and keep the semantics consistent across the codebase.

apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)

83-126: Default‑flag updates are now scoped in a transaction, with a minor style tweak

Wrapping markAsDefault and createAsDefault in ensureTransaction and centralizing the “unmark others” logic in #unmarkAsDefaultExcluding ensures consistent default status per user at commit time.

In createAsDefault, you might want to use the provided tx for the insert as well (instead of this.create) so all DB writes in the callback clearly share the same transactional context.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90b339f and 47469f8.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (35)
  • apps/api/drizzle/0023_sad_adam_warlock.sql (1 hunks)
  • apps/api/drizzle/0024_thankful_stick.sql (1 hunks)
  • apps/api/drizzle/meta/0023_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/0024_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/_journal.json (1 hunks)
  • apps/api/src/auth/services/auth.service.ts (2 hunks)
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts (2 hunks)
  • apps/api/src/billing/http-schemas/stripe.schema.ts (2 hunks)
  • apps/api/src/billing/http-schemas/wallet.schema.ts (0 hunks)
  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts (1 hunks)
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1 hunks)
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (2 hunks)
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2 hunks)
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (2 hunks)
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1 hunks)
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (5 hunks)
  • apps/api/src/billing/services/balances/balances.service.ts (3 hunks)
  • apps/api/src/billing/services/paying-user/paying-user.ts (1 hunks)
  • apps/api/src/billing/services/refill/refill.service.ts (1 hunks)
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (2 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (6 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2 hunks)
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts (2 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (4 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (5 hunks)
  • apps/api/src/core/repositories/base.repository.ts (2 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts (9 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.ts (3 hunks)
  • apps/api/src/core/services/tx/tx.service.ts (1 hunks)
  • apps/api/src/core/types/require.type.ts (1 hunks)
  • apps/api/test/seeders/balance.seeder.ts (2 hunks)
  • apps/api/test/seeders/database-payment-method.seeder.ts (1 hunks)
  • apps/api/test/seeders/payment-method.seeder.ts (2 hunks)
  • packages/http-sdk/src/stripe/stripe.service.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • packages/http-sdk/src/stripe/stripe.service.ts
  • apps/api/src/billing/http-schemas/wallet.schema.ts
🚧 Files skipped from review as they are similar to previous changes (17)
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts
  • apps/api/test/seeders/payment-method.seeder.ts
  • apps/api/src/core/services/tx/tx.service.ts
  • apps/api/src/billing/services/paying-user/paying-user.ts
  • apps/api/drizzle/meta/0023_snapshot.json
  • apps/api/test/seeders/balance.seeder.ts
  • apps/api/drizzle/0024_thankful_stick.sql
  • apps/api/src/core/types/require.type.ts
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
  • apps/api/src/core/repositories/base.repository.ts
  • 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/refill/refill.service.ts
  • apps/api/test/seeders/database-payment-method.seeder.ts
  • apps/api/drizzle/meta/0024_snapshot.json
  • apps/api/drizzle/0023_sad_adam_warlock.sql
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/src/billing/http-schemas/stripe.schema.ts
  • apps/api/src/billing/services/balances/balances.service.ts
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
  • apps/api/src/billing/services/stripe/stripe.service.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.spec.ts
  • apps/api/src/auth/services/auth.service.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test

Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

**/*.spec.{ts,tsx}: Use <Subject>.name in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references
Use either a method name or a condition starting with 'when' for nested suite descriptions in tests
Use present simple, 3rd person singular for test descriptions without prepending 'should'

Files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
🧠 Learnings (8)
📚 Learning: 2025-09-25T14:31:44.914Z
Learnt from: baktun14
Repo: akash-network/console PR: 1969
File: apps/deploy-web/src/pages/payment.tsx:179-191
Timestamp: 2025-09-25T14:31:44.914Z
Learning: The payment confirmation endpoint in apps/api/src/billing/http-schemas/stripe.schema.ts uses zod schema validation with `amount: z.number().gte(20, "Amount must be greater or equal to $20")` to ensure all payment requests meet the minimum amount requirement, preventing zero-amount or invalid payments from reaching Stripe.

Applied to files:

  • apps/api/src/billing/http-schemas/stripe.schema.ts
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
📚 Learning: 2025-10-31T11:26:42.138Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.138Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
📚 Learning: 2025-11-28T09:17:28.821Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2274
File: apps/api/drizzle/0023_sad_adam_warlock.sql:1-1
Timestamp: 2025-11-28T09:17:28.821Z
Learning: In the Akash Network console billing system, the `user_wallets.user_id` column is required (NOT NULL). All user wallets must be associated with a user ID, and there are no anonymous wallets without a user_id in production.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
📚 Learning: 2025-11-28T09:21:45.580Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2274
File: apps/api/drizzle/0023_sad_adam_warlock.sql:1-1
Timestamp: 2025-11-28T09:21:45.580Z
Learning: In the Akash Network console, `userSettings.userId` can be NULL for anonymous users, but `user_wallets.user_id` is always required (NOT NULL). Anonymous users are identified by userId=null in the userSettings table, not in the user_wallets table.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
📚 Learning: 2025-09-29T02:40:22.090Z
Learnt from: stalniy
Repo: akash-network/console PR: 1951
File: apps/api/src/user/services/user/user.service.ts:48-63
Timestamp: 2025-09-29T02:40:22.090Z
Learning: In the Akash Network Console codebase, the users table was originally created by Sequelize and later migrated to Drizzle. Due to this migration history, the unique constraint on the userId field is named "userSetting_userId_unique" rather than following the typical "users_user_id_unique" pattern.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
📚 Learning: 2025-10-15T16:39:55.348Z
Learnt from: jzsfkzm
Repo: akash-network/console PR: 2039
File: apps/deploy-web/tests/ui/change-wallets.spec.ts:4-10
Timestamp: 2025-10-15T16:39:55.348Z
Learning: In the Akash Console E2E tests using the context-with-extension fixture, the first wallet is automatically created during fixture setup via `importWalletToLeap` in `apps/deploy-web/tests/ui/fixture/wallet-setup.ts`, so tests that call `frontPage.createWallet()` are creating a second wallet to test wallet switching functionality.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:52.965Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/setup-instead-of-before-each.mdc:0-0
Timestamp: 2025-11-25T17:45:52.965Z
Learning: Applies to **/*.spec.{ts,tsx} : Use `setup` function instead of `beforeEach` in test files. The `setup` function must be at the bottom of the root `describe` block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
🧬 Code graph analysis (8)
apps/api/src/billing/services/balances/balances.service.ts (3)
apps/api/src/caching/helpers.ts (1)
  • Memoize (21-37)
apps/api/src/utils/constants.ts (1)
  • averageBlockTime (4-4)
apps/api/src/billing/http-schemas/balance.schema.ts (1)
  • GetBalancesResponseOutput (17-17)
apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (4)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (1)
  • markAsDefault (46-51)
apps/api/src/core/services/openapi-docs/openapi-security.ts (1)
  • SECURITY_BEARER_OR_API_KEY (5-5)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethodMarkAsDefaultInputSchema (10-14)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)
  • markAsDefault (83-100)
apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (1)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • assertIsPayingUser (8-10)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (4)
apps/api/src/auth/services/auth.service.ts (1)
  • Protected (62-78)
apps/api/src/billing/http-schemas/stripe.schema.ts (4)
  • PaymentMethodMarkAsDefaultInput (236-236)
  • PaymentMethodsResponse (238-238)
  • ConfirmPaymentRequest (239-239)
  • ConfirmPaymentResponse (242-242)
apps/api/src/core/lib/semaphore.decorator.ts (1)
  • Semaphore (3-28)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • ConfirmPaymentResponse (104-109)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2)
apps/api/src/core/providers/postgres.provider.ts (1)
  • ApiPgTables (80-80)
apps/api/src/core/services/tx/tx.service.ts (1)
  • ApiTransaction (11-11)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (2)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • userId (87-143)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (3)
  • userId (60-77)
  • userId (79-107)
  • walletSetting (145-163)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (9)
apps/api/src/core/types/require.type.ts (1)
  • Require (1-3)
apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (1)
  • UserWalletOutput (18-22)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)
  • WalletSettingOutput (19-22)
apps/api/src/billing/services/paying-user/paying-user.ts (2)
  • PayingUser (6-6)
  • isPayingUser (12-14)
apps/api/src/billing/http-schemas/balance.schema.ts (1)
  • GetBalancesResponseOutput (17-17)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (237-237)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
packages/http-sdk/src/stripe/stripe.service.ts (1)
  • StripeService (18-94)
apps/api/src/billing/events/wallet-balance-reload-check.ts (1)
  • WalletBalanceReloadCheck (5-15)
apps/api/src/auth/services/auth.service.ts (1)
apps/api/src/billing/services/paying-user/paying-user.ts (3)
  • PayingUser (6-6)
  • assertIsPayingUser (8-10)
  • isPayingUser (12-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (29)
apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts (1)

5-5: Import cleanup and type usage look good

The narrowed import set (UserWalletPublicOutput, UserWalletRepository) matches actual usage in this file and keeps the dependency surface minimal. No issues here.

apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1)

7-10: Schema correctly aligned with NOT NULL invariant for user_wallets.user_id.

Making userId .notNull() (while keeping it unique() and FK‑constrained) is consistent with migration 0023_sad_adam_warlock.sql and the documented business rule that all wallets must belong to a concrete user, with anonymous state tracked via userSettings.userId = null instead of null wallets. This removes ORM/DB drift and encodes the actual domain invariant in the model schema. Based on learnings, this looks intentional and sound.

apps/api/src/billing/controllers/stripe/stripe.controller.ts (4)

8-8: Typed Stripe schemas integration looks consistent

Importing PaymentMethodMarkAsDefaultInput and PaymentMethodsResponse from the HTTP schema keeps controller contracts aligned with validated types; no issues here.


45-51: markAsDefault wiring matches service and auth expectations

Using Protected with action: "update" plus getCurrentPayingUser() and delegating to stripe.markPaymentMethodAsDefault is a clean, minimal controller; behavior and types line up with the new service API.


54-62: Graceful handling of missing paying user in getPaymentMethods

Using getCurrentPayingUser({ strict: false }) and returning { data: [] } when no paying user is resolved avoids hard failures in edge cases and keeps the response shape stable.


68-73: Ownership check in confirmPayment is now effective

Fetching the (possibly non‑strict) paying user, asserting its presence, and awaiting this.stripe.hasPaymentMethod before proceeding correctly closes the previous security hole around unauthorized payment method usage.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (2)

10-16: Test wiring for new Stripe and user dependencies looks correct

Bringing in PaymentMethod, StripeService, JobQueueService, TxService, UserRepository, and generatePaymentMethod and using userWithStripe in setup() aligns the spec with the updated WalletSettingService constructor and validation behavior.


279-332: setup helper matches the repository’s testing conventions

Using a setup() factory at the bottom of the root describe, returning the SUT plus all mocks (including userRepository, stripeService, and txService), is consistent with the project’s testing style and keeps tests free of shared mutable state.

apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (5)

67-85: Resource collection and validation are cleanly separated

#collectResources composing #getValidWalletResources, #getDefaultPaymentMethod, and balancesService.getFullBalanceInFiat into a single Result<AllResources, ValidationError> makes the main handle flow straightforward and centralizes failure modes nicely.


87-143: Comprehensive preconditions before attempting auto‑reload

#getValidWalletResources covers all the key guards (wallet setting existence, auto‑reload enabled, threshold and amount set, wallet initialized, user being a PayingUser) and returns structured ValidationErrors. This should significantly reduce noisy failures in the job handler.


145-169: Default payment method resolution integrates Stripe and CASL correctly

Using getDefaultPaymentMethod with a minimal createMongoAbility instance and mapping http-errors into ValidationError via isHttpError cleanly bridges authorization, Stripe state, and your job-level error taxonomy.


178-213: Auto‑reload Stripe call is guarded and idempotent

The reload attempt only fires when autoReloadThreshold >= balance, uses the new createPaymentIntent wrapper (with amounts in base units) and passes an idempotencyKey derived from the job id, which is a solid pattern for preventing duplicate charges on retries.


215-233: Next‑check scheduling and persistence are consistent with wallet settings

scheduleNextCheck enqueues a new WalletBalanceReloadCheck with a singleton key per user and, when a job id is returned, persists it back to the wallet setting; #calculateNextCheckDate currently implements a simple “+1 day” strategy while logging the decision, which is a reasonable starting point.

apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2)

10-12: Exporting PaymentMethodInput/Output improves type reuse

Making the insert/select types public will simplify typing across services, seeders, and tests that need to construct or assert on payment methods.


70-81: Helper methods for counts and default lookup are straightforward

countByUserId and findDefaultByUserId are thin, readable wrappers around Drizzle queries and existing repository helpers; they should make higher‑level Stripe and billing logic easier to express.

apps/api/src/billing/services/stripe/stripe.service.ts (7)

106-120: getPaymentMethods correctly merges Stripe and local validation state

Fetching Stripe payment methods and joining them with DB records to set a validated flag, then sorting by created descending, cleanly exposes exactly what the API needs without overloading the controller.


122-138: getDefaultPaymentMethod enforces both local and remote invariants

Retrieving the Stripe customer (with expanded default payment method) alongside the locally stored default, then asserting on both, ensures you don’t silently drift between DB and Stripe state and provides clear error sourcing via { source: "database" | "stripe" }.


140-145: hasPaymentMethod implements a proper ownership check

Looking up the Stripe payment method by id and comparing its customer to user.stripeCustomerId is the right primitive for the controller’s authorization check; this closes the gap where arbitrary IDs could previously be used.


390-392: Simplified getCoupon implementation

Returning this.coupons.retrieve(couponId) directly keeps the method minimal and consistent with other simple wrappers in this service.


147-174: Review comment verified as accurate and well-founded

The implementation correctly uses customers.update with invoice_settings.default_payment_method (lines 176-179), which aligns with current Stripe API documentation. The sequencing is safe: local database operations complete within the transaction first, then markRemotePaymentMethodAsDefault is called to update Stripe, ensuring the remote call operates on confirmed database state.


182-235: The review comment accurately describes the Stripe SDK integration — verification confirms the implementation is correct.

The Stripe Node.js SDK (v19.3.0) signature for paymentIntents.create is create(params, options) where idempotencyKey belongs in the options object (second parameter). The code correctly:

  1. Types createOptions as Parameters<Stripe["paymentIntents"]["create"]> (tuple array)
  2. Places payment parameters in createOptions[0]
  3. Conditionally pushes { idempotencyKey } to createOptions[1]
  4. Spreads the array with ...createOptions, expanding to create(params, options) when present

The Math.round(params.amount * 100) conversion to cents is also correct for Stripe's API expectations.


1-18: New PaymentMethod type and PayingUser integration are well factored

Defining export type PaymentMethod = Stripe.PaymentMethod & { validated: boolean } and threading AnyAbility / PayingUser through the service public API makes the Stripe layer more expressive and easier to consume from controllers and jobs.

✓ Verified: Stripe.PaymentMethod.created is a numeric field (Unix epoch seconds), and paymentIntents.create(params, options?) accepts the two-argument form as used in createPaymentIntent (line 212, where createOptions is spread to pass idempotencyKey in RequestOptions).

apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (6)

8-12: New dependencies and WalletSettingInput interface align with the extended behavior

Injecting UserRepository, StripeService, JobQueueService, and TxService, plus introducing WalletSettingInput, matches the new responsibilities (validating payments setup and orchestrating auto‑reload jobs) without leaking implementation details to callers.

Also applies to: 21-29


45-58: upsertWalletSetting’s update/create + schedule flow is well structured

Using #update first, falling back to #create on missing settings, and then centralizing job orchestration in #arrangeSchedule under @WithTransaction() keeps the public API simple while handling all edge cases internally.


60-77: Update path cleanly separates read, validate, and write

#update reads the existing setting with ability checks, validates the combination of previous and next values, then performs an updateById with returning: true, returning both prev and next for downstream scheduling logic; no issues spotted.


79-107: Create path now handles unique-constraint races via retry

#create validates input, asserts the presence of a user wallet, and on isUniqueViolation retries via #update, ensuring a single setting per user even under concurrent upserts while preserving meaningful 404/500 errors.


135-163: Scheduling and cancellation logic are coherent and transactional

#arrangeSchedule only creates a schedule when transitioning from disabled→enabled and cancels any existing job when disabling; #schedule wraps cancellation, job id persistence, and enqueueing in txService.transaction, asserting that a job id is actually created.


109-132: Validation correctly gates auto‑reload on payments configuration—confirmed

The review comment is accurate. Verification confirms:

  • CASL ability compatibility: @casl/ability v6.7.1 is current; AnyAbility type is properly imported and used in both StripeService.getDefaultPaymentMethod (stripe.service.ts:122) and the call site (wallet-settings.service.ts:126-128)
  • StripeService.getDefaultPaymentMethod: Exists with correct signature async getDefaultPaymentMethod(user: PayingUser, ability: AnyAbility): Promise<PaymentMethod>, matching the usage pattern
  • Type safety: No any types; PaymentMethod is properly typed as Stripe.PaymentMethod & { validated: boolean }

The #validate method correctly enforces that auto‑reload requires threshold, amount, a valid user with stripeCustomerId, and an active default payment method before enabling.

apps/api/src/core/services/job-queue/job-queue.service.ts (1)

145-145: JobMeta wiring looks good; ensure all handlers are updated to the new signature

Passing { id: job.id } into handlers via JobMeta is a clean way to give handlers job context without exposing the full PgBoss.Job.

Since JobHandler.handle now has a breaking signature (handle(payload: JobPayload<T>, job: JobMeta)), please verify that all existing handlers have been updated to accept the second job parameter and that no overrides still use the old single‑argument form.

Also applies to: 208-215

@ygrishajev ygrishajev force-pushed the feature/auto-reload-handler branch from 47469f8 to af05421 Compare November 28, 2025 10:53
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

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.spec.ts (1)

219-219: Remove as any cast.

Additional as any cast found that should be removed for consistency.

Apply this diff:

-      walletSettingRepository.updateById.mockResolvedValue(updatedSetting as any);
+      walletSettingRepository.updateById.mockResolvedValue(updatedSetting);

Based on coding guidelines.

♻️ Duplicate comments (4)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)

79-126: Default payment-method update flow still has concurrency and scoping pitfalls; simplify and scope by user

The default-handling code is much closer with ensureTransaction, but two issues remain:

  1. createAsDefault runs create(...) and #unmarkAsDefaultExcluding(...) in parallel via Promise.all on the same logical operation, which complicates reasoning about ordering inside the transaction and is unnecessary.
  2. #unmarkAsDefaultExcluding only constrains by { isDefault: true } (plus whatever queryToWhere injects), not explicitly by userId, so its behavior depends on ability filters and is less explicit than it could be.

You can address both by:

  • Running operations sequentially inside the transaction.
  • Passing userId into #unmarkAsDefaultExcluding and scoping the UPDATE explicitly to that user.
  • Dropping the redundant this.toOutput(output) in createAsDefault, since this.create() already returns a PaymentMethodOutput.

For example:

   async markAsDefault(paymentMethodId: string) {
     return this.ensureTransaction(async tx => {
       const [output] = await tx
         .update(this.table)
         .set({
           isDefault: true,
           updatedAt: sql`now()`
         })
         .where(this.queryToWhere({ paymentMethodId }))
         .returning();

       if (output) {
-        await this.#unmarkAsDefaultExcluding(output.id, tx);
-
-        return this.toOutput(output);
+        await this.#unmarkAsDefaultExcluding(output.userId, output.id, tx);
+
+        return this.toOutput(output);
       }
     });
   }

   async createAsDefault(input: Omit<PaymentMethodInput, "id" | "isDefault">) {
     return this.ensureTransaction(async tx => {
       const id = uuidv4();
-      const [output] = await Promise.all([
-        this.create({
-          ...input,
-          isDefault: true,
-          id
-        }),
-        this.#unmarkAsDefaultExcluding(id, tx)
-      ]);
-
-      return this.toOutput(output);
+      const output = await this.create({
+        ...input,
+        isDefault: true,
+        id
+      });
+
+      await this.#unmarkAsDefaultExcluding(output.userId, output.id, tx);
+
+      return output;
     });
   }

-  async #unmarkAsDefaultExcluding(excludedId: PaymentMethodOutput["id"], tx: ApiTransaction) {
-    await tx
-      .update(this.table)
-      .set({
-        isDefault: false,
-        updatedAt: sql`now()`
-      })
-      .where(and(this.queryToWhere({ isDefault: true }), ne(this.table.id, excludedId)));
-  }
+  async #unmarkAsDefaultExcluding(
+    userId: PaymentMethodOutput["userId"],
+    excludedId: PaymentMethodOutput["id"],
+    tx: ApiTransaction
+  ) {
+    await tx
+      .update(this.table)
+      .set({
+        isDefault: false,
+        updatedAt: sql`now()`
+      })
+      .where(
+        and(
+          this.queryToWhere({ userId, isDefault: true }),
+          ne(this.table.id, excludedId)
+        )
+      );
+  }

This makes the mutation order inside the transaction deterministic, avoids concurrent queries on the same operation, and guarantees that only the current user’s payment methods are affected, independent of how queryToWhere composes ability filters. There is still a theoretical window where two concurrent requests for the same user can race, so if “exactly one default per user” is a hard invariant, you may also want a partial unique index on (user_id) where is_default = true.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (3)

114-114: Remove as any cast.

Same issue as line 54 - this unresolved cast should be removed.

Apply this diff:

-      walletSettingRepository.updateById.mockResolvedValue(newSetting as any);
+      walletSettingRepository.updateById.mockResolvedValue(newSetting);

Based on coding guidelines, as per previous review comments.


185-185: Remove as any cast.

Same issue - third occurrence of the unresolved cast.

Apply this diff:

-      walletSettingRepository.updateById.mockResolvedValue(updatedSetting as any);
+      walletSettingRepository.updateById.mockResolvedValue(updatedSetting);

Based on coding guidelines, as per previous review comments.


54-54: Remove as any cast.

This as any cast was flagged in a previous review but remains unaddressed. The mock should return the proper WalletSettingOutput type without casting.

Apply this diff:

-      walletSettingRepository.updateById.mockResolvedValue(updatedSetting as any);
+      walletSettingRepository.updateById.mockResolvedValue(updatedSetting);

If type errors occur, ensure generateWalletSetting returns WalletSettingOutput or properly type the variable:

const updatedSetting: WalletSettingOutput = generateWalletSetting({...});

Based on coding guidelines, as per previous review comments.

🧹 Nitpick comments (9)
apps/api/src/core/repositories/base.repository.ts (1)

59-67: Rename the generic type parameter to avoid shadowing.

The method's generic type parameter T shadows the class's generic T (which represents the table type). While this works, it reduces code clarity and can confuse developers.

Consider renaming it to TResult or R for better readability:

-protected async ensureTransaction<T>(cb: (tx: ApiTransaction) => Promise<T>) {
+protected async ensureTransaction<TResult>(cb: (tx: ApiTransaction) => Promise<TResult>) {
   const txCursor = this.txManager.getPgTx();
   
   if (txCursor) {
     return await cb(txCursor);
   }
   
   return await this.pg.transaction(async tx => await cb(tx));
 }

Otherwise, the transaction logic is correct—it properly reuses existing transactions when available or creates new ones when needed.

apps/api/src/auth/services/auth.service.ts (1)

26-42: getCurrentPayingUser overloads and behavior look sound; minor DRY opportunity

The overloads correctly model the runtime behavior (strict by default, 401 when no user, 402 when strict and not paying, undefined only in non‑strict mode), and the implementation is consistent with the paying-user helpers.

Optionally, consider reusing the existing currentUser accessor or a small private helper to avoid duplicating the "CURRENT_USER" key lookup and 401 assertion logic in multiple places.

apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts (1)

19-21: isDefault column addition looks correct; consider indexing by user + default for lookups

The NOT NULL + default(false) mapping is aligned with how PaymentMethods is used. If you expect frequent findDefaultByUserId queries, consider adding a composite index on (user_id, is_default) in the migration to keep lookups efficient.

apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (1)

5-11: New “mark default payment method” route is wired correctly; 204 could be a cleaner status

The POST /v1/stripe/payment-methods/default route correctly validates the body against PaymentMethodMarkAsDefaultInputSchema, delegates to StripeController.markAsDefault, and is secured consistently with other payment routes.

Since the operation doesn’t return a body, you might consider mirroring the delete route and using 204 + c.body(null, 204) instead of 200 + c.json(undefined, 200), but this is mostly a taste/consistency choice.

Also applies to: 44-69

apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)

48-69: Consider reusing toOutput to avoid duplicating numeric field parsing.

Lines 66-67 duplicate the parseFloat logic from toOutput (lines 75-76). This could lead to drift if the transformation logic changes.

     if (!walletSetting) return undefined;
 
+    const output = this.toOutput(walletSetting);
     return {
-      ...walletSetting,
-      autoReloadThreshold: walletSetting.autoReloadThreshold === null ? undefined : parseFloat(walletSetting.autoReloadThreshold),
-      autoReloadAmount: walletSetting.autoReloadAmount === null ? undefined : parseFloat(walletSetting.autoReloadAmount)
+      ...output,
+      wallet: walletSetting.wallet,
+      user: walletSetting.user
     };

Alternatively, extract the numeric parsing into a shared helper.

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

226-233: Placeholder implementation noted—ensure this is addressed before production use.

The #calculateNextCheckDate method logs "Implementation pending..." and returns a hardcoded 1-day interval. This should be tracked for completion.

Would you like me to open an issue to track implementing the proper next-check-date calculation based on wallet activity or deployment costs?

apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1)

256-306: LGTM! Comprehensive test coverage.

The test suite follows the project's testing guidelines well, using jest-mock-extended, placing the setup function at the bottom, and using proper naming conventions.

As per coding guidelines, the setup function should accept a single parameter with inline type definition to allow for future customization, even if not currently needed. Consider:

-  function setup() {
+  function setup(overrides: { user?: Partial<ReturnType<typeof UserSeeder.create>> } = {}) {
-    const user = UserSeeder.create();
+    const user = UserSeeder.create(overrides.user);
apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (1)

279-333: Consider adding parameter to setup function.

As per coding guidelines, setup functions should accept a single parameter with inline type definition to allow future customization.

-  function setup() {
+  function setup(overrides: { userId?: string; walletSettingOverrides?: Partial<WalletSettingOutput> } = {}) {
-    const user = UserSeeder.create();
+    const user = UserSeeder.create(overrides.userId ? { id: overrides.userId } : undefined);

Based on coding guidelines.

apps/api/src/billing/services/stripe/stripe.service.ts (1)

189-212: Refactor idempotencyKey handling for better readability.

The current implementation using an array with conditional spreading works but is not idiomatic for the Stripe API, which accepts two distinct arguments: params and options.

Apply this diff for clearer intent:

-    const amountInCents = Math.round(params.amount * 100);
-
-    const createOptions: Parameters<Stripe["paymentIntents"]["create"]> = [
-      {
-        customer: params.customer,
-        payment_method: params.payment_method,
-        amount: amountInCents,
-        currency: params.currency,
-        confirm: params.confirm,
-        metadata: params.metadata,
-        automatic_payment_methods: {
-          enabled: true,
-          allow_redirects: "never"
-        }
-      }
-    ];
-
-    if (params.idempotencyKey) {
-      createOptions.push({ idempotencyKey: params.idempotencyKey });
-    }
-
-    const paymentIntent = await this.paymentIntents.create(...createOptions);
+    const amountInCents = Math.round(params.amount * 100);
+
+    const paymentIntent = await this.paymentIntents.create(
+      {
+        customer: params.customer,
+        payment_method: params.payment_method,
+        amount: amountInCents,
+        currency: params.currency,
+        confirm: params.confirm,
+        metadata: params.metadata,
+        automatic_payment_methods: {
+          enabled: true,
+          allow_redirects: "never"
+        }
+      },
+      params.idempotencyKey ? { idempotencyKey: params.idempotencyKey } : undefined
+    );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 47469f8 and af05421.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (35)
  • apps/api/drizzle/0023_sad_adam_warlock.sql (1 hunks)
  • apps/api/drizzle/0024_thankful_stick.sql (1 hunks)
  • apps/api/drizzle/meta/0023_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/0024_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/_journal.json (1 hunks)
  • apps/api/src/auth/services/auth.service.ts (2 hunks)
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts (2 hunks)
  • apps/api/src/billing/http-schemas/stripe.schema.ts (2 hunks)
  • apps/api/src/billing/http-schemas/wallet.schema.ts (0 hunks)
  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts (1 hunks)
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1 hunks)
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (2 hunks)
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2 hunks)
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (2 hunks)
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1 hunks)
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (5 hunks)
  • apps/api/src/billing/services/balances/balances.service.ts (3 hunks)
  • apps/api/src/billing/services/paying-user/paying-user.ts (1 hunks)
  • apps/api/src/billing/services/refill/refill.service.ts (1 hunks)
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (2 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (6 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2 hunks)
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts (2 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (4 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (5 hunks)
  • apps/api/src/core/repositories/base.repository.ts (2 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts (9 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.ts (3 hunks)
  • apps/api/src/core/services/tx/tx.service.ts (1 hunks)
  • apps/api/src/core/types/require.type.ts (1 hunks)
  • apps/api/test/seeders/balance.seeder.ts (2 hunks)
  • apps/api/test/seeders/database-payment-method.seeder.ts (1 hunks)
  • apps/api/test/seeders/payment-method.seeder.ts (2 hunks)
  • packages/http-sdk/src/stripe/stripe.service.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • packages/http-sdk/src/stripe/stripe.service.ts
  • apps/api/src/billing/http-schemas/wallet.schema.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • apps/api/drizzle/0024_thankful_stick.sql
  • apps/api/src/billing/services/refill/refill.service.ts
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
  • apps/api/src/billing/services/paying-user/paying-user.ts
  • apps/api/drizzle/meta/0024_snapshot.json
  • apps/api/test/seeders/balance.seeder.ts
  • apps/api/test/seeders/payment-method.seeder.ts
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts
  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/drizzle/0023_sad_adam_warlock.sql
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/core/services/tx/tx.service.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts
  • apps/api/src/billing/services/balances/balances.service.ts
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/core/types/require.type.ts
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
  • apps/api/src/auth/services/auth.service.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts
  • apps/api/src/billing/http-schemas/stripe.schema.ts
  • apps/api/test/seeders/database-payment-method.seeder.ts
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts
  • apps/api/src/core/repositories/base.repository.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test

Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

**/*.spec.{ts,tsx}: Use <Subject>.name in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references
Use either a method name or a condition starting with 'when' for nested suite descriptions in tests
Use present simple, 3rd person singular for test descriptions without prepending 'should'

Files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
🧠 Learnings (14)
📚 Learning: 2025-09-25T14:31:44.914Z
Learnt from: baktun14
Repo: akash-network/console PR: 1969
File: apps/deploy-web/src/pages/payment.tsx:179-191
Timestamp: 2025-09-25T14:31:44.914Z
Learning: The payment confirmation endpoint in apps/api/src/billing/http-schemas/stripe.schema.ts uses zod schema validation with `amount: z.number().gte(20, "Amount must be greater or equal to $20")` to ensure all payment requests meet the minimum amount requirement, preventing zero-amount or invalid payments from reaching Stripe.

Applied to files:

  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/http-schemas/stripe.schema.ts
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-10-31T11:26:42.138Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.138Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-11-28T09:17:28.821Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2274
File: apps/api/drizzle/0023_sad_adam_warlock.sql:1-1
Timestamp: 2025-11-28T09:17:28.821Z
Learning: In the Akash Network console billing system, the `user_wallets.user_id` column is required (NOT NULL). All user wallets must be associated with a user ID, and there are no anonymous wallets without a user_id in production.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
📚 Learning: 2025-11-28T09:21:45.580Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2274
File: apps/api/drizzle/0023_sad_adam_warlock.sql:1-1
Timestamp: 2025-11-28T09:21:45.580Z
Learning: In the Akash Network console, `userSettings.userId` can be NULL for anonymous users, but `user_wallets.user_id` is always required (NOT NULL). Anonymous users are identified by userId=null in the userSettings table, not in the user_wallets table.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
📚 Learning: 2025-09-29T02:40:22.090Z
Learnt from: stalniy
Repo: akash-network/console PR: 1951
File: apps/api/src/user/services/user/user.service.ts:48-63
Timestamp: 2025-09-29T02:40:22.090Z
Learning: In the Akash Network Console codebase, the users table was originally created by Sequelize and later migrated to Drizzle. Due to this migration history, the unique constraint on the userId field is named "userSetting_userId_unique" rather than following the typical "users_user_id_unique" pattern.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
📚 Learning: 2025-10-15T16:39:55.348Z
Learnt from: jzsfkzm
Repo: akash-network/console PR: 2039
File: apps/deploy-web/tests/ui/change-wallets.spec.ts:4-10
Timestamp: 2025-10-15T16:39:55.348Z
Learning: In the Akash Console E2E tests using the context-with-extension fixture, the first wallet is automatically created during fixture setup via `importWalletToLeap` in `apps/deploy-web/tests/ui/fixture/wallet-setup.ts`, so tests that call `frontPage.createWallet()` are creating a second wallet to test wallet switching functionality.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:44.790Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/no-jest-mock.mdc:0-0
Timestamp: 2025-11-25T17:45:44.790Z
Learning: Applies to **/*.spec.{ts,tsx} : Don't use `jest.mock()` in test files. Instead, use `jest-mock-extended` to create mocks and pass mocks as dependencies to the service under test

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-11-25T17:45:39.561Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/general.mdc:0-0
Timestamp: 2025-11-25T17:45:39.561Z
Learning: Applies to **/*.{ts,tsx,js} : Never use type `any` or cast to type `any`. Always define the proper TypeScript types.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:52.965Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/setup-instead-of-before-each.mdc:0-0
Timestamp: 2025-11-25T17:45:52.965Z
Learning: Applies to **/*.spec.{ts,tsx} : Use `setup` function instead of `beforeEach` in test files. The `setup` function must be at the bottom of the root `describe` block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-19T15:15:07.283Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2254
File: apps/api/test/functional/sign-and-broadcast-tx.spec.ts:4-4
Timestamp: 2025-11-19T15:15:07.283Z
Learning: In the Akash Network Console project, when tests use native Node.js fetch (available in Node 18+), fetch-mock should be used for HTTP mocking instead of nock, as nock does not support intercepting native fetch calls. This applies to apps/api/test/functional/sign-and-broadcast-tx.spec.ts and any other tests using native fetch.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:49.180Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/query-by-in-tests.mdc:0-0
Timestamp: 2025-11-25T17:45:49.180Z
Learning: Applies to {apps/deploy-web,apps/provider-console}/**/*.spec.tsx : Use `queryBy` methods instead of `getBy` methods in test expectations

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: The apps/api codebase consistently uses .toISOString() for startAfter values when scheduling jobs with JobQueueService to ensure UTC timezone consistency. This pattern is used throughout trial-started.handler.ts, trial-deployment-lease-created.handler.ts and other job scheduling code. Do not suggest changing to Date objects as ISO strings provide explicit UTC representation for database storage.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: In apps/api job scheduling with JobQueueService, use .toISOString() for startAfter values instead of Date objects to ensure UTC timezone consistency when dates are persisted to the database. Date objects can be ambiguous about timezone during serialization, while ISO strings with .toISOString() explicitly represent UTC.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
🧬 Code graph analysis (11)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (2)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • userId (87-143)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (3)
  • userId (60-77)
  • userId (79-107)
  • walletSetting (145-163)
apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (7)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (2)
  • createSetupIntent (36-43)
  • markAsDefault (46-51)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • createSetupIntent (51-57)
packages/http-sdk/src/stripe/stripe.service.ts (1)
  • createSetupIntent (23-25)
apps/api/src/core/lib/create-route/create-route.ts (1)
  • createRoute (5-11)
apps/api/src/core/services/openapi-docs/openapi-security.ts (1)
  • SECURITY_BEARER_OR_API_KEY (5-5)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethodMarkAsDefaultInputSchema (10-14)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)
  • markAsDefault (83-100)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (4)
apps/api/src/core/services/job-queue/job-queue.service.ts (2)
  • JobPayload (199-199)
  • JobMeta (208-208)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (4)
  • job (67-85)
  • wallet (226-233)
  • error (171-176)
  • user (145-169)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (1)
  • walletSetting (145-163)
apps/api/src/billing/events/wallet-balance-reload-check.ts (1)
  • WalletBalanceReloadCheck (5-15)
apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (1)
apps/api/src/core/providers/postgres.provider.ts (1)
  • ApiPgTables (80-80)
apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (2)
apps/api/src/user/model-schemas/user/user.schema.ts (1)
  • Users (8-28)
apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1)
  • UserWallets (5-18)
apps/api/src/auth/services/auth.service.ts (1)
apps/api/src/billing/services/paying-user/paying-user.ts (3)
  • PayingUser (6-6)
  • assertIsPayingUser (8-10)
  • isPayingUser (12-14)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (5)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (1)
  • walletSetting (145-163)
apps/api/test/seeders/user-wallet.seeder.ts (1)
  • UserWalletSeeder (6-30)
apps/api/test/seeders/payment-method.seeder.ts (1)
  • generatePaymentMethod (11-56)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (237-237)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (1)
apps/api/src/core/services/job-queue/job-queue.service.ts (2)
  • setup (175-182)
  • Job (189-197)
apps/api/test/seeders/database-payment-method.seeder.ts (1)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)
  • PaymentMethodOutput (11-11)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2)
apps/api/src/core/providers/postgres.provider.ts (1)
  • ApiPgTables (80-80)
apps/api/src/core/services/tx/tx.service.ts (1)
  • ApiTransaction (11-11)
apps/api/src/core/repositories/base.repository.ts (1)
apps/api/src/core/services/tx/tx.service.ts (1)
  • ApiTransaction (11-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
  • GitHub Check: Analyze (javascript-typescript)

stalniy
stalniy previously approved these changes Nov 28, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

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

21-23: Consider importing Require from core instead of redefining it.

The Require utility type is already defined at apps/api/src/core/types/require.type.ts (shown in relevant code snippets). Importing it would eliminate duplication and ensure consistency across the codebase.

Apply this diff:

+import { Require } from "@src/core/types/require.type";
+
 import { WalletBalanceReloadCheck } from "@src/billing/events/wallet-balance-reload-check";
 import type { GetBalancesResponseOutput } from "@src/billing/http-schemas/balance.schema";
 import { UserWalletOutput, WalletSettingOutput, WalletSettingRepository } from "@src/billing/repositories";
 import { BalancesService } from "@src/billing/services/balances/balances.service";
 import { PaymentMethod, StripeService } from "@src/billing/services/stripe/stripe.service";
 import { JobHandler, JobMeta, JobPayload, JobQueueService, LoggerService } from "@src/core";
 import { isPayingUser, PayingUser } from "../paying-user/paying-user";

-type Require<T, K extends keyof T> = Omit<T, K> & {
-  [P in K]-?: NonNullable<T[P]>;
-};
-
 type InitializedWallet = Require<Pick<UserWalletOutput, "address" | "isOldWallet">, "address">;

238-245: Placeholder implementation for next check date calculation.

The #calculateNextCheckDate method currently returns a fixed 1-day interval and logs "Implementation pending...". Ensure this logic is implemented before production use, as the current approach doesn't account for wallet activity patterns or dynamic scheduling needs.

Do you want me to help design a more sophisticated scheduling algorithm based on historical balance burn rates or other factors?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af05421 and 492a4ad.

📒 Files selected for processing (2)
  • apps/api/src/billing/services/stripe/stripe.service.ts (6 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
🧠 Learnings (3)
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-10-31T11:26:42.138Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.138Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-09-25T14:31:44.914Z
Learnt from: baktun14
Repo: akash-network/console PR: 1969
File: apps/deploy-web/src/pages/payment.tsx:179-191
Timestamp: 2025-09-25T14:31:44.914Z
Learning: The payment confirmation endpoint in apps/api/src/billing/http-schemas/stripe.schema.ts uses zod schema validation with `amount: z.number().gte(20, "Amount must be greater or equal to $20")` to ensure all payment requests meet the minimum amount requirement, preventing zero-amount or invalid payments from reaching Stripe.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
🧬 Code graph analysis (2)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (9)
apps/api/src/core/types/require.type.ts (1)
  • Require (1-3)
apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (1)
  • UserWalletOutput (18-22)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)
  • WalletSettingOutput (19-22)
apps/api/src/billing/services/paying-user/paying-user.ts (2)
  • PayingUser (6-6)
  • isPayingUser (12-14)
apps/api/src/billing/http-schemas/balance.schema.ts (1)
  • GetBalancesResponseOutput (17-17)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (237-237)
apps/api/src/core/services/job-queue/job-queue.service.ts (2)
  • JobPayload (199-199)
  • JobMeta (208-208)
apps/api/src/billing/events/wallet-balance-reload-check.ts (1)
  • WalletBalanceReloadCheck (5-15)
apps/api/src/billing/services/stripe/stripe.service.ts (5)
apps/api/src/billing/http-schemas/stripe.schema.ts (2)
  • PaymentMethod (237-237)
  • PaymentIntentResult (240-240)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • PaymentMethod (30-41)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • PayingUser (6-6)
apps/api/src/user/repositories/user/user.repository.ts (1)
  • UserOutput (10-12)
apps/api/src/core/services/tx/tx.service.ts (1)
  • WithTransaction (39-51)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (5)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2)

87-143: LGTM!

The validation logic is comprehensive and correctly handles all edge cases with appropriate error events. The use of type guards like isPayingUser ensures proper type narrowing for downstream operations.


178-213: LGTM!

The reload logic correctly triggers when the balance drops to or below the threshold. The use of job.id in the idempotency key ensures Stripe won't create duplicate payment intents if the job is retried, addressing the transaction consistency concern from past reviews.

apps/api/src/billing/services/stripe/stripe.service.ts (3)

122-138: LGTM!

The getDefaultPaymentMethod implementation correctly retrieves and validates payment method data from both local database and Stripe, with appropriate authorization checks via CASL abilities. The error messages with source attribution aid debugging.


147-174: Transaction + external API pattern acceptable per past reviews.

The method now calls markRemotePaymentMethodAsDefault (Stripe update) after local DB operations complete (lines 157, 171), addressing the transaction consistency concern from past reviews. The 3-second timeouts help limit DB connection hold time, though this remains a trade-off between consistency and performance as noted in previous discussions.


186-239: LGTM!

The createPaymentIntent method correctly handles the optional idempotencyKey parameter by conditionally adding it to the Stripe API call's options parameter. The use of Parameters utility type maintains type safety while allowing flexible argument construction.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/api/drizzle.config.ts (1)

5-9: Good addition of pre-flight validation.

The runtime check ensures POSTGRES_DB_URI is set before the config is used, preventing downstream errors.

Optionally, you could make the error message more helpful:

 if (!POSTGRES_DB_URI) {
-  throw new Error("POSTGRES_DB_URI must be set");
+  throw new Error("POSTGRES_DB_URI environment variable must be set. Please check your .env file or environment configuration.");
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 492a4ad and 5d2f34f.

📒 Files selected for processing (5)
  • apps/api/drizzle.config.ts (1 hunks)
  • apps/api/drizzle/0023_steady_leech.sql (1 hunks)
  • apps/api/drizzle/meta/0023_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/_journal.json (1 hunks)
  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/drizzle.config.ts
🧠 Learnings (1)
📚 Learning: 2025-11-28T09:17:28.821Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2274
File: apps/api/drizzle/0023_sad_adam_warlock.sql:1-1
Timestamp: 2025-11-28T09:17:28.821Z
Learning: In the Akash Network console billing system, the `user_wallets.user_id` column is required (NOT NULL). All user wallets must be associated with a user ID, and there are no anonymous wallets without a user_id in production.

Applied to files:

  • apps/api/drizzle/0023_steady_leech.sql
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
apps/api/drizzle.config.ts (1)

16-16: LGTM!

Using the environment variable directly is appropriate for the Drizzle configuration file and simplifies the dependency chain.

apps/api/drizzle/meta/_journal.json (1)

166-172: LGTM!

The migration journal entry is properly formatted and correctly tracks the new migration.

apps/api/drizzle/0023_steady_leech.sql (3)

1-1: LGTM!

The NOT NULL constraint on user_wallets.user_id is correct. Based on learnings, all user wallets must be associated with a user ID in the Akash Network console billing system.


2-2: LGTM!

Adding the is_default column with a default value of false and NOT NULL constraint is appropriate for tracking default payment methods per user.


3-3: Excellent use of partial unique index.

The partial unique index payment_methods_user_id_is_default_unique on (user_id, is_default) WHERE is_default = true is the correct pattern for ensuring each user can have at most one default payment method. This prevents data integrity issues while allowing multiple non-default payment methods.

apps/api/drizzle/meta/0023_snapshot.json (1)

1-820: LGTM!

The schema snapshot is consistent with the migration and correctly reflects the database state after applying migration 0023_steady_leech.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
apps/api/src/billing/routes/stripe-webhook/stripe-webhook.router.ts (1)

44-52: Consider explicit error handling for webhook processing.

The webhook handler has no try-catch around CheckoutController.webhook(). If the webhook processing throws an error, it will result in a 500 response, which may trigger Stripe to retry the webhook. If this is intentional retry behavior, consider documenting it. Otherwise, add error handling to log failures and return appropriate status codes.

Additionally, the as never cast on line 51 suggests a type mismatch with Hono's response types. Consider investigating the underlying type issue rather than using a type assertion.

 stripeWebhook.openapi(route, async function routeStripeWebhook(c) {
   const sig = c.req.header("stripe-signature");
   if (!sig) {
     return c.json({ error: "Stripe signature is required" }, 400);
   }
 
+  try {
     await container.resolve(CheckoutController).webhook(sig, await c.req.text());
-    return c.text("", 200) as never;
+    return c.text("", 200);
+  } catch (error) {
+    // Log error and let Stripe retry
+    console.error("Webhook processing failed:", error);
+    throw error;
+  }
 });
apps/api/src/billing/controllers/stripe/stripe.controller.ts (1)

58-68: Clarify the 404 error message.

Line 63 returns a 404 with the message "Default payment method not found" when currentUser doesn't exist. However, this message is misleading because:

  • The check is for user existence/configuration, not whether a default payment method exists
  • A more accurate message would be "Payment account not configured" or "User not found"

The actual "payment method not found" case would occur inside stripe.getDefaultPaymentMethod() if the user exists but has no default payment method.

-    assert(currentUser, 404, "Default payment method not found");
+    assert(currentUser, 404, "Payment account not configured");
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d2f34f and 54f2e22.

📒 Files selected for processing (4)
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts (2 hunks)
  • apps/api/src/billing/http-schemas/stripe.schema.ts (3 hunks)
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (5 hunks)
  • apps/api/src/billing/routes/stripe-webhook/stripe-webhook.router.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/billing/http-schemas/stripe.schema.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/billing/routes/stripe-webhook/stripe-webhook.router.ts
🧠 Learnings (3)
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
📚 Learning: 2025-10-31T11:26:42.138Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.138Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
📚 Learning: 2025-09-25T14:31:44.914Z
Learnt from: baktun14
Repo: akash-network/console PR: 1969
File: apps/deploy-web/src/pages/payment.tsx:179-191
Timestamp: 2025-09-25T14:31:44.914Z
Learning: The payment confirmation endpoint in apps/api/src/billing/http-schemas/stripe.schema.ts uses zod schema validation with `amount: z.number().gte(20, "Amount must be greater or equal to $20")` to ensure all payment requests meet the minimum amount requirement, preventing zero-amount or invalid payments from reaching Stripe.

Applied to files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
🧬 Code graph analysis (1)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (3)
apps/api/src/auth/services/auth.service.ts (1)
  • Protected (62-78)
apps/api/src/billing/http-schemas/stripe.schema.ts (5)
  • PaymentMethodMarkAsDefaultInput (240-240)
  • PaymentMethodResponse (243-243)
  • PaymentMethodsResponse (242-242)
  • ConfirmPaymentRequest (244-244)
  • ConfirmPaymentResponse (247-247)
apps/api/src/core/lib/semaphore.decorator.ts (1)
  • Semaphore (3-28)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: codecov/patch/api
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (4)
apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (1)

5-6: LGTM! Default payment method routes are well-structured.

The new routes for managing default payment methods follow the existing patterns in the codebase:

  • Proper OpenAPI documentation with request/response schemas
  • Schema validation via PaymentMethodMarkAsDefaultInputSchema and PaymentMethodResponseSchema
  • Appropriate security decorators (SECURITY_BEARER_OR_API_KEY)
  • Clean delegation to StripeController methods

Also applies to: 45-99

apps/api/src/billing/controllers/stripe/stripe.controller.ts (3)

70-80: LGTM! Graceful handling of optional payment user.

The method correctly uses {strict: false} and returns an empty array when no paying user is configured, providing a clean experience for users who haven't set up payments yet.


84-125: Excellent! Critical security bug has been fixed.

Line 90 now correctly awaits this.stripe.hasPaymentMethod() before passing the result to assert(). This fixes the critical security vulnerability flagged in previous reviews where the Promise object (always truthy) was being checked instead of its resolved boolean value.

The ownership check now properly validates that the payment method belongs to the user before processing the payment.

Confirmed fix from previous review feedback


50-56: Based on my verification, I can now provide the definitive assessment:

The inconsistency is intentional and not an oversight.

Here's what I discovered:

  1. getCurrentPayingUser() behavior:

    • Default (no options): strict: true — enforces PayingUser via assertIsPayingUser() or throws
    • With { strict: false }: Returns undefined gracefully if user isn't a paying user
  2. Pattern in the controller:

    • Read operations (getDefaultPaymentMethod, getPaymentMethods): Use strict: false for defensive, graceful handling
    • Create operation (confirmPayment): Uses strict: false but then asserts afterward
    • Update operation (markAsDefault): Uses strict mode (default) — requires a fully configured paying user
  3. Business logic rationale:

    • Marking a payment method as default is a write operation that modifies account configuration
    • It's more restrictive than read operations, intentionally requiring a fully configured paying user
    • This is a deliberate design choice, not an oversight

The original review comment assumes an inconsistency that doesn't exist. The code is correct as-is.

@ygrishajev ygrishajev force-pushed the feature/auto-reload-handler branch from 54f2e22 to b4ba140 Compare November 28, 2025 16:09
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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/http-schemas/stripe.schema.ts (1)

16-55: Missing id field in PaymentMethodSchema.

The PaymentMethodResponseSchema wraps PaymentMethodSchema, but PaymentMethodSchema lacks an id field. The relevant type definitions in packages/http-sdk/src/stripe/stripe.types.ts (lines 29-40) show that PaymentMethod includes an id: string field. The response schema should include this to properly identify the returned payment method.

 export const PaymentMethodSchema = z.object({
+  id: z.string(),
   type: z.string(),
   validated: z.boolean().optional(),
   card: z

Also applies to: 61-63

🧹 Nitpick comments (1)
apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (1)

67-70: Consider using 204 No Content for void operations.

Returning c.json(undefined, 200) is semantically unusual. For operations that succeed without returning data, HTTP 204 No Content is the standard practice.

-stripePaymentMethodsRouter.openapi(markAsDefaultRoute, async function markAsDefault(c) {
-  await container.resolve(StripeController).markAsDefault(c.req.valid("json"));
-  return c.json(undefined, 200);
-});
+stripePaymentMethodsRouter.openapi(markAsDefaultRoute, async function markAsDefault(c) {
+  await container.resolve(StripeController).markAsDefault(c.req.valid("json"));
+  return c.body(null, 204);
+});

This would also require updating the route definition to use 204 instead of 200 in the responses.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54f2e22 and b4ba140.

📒 Files selected for processing (4)
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts (2 hunks)
  • apps/api/src/billing/http-schemas/stripe.schema.ts (3 hunks)
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (5 hunks)
  • apps/api/src/billing/routes/stripe-webhook/stripe-webhook.router.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/billing/routes/stripe-webhook/stripe-webhook.router.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/http-schemas/stripe.schema.ts
🧠 Learnings (3)
📚 Learning: 2025-09-25T14:31:44.914Z
Learnt from: baktun14
Repo: akash-network/console PR: 1969
File: apps/deploy-web/src/pages/payment.tsx:179-191
Timestamp: 2025-09-25T14:31:44.914Z
Learning: The payment confirmation endpoint in apps/api/src/billing/http-schemas/stripe.schema.ts uses zod schema validation with `amount: z.number().gte(20, "Amount must be greater or equal to $20")` to ensure all payment requests meet the minimum amount requirement, preventing zero-amount or invalid payments from reaching Stripe.

Applied to files:

  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/http-schemas/stripe.schema.ts
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
📚 Learning: 2025-10-31T11:26:42.138Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.138Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
🧬 Code graph analysis (2)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (3)
apps/api/src/auth/services/auth.service.ts (1)
  • Protected (62-78)
apps/api/src/billing/http-schemas/stripe.schema.ts (4)
  • PaymentMethodMarkAsDefaultInput (240-240)
  • PaymentMethodResponse (243-243)
  • PaymentMethodsResponse (242-242)
  • ConfirmPaymentRequest (244-244)
apps/api/src/core/lib/semaphore.decorator.ts (1)
  • Semaphore (3-28)
apps/api/src/billing/http-schemas/stripe.schema.ts (2)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • PaymentMethod (30-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Validate local packages
🔇 Additional comments (5)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)

10-14: LGTM!

The schema follows the established pattern of wrapping data in a data object, consistent with other request schemas in this file.

apps/api/src/billing/controllers/stripe/stripe.controller.ts (3)

50-56: LGTM!

The method correctly uses the @Protected decorator with appropriate action/subject, retrieves the paying user, and delegates the authorization check to the service layer via the ability parameter.


67-77: LGTM!

Good defensive pattern: using { strict: false } and returning an empty array when no paying user exists ensures graceful handling without errors for unauthenticated or non-paying users.


82-87: LGTM! Previous security issue addressed.

The ownership check now correctly awaits the hasPaymentMethod promise before asserting, fixing the critical security bug from the previous review.

apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (1)

90-99: 404 handling for missing default payment method is working correctly via global error middleware.

The underlying StripeService.getDefaultPaymentMethod() (lines 134-135 in stripe.service.ts) properly throws 404 errors using assert when no default payment method exists in either the database or Stripe. These exceptions are caught and converted to appropriate HTTP responses by the global error handler registered at line 184 of rest-app.ts (appHono.onError(container.resolve(HonoErrorHandlerService).handle)). The handler's unconditional 200 return is safe because exceptions from the service layer bypass it.

While the error handling works correctly, the pattern of implicit error handling via middleware is not obvious from reading the route handler alone. Consider adding explicit error handling or documentation if clarity is desired.

@ygrishajev ygrishajev force-pushed the feature/auto-reload-handler branch 2 times, most recently from 95de779 to c322b6c Compare December 2, 2025 13:56
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (3)
apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts (1)

21-21: Remove jest.mock() usage per coding guidelines.

The coding guidelines explicitly prohibit using jest.mock() in test files. The logger mock is already imported on line 1 via @test/mocks/logger-service.mock. If additional mocking is needed, inject mocks as dependencies using jest-mock-extended.

Based on learnings and coding guidelines.

-jest.mock("@akashnetwork/logging");
apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts (1)

53-63: Add error handling to prevent orphaned records.

The initialize method lacks error handling that could lead to orphaned database records. If walletManager.createWallet (line 55) fails after userWalletRepository.create (line 54) succeeds, a user wallet record will exist without an address.

This is inconsistent with initializeAndGrantTrialLimits (lines 27-42), which includes proper rollback logic (line 40).

Apply this diff to add error handling:

 async initialize(userId: string) {
   const { id } = await this.userWalletRepository.create({ userId });
-  const wallet = await this.walletManager.createWallet({ addressIndex: id, useOldWallet: false });
-  return await this.userWalletRepository.updateById(
-    id,
-    {
-      address: wallet.address
-    },
-    { returning: true }
-  );
+  try {
+    const wallet = await this.walletManager.createWallet({ addressIndex: id, useOldWallet: false });
+    return await this.userWalletRepository.updateById(
+      id,
+      {
+        address: wallet.address
+      },
+      { returning: true }
+    );
+  } catch (error) {
+    await this.userWalletRepository.deleteById(id);
+    throw error;
+  }
 }
apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (1)

173-178: Consider removing fingerprint from detach logs for consistency.

A past review flagged fingerprint logging as sensitive in the attach handler. For consistency, consider removing or hashing the fingerprint from these detach logs as well, since it's a stable card identifier that could leak payment metadata.

     this.logger.info({
       event: "PAYMENT_METHOD_DETACHED",
       paymentMethodId: paymentMethod.id,
-      customerId,
-      fingerprint
+      customerId
     });

Also applies to line 210 where fingerprint is logged again.

♻️ Duplicate comments (3)
apps/api/src/core/services/job-queue/job-queue.service.ts (1)

89-94: Full job payload still logged - PII concern persists.

The JOB_ENQUEUED log still includes the full job object. As flagged in a past review, this risks logging sensitive data (e.g., email addresses, message content) for jobs like notifications.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (1)

49-49: as any cast persists - consider using proper types.

The as any casts on mock return values violate the coding guideline "Never use type any or cast to type any". Consider typing generateWalletSetting to return the expected output type, or using a type assertion to the concrete WalletSettingOutput type instead.

- walletSettingRepository.updateById.mockResolvedValue(updatedSetting as any);
+ walletSettingRepository.updateById.mockResolvedValue(updatedSetting as WalletSettingOutput);

Also applies to lines 110, 181, 216.

apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)

102-116: Potential race condition still exists in createAsDefault.

As noted in the previous review, using Promise.all to run create and #unmarkAsDefaultExcluding concurrently can still lead to multiple default payment methods if two createAsDefault calls execute in parallel for the same user. The transaction ensures atomicity within a single call, but doesn't prevent interleaving between concurrent calls.

Consider:

  1. Adding a unique partial index on (userId, isDefault) WHERE isDefault = true at the database level (if not already present)
  2. Or sequencing the operations: unmark first, then create
   async createAsDefault(input: Omit<PaymentMethodInput, "id" | "isDefault">) {
     return this.ensureTransaction(async tx => {
       const id = uuidv4();
-      const [output] = await Promise.all([
-        this.create({
-          ...input,
-          isDefault: true,
-          id
-        }),
-        this.#unmarkAsDefaultExcluding(id, tx)
-      ]);
+      await this.#unmarkAsDefaultExcluding(id, tx);
+      const output = await this.create({
+        ...input,
+        isDefault: true,
+        id
+      });

       return this.toOutput(output);
     });
   }
🧹 Nitpick comments (9)
apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts (1)

200-222: Verify the block calculation in the comment.

The comment states "~168 hours = ~1100800 blocks" which implies ~6552 blocks/hour. This seems inconsistent with typical Akash block rates (~580 blocks/hour based on ~6.2 second block time). The test uses toBeCloseTo with tolerance, so it likely passes, but the comment may confuse future readers.

Consider updating the comment to reflect actual expected values or simplify by removing the intermediate calculations.

apps/api/test/seeders/balance.seeder.ts (1)

21-34: Helper implementation is sound; consider determinism only if tests require it

The construction of balance, deployments, and derived total with override support is logically correct and type-safe. If you ever hit flaky tests around balances, you might switch from random faker values to fixed defaults, but as a generic seeder this is fine as-is.

apps/api/src/billing/services/balances/balances.service.ts (1)

112-121: Minor: consider parallelizing and scoping fiat helpers

The fiat helpers are clear and readable; two small polish ideas you can apply later if desired:

  • Run the two conversions in parallel to shave a round‑trip when StatsService hits external data:
const [balance, deployments] = await Promise.all([
  this.toFiatAmount(data.balance),
  this.toFiatAmount(data.deployments)
]);
const total = this.ensure2floatingDigits(balance + deployments);
  • If ensure2floatingDigits is not intended for external callers, marking it private would better express its helper nature.

These are non‑blocking cleanups.

Also applies to: 123-134

apps/api/src/billing/services/wallet-balance-reload-check/README.md (1)

53-53: Add fenced‑code languages to satisfy markdownlint (MD040)

The three fenced blocks for the sequence diagram and formula examples currently use bare ``` fences, which triggers MD040. Consider tagging them with a language, e.g.:

```text
...sequence diagram...
balance < 0.25 * costUntilTargetDateInFiat
reloadAmount = max(costUntilTargetDateInFiat - balance, $20)

This keeps the existing formatting while making markdownlint happy.

Also applies to: 104-104, 150-150

apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (1)

5-12: Default payment‑method routes are well‑structured; consider 204 for mark‑as‑default

The new POST/GET /v1/stripe/payment-methods/default routes are wired cleanly to the controller and schemas, and the 204 behavior on delete matches the description.

For markAsDefault, you currently return c.json(undefined, 200) while the OpenAPI response has no content schema. Consider returning a 204 with an empty body (to mirror the delete route) or, alternatively, defining a minimal success payload in the OpenAPI schema so the status/body contract is explicit. Not blocking, but it would tighten the API surface.

Also applies to: 45-70, 72-99, 127-148

apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)

48-65: New relation-loading method looks correct.

The findInternalByUserIdWithRelations method correctly fetches wallet settings with related wallet and user data. The implementation properly applies access control via whereAccessibleBy and returns the raw relation-loaded object for the handler to destructure.

Consider adding an explicit return type for better type safety and documentation:

- async findInternalByUserIdWithRelations(userId: WalletSettingOutput["userId"]) {
+ async findInternalByUserIdWithRelations(userId: WalletSettingOutput["userId"]): Promise<
+   | (DbWalletSettingOutput & {
+       wallet: { address: string | null; isOldWallet: boolean | null };
+       user: ApiPgTables["User"]["$inferSelect"];
+     })
+   | undefined
+ > {
apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts (1)

56-58: Unused userId parameter in cancel method.

The userId parameter is never used in the method body. Consider removing it or documenting why it's reserved for future use.

-  async cancel(userId: string, jobId: string): Promise<void> {
+  async cancel(jobId: string): Promise<void> {
     await this.jobQueueService.cancel(WalletBalanceReloadCheck.name, jobId);
   }

Note: If callers depend on the current signature, verify them before changing.

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

23-25: Duplicate type definition.

The Require type is already exported from @src/core/types/require.type.ts. Consider importing it instead of redefining.

+import { Require } from "@src/core/types/require.type";
 import { isPayingUser, PayingUser } from "../paying-user/paying-user";

-type Require<T, K extends keyof T> = Omit<T, K> & {
-  [P in K]-?: NonNullable<T[P]>;
-};
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (1)

12-16: Consider renaming to avoid type name collision with repository.

A WalletSettingInput type also exists in wallet-settings.repository.ts with a different structure. While they serve different purposes (service API vs. database operations), the name collision could cause confusion during maintenance or when reviewing type errors.

Consider renaming the local interface to clarify its scope:

-export interface WalletSettingInput {
+export interface WalletSettingUpdateInput {
   autoReloadEnabled?: boolean;
   autoReloadThreshold?: number;
   autoReloadAmount?: number;
 }

Then update the method signatures accordingly:

async upsertWalletSetting(userId: UserOutput["id"], input: WalletSettingUpdateInput): Promise<...>
async #update(userId: UserOutput["id"], settings: WalletSettingUpdateInput): Promise<...>
async #create(userId: UserOutput["id"], settings: WalletSettingUpdateInput): Promise<...>
async #validate({ prev, next, userId }: { next: WalletSettingUpdateInput; prev?: WalletSettingOutput; userId: string })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b4ba140 and c322b6c.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (44)
  • apps/api/drizzle.config.ts (1 hunks)
  • apps/api/drizzle/0023_clumsy_vertigo.sql (1 hunks)
  • apps/api/drizzle/meta/0023_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/_journal.json (1 hunks)
  • apps/api/src/auth/services/ability/ability.service.ts (2 hunks)
  • apps/api/src/auth/services/auth.service.ts (2 hunks)
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts (2 hunks)
  • apps/api/src/billing/http-schemas/stripe.schema.ts (3 hunks)
  • apps/api/src/billing/http-schemas/wallet.schema.ts (0 hunks)
  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts (1 hunks)
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1 hunks)
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (2 hunks)
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2 hunks)
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (2 hunks)
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1 hunks)
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (5 hunks)
  • apps/api/src/billing/routes/stripe-webhook/stripe-webhook.router.ts (1 hunks)
  • apps/api/src/billing/services/balances/balances.service.ts (3 hunks)
  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts (6 hunks)
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts (4 hunks)
  • apps/api/src/billing/services/paying-user/paying-user.ts (1 hunks)
  • apps/api/src/billing/services/refill/refill.service.ts (1 hunks)
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (2 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (6 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/README.md (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2 hunks)
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts (2 hunks)
  • apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts (1 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (11 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (6 hunks)
  • apps/api/src/core/repositories/base.repository.ts (2 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts (9 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.ts (4 hunks)
  • apps/api/src/core/services/tx/tx.service.ts (1 hunks)
  • apps/api/src/core/types/require.type.ts (1 hunks)
  • apps/api/src/dashboard/services/stats/stats.service.ts (1 hunks)
  • apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts (2 hunks)
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts (7 hunks)
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.ts (1 hunks)
  • apps/api/test/seeders/balance.seeder.ts (2 hunks)
  • apps/api/test/seeders/database-payment-method.seeder.ts (1 hunks)
  • apps/api/test/seeders/payment-method.seeder.ts (2 hunks)
  • packages/http-sdk/src/stripe/stripe.service.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • packages/http-sdk/src/stripe/stripe.service.ts
  • apps/api/src/billing/http-schemas/wallet.schema.ts
🚧 Files skipped from review as they are similar to previous changes (17)
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts
  • apps/api/src/core/types/require.type.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/core/repositories/base.repository.ts
  • apps/api/src/billing/services/refill/refill.service.ts
  • apps/api/test/seeders/database-payment-method.seeder.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.ts
  • apps/api/src/dashboard/services/stats/stats.service.ts
  • apps/api/test/seeders/payment-method.seeder.ts
  • apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
  • apps/api/src/auth/services/auth.service.ts
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/billing/routes/stripe-webhook/stripe-webhook.router.ts
  • apps/api/drizzle/meta/0023_snapshot.json
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/drizzle.config.ts
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
  • apps/api/test/seeders/balance.seeder.ts
  • apps/api/src/billing/services/paying-user/paying-user.ts
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts
  • apps/api/src/billing/services/balances/balances.service.ts
  • apps/api/src/billing/http-schemas/stripe.schema.ts
  • apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts
  • apps/api/src/core/services/tx/tx.service.ts
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
  • apps/api/src/auth/services/ability/ability.service.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts
  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test

Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

**/*.spec.{ts,tsx}: Use <Subject>.name in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references
Use either a method name or a condition starting with 'when' for nested suite descriptions in tests
Use present simple, 3rd person singular for test descriptions without prepending 'should'

Files:

  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
🧠 Learnings (16)
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-11-25T17:45:44.790Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/no-jest-mock.mdc:0-0
Timestamp: 2025-11-25T17:45:44.790Z
Learning: Applies to **/*.spec.{ts,tsx} : Don't use `jest.mock()` in test files. Instead, use `jest-mock-extended` to create mocks and pass mocks as dependencies to the service under test

Applied to files:

  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:52.965Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/setup-instead-of-before-each.mdc:0-0
Timestamp: 2025-11-25T17:45:52.965Z
Learning: Applies to **/*.spec.{ts,tsx} : Use `setup` function instead of `beforeEach` in test files. The `setup` function must be at the bottom of the root `describe` block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

Applied to files:

  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:58.258Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/test-descriptions.mdc:0-0
Timestamp: 2025-11-25T17:45:58.258Z
Learning: Applies to **/*.spec.{ts,tsx} : Use `<Subject>.name` in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references

Applied to files:

  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
📚 Learning: 2025-11-19T15:15:07.283Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2254
File: apps/api/test/functional/sign-and-broadcast-tx.spec.ts:4-4
Timestamp: 2025-11-19T15:15:07.283Z
Learning: In the Akash Network Console project, when tests use native Node.js fetch (available in Node 18+), fetch-mock should be used for HTTP mocking instead of nock, as nock does not support intercepting native fetch calls. This applies to apps/api/test/functional/sign-and-broadcast-tx.spec.ts and any other tests using native fetch.

Applied to files:

  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-09-25T14:31:44.914Z
Learnt from: baktun14
Repo: akash-network/console PR: 1969
File: apps/deploy-web/src/pages/payment.tsx:179-191
Timestamp: 2025-09-25T14:31:44.914Z
Learning: The payment confirmation endpoint in apps/api/src/billing/http-schemas/stripe.schema.ts uses zod schema validation with `amount: z.number().gte(20, "Amount must be greater or equal to $20")` to ensure all payment requests meet the minimum amount requirement, preventing zero-amount or invalid payments from reaching Stripe.

Applied to files:

  • apps/api/src/billing/http-schemas/stripe.schema.ts
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
📚 Learning: 2025-09-04T04:27:50.638Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/billing/services/managed-signer/managed-signer.service.ts:1-1
Timestamp: 2025-09-04T04:27:50.638Z
Learning: In the akash-network/console project, importing MsgCreateLease from "akashnetwork/akash-api/v1beta3" instead of v1beta4 is considered non-critical by the maintainers, likely due to backward compatibility or project-specific requirements.

Applied to files:

  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
📚 Learning: 2025-09-04T04:27:40.858Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/billing/services/managed-signer/managed-signer.service.ts:98-107
Timestamp: 2025-09-04T04:27:40.858Z
Learning: In the Akash protocol, when processing MsgCreateLease messages, the bidId.dseq field is always required and guaranteed to be present at runtime, even though TypeScript types may suggest it could be undefined. The non-null assertion operator (!) is appropriate to use in this context.

Applied to files:

  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
📚 Learning: 2025-09-29T02:40:22.090Z
Learnt from: stalniy
Repo: akash-network/console PR: 1951
File: apps/api/src/user/services/user/user.service.ts:48-63
Timestamp: 2025-09-29T02:40:22.090Z
Learning: In the Akash Network Console codebase, the users table was originally created by Sequelize and later migrated to Drizzle. Due to this migration history, the unique constraint on the userId field is named "userSetting_userId_unique" rather than following the typical "users_user_id_unique" pattern.

Applied to files:

  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
📚 Learning: 2025-10-31T11:26:42.138Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.138Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-11-25T17:45:39.561Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/general.mdc:0-0
Timestamp: 2025-11-25T17:45:39.561Z
Learning: Applies to **/*.{ts,tsx,js} : Never use type `any` or cast to type `any`. Always define the proper TypeScript types.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:49.180Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/query-by-in-tests.mdc:0-0
Timestamp: 2025-11-25T17:45:49.180Z
Learning: Applies to {apps/deploy-web,apps/provider-console}/**/*.spec.tsx : Use `queryBy` methods instead of `getBy` methods in test expectations

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-10-15T16:39:55.348Z
Learnt from: jzsfkzm
Repo: akash-network/console PR: 2039
File: apps/deploy-web/tests/ui/change-wallets.spec.ts:4-10
Timestamp: 2025-10-15T16:39:55.348Z
Learning: In the Akash Console E2E tests using the context-with-extension fixture, the first wallet is automatically created during fixture setup via `importWalletToLeap` in `apps/deploy-web/tests/ui/fixture/wallet-setup.ts`, so tests that call `frontPage.createWallet()` are creating a second wallet to test wallet switching functionality.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: In apps/api job scheduling with JobQueueService, use .toISOString() for startAfter values instead of Date objects to ensure UTC timezone consistency when dates are persisted to the database. Date objects can be ambiguous about timezone during serialization, while ISO strings with .toISOString() explicitly represent UTC.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: The apps/api codebase consistently uses .toISOString() for startAfter values when scheduling jobs with JobQueueService to ensure UTC timezone consistency. This pattern is used throughout trial-started.handler.ts, trial-deployment-lease-created.handler.ts and other job scheduling code. Do not suggest changing to Date objects as ISO strings provide explicit UTC representation for database storage.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.ts
📚 Learning: 2025-11-28T09:17:28.832Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2274
File: apps/api/drizzle/0023_sad_adam_warlock.sql:1-1
Timestamp: 2025-11-28T09:17:28.832Z
Learning: In the Akash Network console billing system, the `user_wallets.user_id` column is required (NOT NULL). All user wallets must be associated with a user ID, and there are no anonymous wallets without a user_id in production.

Applied to files:

  • apps/api/drizzle/0023_clumsy_vertigo.sql
🧬 Code graph analysis (13)
apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (2)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • user (142-166)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • assertIsPayingUser (8-10)
apps/api/test/seeders/balance.seeder.ts (2)
apps/api/src/billing/http-schemas/balance.schema.ts (1)
  • GetBalancesResponseOutput (17-17)
apps/deploy-web/src/utils/apiUtils.ts (1)
  • balance (44-46)
apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts (4)
apps/api/test/seeders/user-wallet.seeder.ts (1)
  • UserWalletSeeder (6-30)
apps/api/test/seeders/draining-deployment.seeder.ts (1)
  • DrainingDeploymentSeeder (7-25)
apps/api/test/seeders/auto-top-up-deployment.seeder.ts (1)
  • AutoTopUpDeploymentSeeder (6-21)
apps/api/test/mocks/config-service.mock.ts (1)
  • mockConfigService (16-25)
apps/api/src/billing/http-schemas/stripe.schema.ts (2)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • PaymentMethod (30-41)
apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (4)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (3)
  • createSetupIntent (41-48)
  • markAsDefault (51-56)
  • getDefaultPaymentMethod (59-65)
apps/api/src/billing/services/stripe/stripe.service.ts (2)
  • createSetupIntent (51-57)
  • getDefaultPaymentMethod (122-138)
packages/http-sdk/src/stripe/stripe.service.ts (1)
  • createSetupIntent (23-25)
apps/api/src/billing/http-schemas/stripe.schema.ts (2)
  • PaymentMethodMarkAsDefaultInputSchema (10-14)
  • PaymentMethodResponseSchema (61-63)
apps/api/src/billing/services/managed-signer/managed-signer.service.ts (2)
apps/stats-web/src/components/transactions/akash/MsgCreateDeployment.tsx (1)
  • MsgCreateDeployment (12-29)
apps/stats-web/src/components/transactions/akash/MsgAccountDeposit.tsx (1)
  • MsgAccountDeposit (10-19)
apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (2)
apps/api/src/user/model-schemas/user/user.schema.ts (1)
  • Users (8-28)
apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1)
  • UserWallets (5-18)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (8)
apps/api/src/core/types/require.type.ts (1)
  • Require (1-3)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)
  • WalletSettingOutput (19-22)
apps/api/src/billing/services/paying-user/paying-user.ts (2)
  • PayingUser (6-6)
  • isPayingUser (12-14)
apps/api/src/billing/http-schemas/balance.schema.ts (1)
  • GetBalancesResponseOutput (17-17)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (241-241)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
apps/api/src/core/services/job-queue/job-queue.service.ts (2)
  • JobPayload (209-209)
  • JobMeta (218-218)
apps/api/src/billing/events/wallet-balance-reload-check.ts (1)
  • WalletBalanceReloadCheck (5-15)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (3)
apps/api/src/auth/services/auth.service.ts (1)
  • Protected (62-78)
apps/api/src/billing/http-schemas/stripe.schema.ts (3)
  • PaymentMethodMarkAsDefaultInput (240-240)
  • PaymentMethodResponse (243-243)
  • PaymentMethodsResponse (242-242)
apps/api/src/core/lib/semaphore.decorator.ts (1)
  • Semaphore (3-28)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (3)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (2)
  • WalletSettingInput (12-17)
  • WalletSettingOutput (19-22)
apps/api/src/auth/services/auth.service.ts (2)
  • ability (44-46)
  • ability (48-50)
apps/api/src/core/repositories/base.repository.ts (1)
  • isUniqueViolation (237-239)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2)
apps/api/src/core/providers/postgres.provider.ts (1)
  • ApiPgTables (80-80)
apps/api/src/core/services/tx/tx.service.ts (1)
  • ApiTransaction (11-11)
apps/api/src/billing/services/stripe/stripe.service.ts (4)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • PaymentMethod (30-41)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • PayingUser (6-6)
apps/api/src/user/repositories/user/user.repository.ts (1)
  • UserOutput (10-12)
apps/api/src/core/services/tx/tx.service.ts (1)
  • WithTransaction (39-51)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (2)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • userId (97-140)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (2)
  • userId (58-75)
  • userId (77-105)
🪛 LanguageTool
apps/api/src/billing/services/wallet-balance-reload-check/README.md

[style] ~39-~39: ‘emergency situations’ might be wordy. Consider a shorter alternative.
Context: ... margin before funds run out - Prevents emergency situations ## When Does the Check Run? The handl...

(EN_WORDINESS_PREMIUM_EMERGENCY_SITUATIONS)

🪛 markdownlint-cli2 (0.18.1)
apps/api/src/billing/services/wallet-balance-reload-check/README.md

53-53: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


104-104: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


150-150: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: validate / validate-app
🔇 Additional comments (50)
apps/api/src/auth/services/ability/ability.service.ts (1)

30-30: LGTM! Payment method permissions properly scoped.

The addition of "manage" permissions for PaymentMethod to both REGULAR_USER and REGULAR_PAYING_USER roles is correct. The userId condition ensures users can only manage their own payment methods, following the same secure pattern used for WalletSetting, Alert, and NotificationChannel. Both roles appropriately need this access for payment method setup and management.

Also applies to: 42-42

apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts (5)

32-108: Well-structured test with comprehensive assertions.

The test properly validates pagination behavior, marks closed deployments, and filters out closed ones from callback results. The setup pattern and mock usage follow project conventions.


129-141: LGTM!

Tests are concise, focused, and follow naming conventions.


143-193: LGTM!

Good coverage of success and failure paths. The use of jest.spyOn to mock calculateTopUpAmount appropriately isolates the unit under test.


302-343: LGTM!

The setupCalculateCost helper is well-designed. The "userWallet" in input check correctly distinguishes between a missing key (use default) and an explicit undefined value (test the undefined case).


346-373: LGTM!

The setup function follows all coding guidelines: positioned at the bottom of the root describe block, accepts a single parameter with inline type definition, avoids shared state, and returns all dependencies for test assertions.

apps/api/test/seeders/balance.seeder.ts (1)

5-6: Importing response type for the seeder looks good

Using GetBalancesResponseOutput["data"] here keeps the test helper in lockstep with the HTTP schema type, avoiding drift if the response shape changes. No issues.

apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts (3)

5-5: LGTM: Import simplification is appropriate.

Removing the UserWalletInput import aligns with the method signature change and reduces unnecessary dependencies.


53-53: LGTM: Method signature simplification improves consistency.

Changing the parameter type from UserWalletInput["userId"] to string improves consistency with initializeAndGrantTrialLimits (line 21) and simplifies the API surface.


53-63: Authorization is properly enforced through the call chain.

The initialize() method doesn't require .accessibleBy() because it's an internal service method called exclusively through authorized paths:

  • Via StripeController.applyCoupon() (protected by @Protected decorator)
  • Via StripeWebhookService (protected by Stripe webhook signature verification)

Both paths validate the caller's identity before invoking topUpWallet(), which is the sole caller of initialize(). No security vulnerability exists.

Likely an incorrect or invalid review comment.

apps/api/drizzle.config.ts (1)

5-9: Fail‑fast DB URL check looks good

Reading POSTGRES_DB_URI from process.env and throwing early if it’s missing is a sensible way to surface misconfiguration for the drizzle CLI, and wiring it directly into dbCredentials.url keeps the config simple and explicit.

Also applies to: 16-16

apps/api/src/core/services/tx/tx.service.ts (1)

11-11: ALS‑backed transaction reuse is sound

Exporting ApiTransaction clarifies the transaction type, and using AsyncLocalStorage<Map<TxType, ApiTransaction>> with a PG_TX key plus the early return in transaction() cleanly avoids nested drizzle transactions while preserving access to the existing tx via getPgTx(). This design looks correct and cohesive with the rest of the service.

Also applies to: 15-15, 20-25

apps/api/src/billing/services/balances/balances.service.ts (1)

9-9: Verify unit assumptions in fiat conversion

getFullBalanceInFiat and toFiatAmount assume that:

  • getFullBalance(...).data.balance and .deployments are in micro‑denom units (e.g. uakt / 1e6), and
  • StatsService.convertToFiatAmount(amount, coin) expects whole‑token amounts, not micro‑denom.

Given retrieveAndCalcFeeLimit and calculateDeploymentEscrowBalance both work with raw on‑chain amounts, the uTokenAmount / 1_000_000 scaling in toFiatAmount looks plausible, but if convertToFiatAmount already expects micro‑denoms you’d end up scaling twice and under‑reporting balances. Please double‑check the expected units on convertToFiatAmount and the shape of calculateDeploymentEscrowBalance to ensure there’s no mismatch.

Also applies to: 96-110, 112-131

apps/api/drizzle/0023_clumsy_vertigo.sql (1)

1-5: Migration aligns DB with current billing model

  • Making user_wallets.user_id NOT NULL matches the Drizzle model and the existing invariant that wallets are always associated with a user (no anonymous wallets). Based on learnings, this should be safe against existing data but is still worth double‑checking in lower envs.
  • Adding is_default boolean DEFAULT false NOT NULL plus the partial unique index on (user_id, is_default) where is_default = true correctly enforces “at most one default payment method per user” while allowing multiple non‑default methods.
  • Dropping wallet_settings.auto_reload_threshold and auto_reload_amount is consistent with the refactored wallet‑settings schema where these fields are no longer used.
apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (1)

1-3: WalletSetting schema and relations look consistent

The WalletSetting table now cleanly models a single setting row per wallet (wallet_settings_wallet_id_unique) and references both UserWallets.id and Users.id with ON DELETE CASCADE, while WalletSettingRelations correctly wires user and wallet one‑to‑one relations via those FKs. This aligns well with the surrounding user/wallet schema and should make Drizzle relation queries straightforward.

Also applies to: 7-29, 31-40

apps/api/src/billing/services/paying-user/paying-user.ts (1)

1-14: PayingUser type and guards are clear and type‑safe

Using Require<UserOutput, "stripeCustomerId"> plus the isPayingUser type guard and assertIsPayingUser assertion (with 402) gives a clean, reusable way to enforce “payments set up” preconditions while benefiting from TypeScript narrowing in Stripe-related code paths. This looks solid.

apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts (1)

19-27: Well-designed partial unique index for enforcing single default per user.

The isDefault column with the filtered unique index userIdIsDefaultUnique ensures at the database level that only one payment method per user can be marked as default. This is the correct approach to prevent race conditions when multiple payment methods are attached concurrently.

apps/api/src/billing/services/managed-signer/managed-signer.service.ts (2)

1-2: Import versions look intentional.

Based on learnings, the Akash protocol message imports from different versions (v1, v1beta4, v1beta5) are considered acceptable by maintainers due to backward compatibility requirements.


68-78: Good addition of wallet reload scheduling after spending transactions.

The logic correctly schedules an immediate wallet reload when spending-related messages (MsgCreateDeployment or MsgAccountDeposit) are detected. The scheduling happens after successful transaction execution, and the original result is properly returned. The includes() pattern with $type static properties is consistent with how typeUrl matching is done elsewhere in the codebase and works as expected with proto-generated message classes.

apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (1)

144-163: Good implementation of default payment method logic.

The approach of determining isDefault based on existing count and parallelizing local creation with remote default marking is efficient. The assertIsPayingUser check ensures proper user state before Stripe operations.

The race condition concern from past reviews is addressed by the partial unique index in the schema (userIdIsDefaultUnique), which prevents multiple defaults at the database level.

apps/api/src/core/services/job-queue/job-queue.service.ts (1)

108-115: Clean addition of the complete method.

The implementation follows the same pattern as the existing cancel method, with proper logging. This enables handlers to manually complete jobs when needed.

apps/api/src/billing/http-schemas/stripe.schema.ts (2)

10-14: LGTM!

The new schema follows the established pattern for input schemas with nested data objects, consistent with other schemas in this file like ConfirmPaymentRequestSchema.


61-63: LGTM!

The PaymentMethodResponseSchema appropriately wraps PaymentMethodSchema in a data envelope, maintaining consistency with the response schema patterns used throughout this file.

apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts (2)

16-24: LGTM!

The scheduleImmediate method properly handles the case where wallet settings don't exist, preventing unnecessary job scheduling.


26-54: Good transaction handling and idempotency.

The method correctly:

  • Uses a transaction to ensure atomicity between job state changes and DB updates
  • Handles previous job cleanup with configurable action (cancel vs complete)
  • Uses singletonKey to prevent duplicate jobs per user
  • Validates job creation success
apps/api/src/billing/controllers/stripe/stripe.controller.ts (4)

50-56: LGTM!

The new markAsDefault endpoint properly:

  • Uses the Protected decorator with appropriate permissions
  • Retrieves the paying user via getCurrentPayingUser()
  • Delegates to the stripe service with proper authorization

58-65: The 404 handling is now in the service layer.

The previous review comment about missing 404 handling is no longer applicable. Looking at stripe.service.ts lines 134-135, getDefaultPaymentMethod now throws 404 assertions when the payment method is not found in either the database or Stripe.


67-77: LGTM!

The non-strict user retrieval pattern is appropriate here - returning an empty array for unauthenticated/non-paying users is a reasonable fallback.


82-87: Previous critical issue has been addressed.

The hasPaymentMethod call is now properly awaited, fixing the security bug where the Promise was being evaluated as truthy instead of its resolved boolean value.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (2)

276-319: Good test setup pattern.

The setup function follows the coding guidelines:

  • Positioned at the bottom of the root describe block
  • Creates and returns the service under test
  • Uses jest-mock-extended for mocking
  • Avoids shared state
  • No explicit return type

The new dependencies (userRepository, stripeService, walletReloadJobService) are properly wired.


98-103: LGTM!

The test correctly verifies that scheduleForWalletSetting is called with the expected wallet setting properties when creating a new setting with auto-reload enabled.

apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (4)

66-75: Clear orchestration flow with Result-based error handling.

The handler appropriately:

  • Collects resources first with validation
  • Only attempts reload if resources are valid
  • Schedules next check after successful resource collection
  • Logs validation errors without rethrowing (allowing graceful termination)

142-166: Good error handling for payment method retrieval.

HTTP errors are converted to structured ValidationError results, while unexpected errors are re-thrown for proper job failure handling. The error field in ValidationError preserves the original error for logging context.


175-219: Solid reload logic with idempotency protection.

Key strengths:

  • Uses idempotencyKey with job ID to prevent duplicate Stripe charges on retry
  • Enforces minimum reload amount ($20) per retrieved learning
  • Logs both success and failure cases with relevant context
  • Re-throws errors to trigger job retry mechanism

221-234: Consider the impact of swallowing scheduling errors.

If scheduleForWalletSetting fails, the error is logged but not re-thrown, meaning the current job completes successfully. This could leave the user without a scheduled next check.

Is this intentional? If so, consider adding a comment explaining the rationale. If not, you may want to re-throw or implement a fallback mechanism.

apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2)

70-81: LGTM!

New utility methods countByUserId and findDefaultByUserId are clean implementations using proper access control via whereAccessibleBy.


83-100: Previous race condition addressed.

The markAsDefault method now uses ensureTransaction and performs the unmark operation after successfully marking the target, which addresses the previous review concern about ordering.

apps/api/src/billing/services/stripe/stripe.service.ts (5)

32-33: LGTM!

The PaymentMethod type extension adds the validated flag to the Stripe type, providing a clear contract for methods that merge local validation state with remote Stripe data.


122-138: Good defensive assertions for payment method retrieval.

The method properly:

  • Checks for deleted customer (402 status)
  • Asserts on both local and remote sources with clear source attribution
  • Returns merged data with validation status

140-145: LGTM!

Simple ownership verification that properly handles both string and object customer references from Stripe.


147-174: Previous transaction/remote-call concern addressed.

The Stripe remote update (markRemotePaymentMethodAsDefault) is now called after local database operations succeed, reducing the risk of inconsistency. The added timeouts (3 seconds) on Stripe calls help prevent connection pool exhaustion as noted in the previous discussion.


186-216: Clean idempotencyKey implementation.

The optional idempotencyKey parameter is properly handled:

  • Only included in request options when provided
  • Uses correct Stripe API parameter structure

This enables the wallet reload handler to prevent duplicate charges on job retries.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (8)

6-10: LGTM: New imports support the expanded validation and job scheduling.

The imported dependencies are properly used throughout the refactored code - StripeService for payment validation, WalletReloadJobService for scheduling, isUniqueViolation for error handling, and UserRepository for user lookups.


20-27: LGTM: Constructor properly injects new dependencies.

The expanded dependencies (UserRepository, StripeService, WalletReloadJobService) support the enhanced validation and job scheduling logic. Proper dependency injection pattern with readonly modifiers.


43-56: LGTM: Clean upsert flow with proper transaction handling.

The refactored upsert flow is clear and correct:

  1. Try update first
  2. If no existing setting, create new one
  3. Arrange job scheduling based on state transition
  4. Return setting without exposing internal autoReloadJobId

The @WithTransaction() decorator ensures atomicity across the update/create and scheduling operations.


58-75: LGTM: Clean update logic with early returns.

The update method properly:

  • Returns early if no existing setting (avoiding unnecessary work)
  • Validates changes before persisting
  • Maintains access control via ability
  • Returns both previous and next states for scheduling decisions

77-105: LGTM: Robust create logic with race condition handling.

The create method properly handles concurrent creation attempts:

  • Uses isUniqueViolation to detect conflicts
  • Retries with update on unique violation (race condition)
  • Asserts success to prevent silent failures

The validation call without prev is correct for new settings. The userWallet lookup and assertion properly guard against invalid state.


143-153: LGTM: Deletion properly integrated with job cancellation.

The delete method correctly:

  • Maintains access control checks
  • Conditionally cancels the job only if it exists
  • Uses Promise.all for efficient parallel execution of delete and cancel operations

107-131: Verify that the external service methods are available and properly typed.

The validation correctly expands to ensure users have Stripe payment setup before enabling auto-reload. Verification confirms:

  1. UserRepository.findById() exists and returns Promise<UserOutput | undefined>
  2. StripeService.getDefaultPaymentMethod() exists with signature (user: PayingUser, ability: AnyAbility) => Promise<PaymentMethod>

However, do not simplify line 126. The spreading { ...user, stripeCustomerId } is necessary, not redundant. Since PayingUser is defined as Require<UserOutput, "stripeCustomerId"> (requiring stripeCustomerId to be non-optional), the spreading explicitly includes the asserted property to help TypeScript properly narrow the type for the getDefaultPaymentMethod() call. Removing it would cause a type error.

Likely an incorrect or invalid review comment.


133-141: Scheduling logic is correct.

The method properly handles transitions: schedules when auto-reload changes from disabled to enabled, and cancels any existing job when disabling. Changes to threshold/amount while enabled don't trigger rescheduling, which is appropriate since the job reads current settings on each execution.

@ygrishajev ygrishajev force-pushed the feature/auto-reload-handler branch from a45589e to 2cdd4d8 Compare December 2, 2025 15:11
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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/deployment/repositories/deployment-setting/deployment-setting.repository.ts (1)

64-74: Address the LEFT JOIN nullability issue flagged in the comment.

The comment on line 73 correctly identifies that walletId may be null due to the LEFT JOIN with UserWallets. The type assertion to AutoTopUpDeployment[] on line 74 is unsafe, as AutoTopUpDeployment expects walletId: number (non-null).

Consider changing to INNER JOIN to ensure walletId is never null:

       const items = await this.pg
         .select({
           id: this.table.id,
           dseq: this.table.dseq,
           walletId: UserWallets.id,
           address: UserWallets.address,
           isOldWallet: UserWallets.isOldWallet
         })
         .from(this.table)
-        .leftJoin(Users, eq(this.table.userId, Users.id))
-        .leftJoin(UserWallets, eq(Users.id, UserWallets.userId))
+        .innerJoin(Users, eq(this.table.userId, Users.id))
+        .innerJoin(UserWallets, eq(Users.id, UserWallets.userId))
         .where(and(...clauses))
         .limit(options.limit)
         .orderBy(desc(this.table.id));

       lastId = last(items)?.id;

       if (items.length) {
-        // BUGALERT: walletId may be null because of LEFT JOIN. should be INNER JOIN?
         yield items as AutoTopUpDeployment[];
       }

If deployments without associated wallets are valid, update the AutoTopUpDeployment type to reflect nullability:

export type AutoTopUpDeployment = {
  id: string;
-  walletId: number;
+  walletId: number | null;
  dseq: string;
  address: string;
  isOldWallet: boolean | null;
};
♻️ Duplicate comments (1)
apps/api/src/core/services/job-queue/job-queue.service.ts (1)

89-94: Avoid logging full job payload to prevent leaking PII.

The JOB_ENQUEUED event logs the entire job object (line 91), which may contain sensitive data such as email addresses and message content. This was flagged in a previous review but has not been addressed.

Apply this diff:

     this.logger.info({
       event: "JOB_ENQUEUED",
-      job,
+      jobName: job.name,
       jobId,
+      version: job.version,
       options
     });
🧹 Nitpick comments (3)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (1)

132-136: Consider consistent naming: jobId vs id.

The cancel event uses id while other job events (STARTED, DONE, FAILED) use jobId. Consider aligning to jobId for consistency across all job-related log events.

apps/api/src/billing/services/wallet-balance-reload-check/README.md (1)

53-53: Consider adding language specifiers to code blocks.

Fenced code blocks at lines 53, 104, and 150 would benefit from language specifiers for syntax highlighting.

For example:

-```
+```text
 balance < 0.25 * costUntilTargetDateInFiat


Also applies to: 104-104, 150-150

</blockquote></details>
<details>
<summary>apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1)</summary><blockquote>

`293-300`: **Simplify the nested ternary logic for improved readability.**

The nested ternary expressions determining `userWithStripe` are difficult to follow and maintain. Consider using a more explicit conditional structure or helper function.



Apply this diff to simplify:

```diff
-    const userWithStripe =
-      input?.user && input.user.stripeCustomerId === null
-        ? user
-        : input?.user && input.user.stripeCustomerId
-          ? user
-          : user.stripeCustomerId
-            ? user
-            : { ...user, stripeCustomerId: faker.string.uuid() };
+    const userWithStripe = (() => {
+      if (input?.user?.stripeCustomerId === null) return user;
+      if (input?.user?.stripeCustomerId) return user;
+      if (user.stripeCustomerId) return user;
+      return { ...user, stripeCustomerId: faker.string.uuid() };
+    })();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c845f92 and 2cdd4d8.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (23)
  • apps/api/drizzle/0023_clumsy_vertigo.sql (1 hunks)
  • apps/api/drizzle/meta/0023_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/_journal.json (1 hunks)
  • apps/api/src/auth/services/ability/ability.service.ts (2 hunks)
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts (2 hunks)
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (2 hunks)
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1 hunks)
  • apps/api/src/billing/services/balances/balances.service.ts (3 hunks)
  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts (6 hunks)
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts (4 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (6 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/README.md (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2 hunks)
  • apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts (1 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (11 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (6 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts (9 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.ts (4 hunks)
  • apps/api/src/dashboard/services/stats/stats.service.ts (1 hunks)
  • apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts (2 hunks)
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts (7 hunks)
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/api/src/dashboard/services/stats/stats.service.ts
  • apps/api/src/auth/services/ability/ability.service.ts
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
  • apps/api/drizzle/0023_clumsy_vertigo.sql
  • apps/api/drizzle/meta/0023_snapshot.json
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/billing/services/balances/balances.service.ts
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test

Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

**/*.spec.{ts,tsx}: Use <Subject>.name in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references
Use either a method name or a condition starting with 'when' for nested suite descriptions in tests
Use present simple, 3rd person singular for test descriptions without prepending 'should'

Files:

  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
🧠 Learnings (15)
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-10-31T11:26:42.138Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.138Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-09-25T14:31:44.914Z
Learnt from: baktun14
Repo: akash-network/console PR: 1969
File: apps/deploy-web/src/pages/payment.tsx:179-191
Timestamp: 2025-09-25T14:31:44.914Z
Learning: The payment confirmation endpoint in apps/api/src/billing/http-schemas/stripe.schema.ts uses zod schema validation with `amount: z.number().gte(20, "Amount must be greater or equal to $20")` to ensure all payment requests meet the minimum amount requirement, preventing zero-amount or invalid payments from reaching Stripe.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-09-04T04:27:50.638Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/billing/services/managed-signer/managed-signer.service.ts:1-1
Timestamp: 2025-09-04T04:27:50.638Z
Learning: In the akash-network/console project, importing MsgCreateLease from "akashnetwork/akash-api/v1beta3" instead of v1beta4 is considered non-critical by the maintainers, likely due to backward compatibility or project-specific requirements.

Applied to files:

  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-09-04T04:27:40.858Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/billing/services/managed-signer/managed-signer.service.ts:98-107
Timestamp: 2025-09-04T04:27:40.858Z
Learning: In the Akash protocol, when processing MsgCreateLease messages, the bidId.dseq field is always required and guaranteed to be present at runtime, even though TypeScript types may suggest it could be undefined. The non-null assertion operator (!) is appropriate to use in this context.

Applied to files:

  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: In apps/api job scheduling with JobQueueService, use .toISOString() for startAfter values instead of Date objects to ensure UTC timezone consistency when dates are persisted to the database. Date objects can be ambiguous about timezone during serialization, while ISO strings with .toISOString() explicitly represent UTC.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-10-15T16:39:55.348Z
Learnt from: jzsfkzm
Repo: akash-network/console PR: 2039
File: apps/deploy-web/tests/ui/change-wallets.spec.ts:4-10
Timestamp: 2025-10-15T16:39:55.348Z
Learning: In the Akash Console E2E tests using the context-with-extension fixture, the first wallet is automatically created during fixture setup via `importWalletToLeap` in `apps/deploy-web/tests/ui/fixture/wallet-setup.ts`, so tests that call `frontPage.createWallet()` are creating a second wallet to test wallet switching functionality.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:44.790Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/no-jest-mock.mdc:0-0
Timestamp: 2025-11-25T17:45:44.790Z
Learning: Applies to **/*.spec.{ts,tsx} : Don't use `jest.mock()` in test files. Instead, use `jest-mock-extended` to create mocks and pass mocks as dependencies to the service under test

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-11-25T17:45:39.561Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/general.mdc:0-0
Timestamp: 2025-11-25T17:45:39.561Z
Learning: Applies to **/*.{ts,tsx,js} : Never use type `any` or cast to type `any`. Always define the proper TypeScript types.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:52.965Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/setup-instead-of-before-each.mdc:0-0
Timestamp: 2025-11-25T17:45:52.965Z
Learning: Applies to **/*.spec.{ts,tsx} : Use `setup` function instead of `beforeEach` in test files. The `setup` function must be at the bottom of the root `describe` block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
📚 Learning: 2025-11-19T15:15:07.283Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2254
File: apps/api/test/functional/sign-and-broadcast-tx.spec.ts:4-4
Timestamp: 2025-11-19T15:15:07.283Z
Learning: In the Akash Network Console project, when tests use native Node.js fetch (available in Node 18+), fetch-mock should be used for HTTP mocking instead of nock, as nock does not support intercepting native fetch calls. This applies to apps/api/test/functional/sign-and-broadcast-tx.spec.ts and any other tests using native fetch.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
📚 Learning: 2025-11-25T17:45:49.180Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/query-by-in-tests.mdc:0-0
Timestamp: 2025-11-25T17:45:49.180Z
Learning: Applies to {apps/deploy-web,apps/provider-console}/**/*.spec.tsx : Use `queryBy` methods instead of `getBy` methods in test expectations

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:58.258Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/test-descriptions.mdc:0-0
Timestamp: 2025-11-25T17:45:58.258Z
Learning: Applies to **/*.spec.{ts,tsx} : Use `<Subject>.name` in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references

Applied to files:

  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: The apps/api codebase consistently uses .toISOString() for startAfter values when scheduling jobs with JobQueueService to ensure UTC timezone consistency. This pattern is used throughout trial-started.handler.ts, trial-deployment-lease-created.handler.ts and other job scheduling code. Do not suggest changing to Date objects as ISO strings provide explicit UTC representation for database storage.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-07-24T17:00:52.361Z
Learnt from: baktun14
Repo: akash-network/console PR: 1725
File: apps/api/src/utils/constants.ts:5-5
Timestamp: 2025-07-24T17:00:52.361Z
Learning: In the Akash Network Console project, when cross-cutting concerns or broader refactoring issues are identified during PR review, the preferred approach is to create a separate GitHub issue to track the work rather than expanding the scope of the current PR. This maintains focus and allows for proper planning of architectural improvements.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
🧬 Code graph analysis (12)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (8)
apps/api/src/core/types/require.type.ts (1)
  • Require (1-3)
apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (1)
  • UserWalletOutput (18-22)
apps/api/src/billing/services/paying-user/paying-user.ts (2)
  • PayingUser (6-6)
  • isPayingUser (12-14)
apps/api/src/billing/http-schemas/balance.schema.ts (1)
  • GetBalancesResponseOutput (17-17)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (241-241)
packages/http-sdk/src/stripe/stripe.service.ts (1)
  • StripeService (18-94)
apps/api/src/core/services/job-queue/job-queue.service.ts (2)
  • JobPayload (209-209)
  • JobMeta (218-218)
apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts (2)
apps/stats-web/src/components/transactions/akash/MsgCreateDeployment.tsx (1)
  • MsgCreateDeployment (12-29)
apps/stats-web/src/components/transactions/akash/MsgAccountDeposit.tsx (1)
  • MsgAccountDeposit (10-19)
apps/api/src/deployment/services/draining-deployment/draining-deployment.service.ts (2)
apps/api/src/utils/constants.ts (1)
  • averageBlockCountInAnHour (8-8)
apps/indexer/drizzle/schema.ts (1)
  • deployment (185-208)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (3)
apps/api/src/auth/services/auth.service.ts (5)
  • Protected (62-78)
  • currentUser (13-15)
  • currentUser (17-24)
  • ability (44-46)
  • ability (48-50)
apps/api/src/billing/http-schemas/stripe.schema.ts (5)
  • PaymentMethodMarkAsDefaultInput (240-240)
  • PaymentMethodResponse (243-243)
  • PaymentMethodsResponse (242-242)
  • ConfirmPaymentRequest (244-244)
  • ConfirmPaymentResponse (247-247)
apps/api/src/core/lib/semaphore.decorator.ts (1)
  • Semaphore (3-28)
apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts (3)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)
  • WalletSettingOutput (19-22)
apps/api/src/core/services/job-queue/job-queue.service.ts (1)
  • EnqueueOptions (227-227)
apps/api/src/billing/events/wallet-balance-reload-check.ts (1)
  • WalletBalanceReloadCheck (5-15)
apps/api/src/billing/services/stripe/stripe.service.ts (5)
apps/api/src/billing/http-schemas/stripe.schema.ts (2)
  • PaymentMethod (241-241)
  • PaymentIntentResult (245-245)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • PaymentMethod (30-41)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • PayingUser (6-6)
apps/api/src/user/repositories/user/user.repository.ts (1)
  • UserOutput (10-12)
apps/api/src/core/services/tx/tx.service.ts (1)
  • WithTransaction (39-51)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (3)
  • job (75-93)
  • error (161-166)
  • user (140-159)
apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts (1)
apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1)
  • UserWallets (5-18)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (4)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (2)
  • WalletSettingInput (12-17)
  • WalletSettingOutput (19-22)
packages/http-sdk/src/stripe/stripe.service.ts (1)
  • StripeService (18-94)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (3)
  • userId (95-138)
  • error (161-166)
  • user (140-159)
apps/api/src/core/repositories/base.repository.ts (1)
  • isUniqueViolation (237-239)
apps/api/src/billing/services/managed-signer/managed-signer.service.ts (2)
apps/stats-web/src/components/transactions/akash/MsgCreateDeployment.tsx (1)
  • MsgCreateDeployment (12-29)
apps/stats-web/src/components/transactions/akash/MsgAccountDeposit.tsx (1)
  • MsgAccountDeposit (10-19)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (2)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • userId (95-138)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (2)
  • userId (58-75)
  • userId (77-105)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (1)
apps/api/src/core/services/job-queue/job-queue.service.ts (2)
  • setup (185-192)
  • Job (199-207)
🪛 LanguageTool
apps/api/src/billing/services/wallet-balance-reload-check/README.md

[style] ~39-~39: ‘emergency situations’ might be wordy. Consider a shorter alternative.
Context: ... margin before funds run out - Prevents emergency situations ## When Does the Check Run? The handl...

(EN_WORDINESS_PREMIUM_EMERGENCY_SITUATIONS)

🪛 markdownlint-cli2 (0.18.1)
apps/api/src/billing/services/wallet-balance-reload-check/README.md

53-53: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


104-104: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


150-150: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: validate / validate-app
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (19)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (4)

67-76: Good addition of edge case test for duplicate handler registration.

This test ensures the service properly guards against handler conflicts on the same queue, which prevents subtle runtime bugs.


95-101: LGTM!

The enhanced logging with jobId and options improves job traceability. The test correctly validates both the log structure and return value.


152-180: LGTM!

The test correctly validates the new handler signature with JobMeta context. The job id is properly passed through the worker execution flow and verified in both handler calls and log events.


300-303: LGTM!

The runWithContext mock correctly simulates the context execution pattern by invoking the callback. The type assertion ensures compatibility with the service signature.

apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts (1)

41-53: LGTM! Address filtering implemented correctly.

The optional address parameter and its filtering logic are correctly implemented. The filter integrates cleanly into the existing pagination pattern.

apps/api/src/deployment/services/draining-deployment/draining-deployment.service.ts (1)

96-134: LGTM! Cost calculation logic is correct.

The method correctly:

  • Returns early if wallet/address is missing
  • Calculates target height based on the time difference
  • Filters deployments that would close within the target range
  • Calculates the cost to keep each deployment running until the target date

The JSDoc provides clear documentation of the method's purpose.

apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts (1)

195-353: Excellent test coverage for the new method!

The tests comprehensively cover:

  • Happy path with multiple deployments
  • Edge cases (missing wallet, null address, no deployments)
  • Boundary conditions (null predictedClosedHeight, out-of-range heights)

The setupCalculateCost helper function provides a clean way to configure test scenarios. The test structure follows all coding guidelines, including the use of the setup function pattern and jest-mock-extended for mocking.

apps/api/src/billing/services/managed-signer/managed-signer.service.ts (1)

74-76: Verify error handling strategy when scheduling fails after successful transaction.

Currently, if scheduleImmediate throws an error, it propagates to the caller even though the transaction has already succeeded (line 70). This means the caller receives an error despite a successful blockchain transaction.

Consider wrapping the scheduling call in try-catch and logging failures instead, or document that this behavior is intentional for alerting purposes.

apps/api/src/billing/services/balances/balances.service.ts (1)

112-134: LGTM! Clean fiat conversion implementation.

The fiat conversion flow is well-structured:

  • Memoization is consistent with getFullBalance
  • Token conversion (÷1,000,000) is correct for uTokens
  • Two-decimal formatting is appropriate for currency display
  • Coin mapping logic correctly delegates to StatsService
apps/api/src/billing/controllers/stripe/stripe.controller.ts (1)

50-79: LGTM! Payment method handling correctly implemented.

The new payment method endpoints are well-structured:

  • Authorization checks are properly applied via @Protected decorators
  • 404 handling for missing default payment method (line 64) addresses past review feedback
  • Async ownership verification with await (line 89) addresses past review feedback
  • Graceful handling when no paying user exists (lines 71-78)
apps/api/src/billing/services/stripe/stripe.service.ts (1)

146-183: LGTM! Payment method default handling correctly sequenced.

The refactored flow addresses past review feedback:

  • Stripe updates execute after local DB operations (lines 156, 170)
  • 3-second timeouts prevent connection pool exhaustion (lines 150, 181)
  • Transaction rollback protects consistency if Stripe calls fail
  • Idempotency through local record creation prevents duplicate work
apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts (1)

26-54: LGTM! Transactional job scheduling correctly implemented.

The scheduling logic is well-designed:

  • Transaction wrapper ensures atomicity between DB update and job enqueue
  • Singleton key prevents duplicate jobs (line 43)
  • Pre-existing job cleanup (cancel/complete) prevents orphaned jobs
  • Throwing on enqueue failure triggers rollback of wallet setting update
apps/api/src/core/services/job-queue/job-queue.service.ts (1)

218-224: LGTM! JobMeta extension enables better job context.

The JobHandler interface extension is well-designed:

  • Optional job parameter allows gradual handler migration
  • JobMeta type provides job ID for tracing and idempotency
  • Handler invocation (line 155) correctly passes the context
apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts (1)

492-578: LGTM! Comprehensive test coverage for wallet reload scheduling.

The new test suite effectively validates the scheduling behavior:

  • Verifies spending messages (MsgCreateDeployment, MsgAccountDeposit) trigger immediate reload
  • Confirms non-spending messages skip scheduling
  • Properly mocks dependencies (registryMock, walletReloadJobService)
  • Follows project testing conventions
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2)

75-159: LGTM! Well-structured resource validation flow.

The resource collection and validation logic properly uses the Result pattern to handle errors, performs comprehensive checks (wallet settings, auto-reload enabled, wallet initialized, Stripe customer ID), and returns appropriate validation errors for each failure case.


168-233: LGTM! Solid reload and scheduling implementation.

The reload logic correctly:

  • Skips reloads when cost is zero or balance is sufficient (line 180)
  • Applies minimum reload amount of $20 (line 189)
  • Uses idempotent payment intent creation with job-scoped key (line 197)
  • Re-throws scheduling errors to enable pgboss retry (line 226)

The implementation properly handles edge cases and ensures reliability.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (2)

107-131: LGTM! Comprehensive validation with proper Stripe integration.

The validation logic correctly:

  • Ensures threshold and amount are provided when auto-reload is enabled
  • Verifies user existence and Stripe customer ID setup
  • Confirms default payment method exists before enabling auto-reload

The error messages are clear and actionable for users.


58-105: LGTM! Robust upsert implementation with race condition handling.

The update/create flow properly:

  • Returns early from update if no existing setting (line 63-65)
  • Uses isUniqueViolation to detect race conditions (line 93)
  • Retries via update path on duplicate key violations (line 94)
  • Validates settings before persistence

This prevents race conditions during concurrent upserts.

apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)

38-46: Critical: toOutput method is called but was removed.

Line 45 calls this.toOutput(walletSetting), but according to the AI summary, the toOutput method has been removed from this class. This will cause a runtime error.

The method should likely return the walletSetting directly after the findFirst operation:

  async findByUserId(userId: WalletSettingOutput["userId"]): Promise<WalletSettingOutput | undefined> {
    const walletSetting = await this.cursor.query.WalletSetting.findFirst({
      where: this.whereAccessibleBy(eq(this.table.userId, userId))
    });

    if (!walletSetting) return undefined;

-    return this.toOutput(walletSetting);
+    return walletSetting;
  }

Likely an incorrect or invalid review comment.

@ygrishajev ygrishajev force-pushed the feature/auto-reload-handler branch from 2cdd4d8 to 20f5527 Compare December 2, 2025 15:23
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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/deployment/repositories/deployment-setting/deployment-setting.repository.ts (1)

73-74: Existing type safety issue now more critical with address filtering.

The BUGALERT comment correctly identifies that walletId can be null due to the LEFT JOIN, but the type assertion to AutoTopUpDeployment[] masks this. With the new address filtering feature, this issue becomes more critical because:

  1. The AutoTopUpDeployment type declares walletId: number (non-nullable)
  2. Consumers of this method may assume walletId is always present
  3. Runtime null reference errors could occur when accessing walletId

Consider changing the LEFT JOIN to INNER JOIN for UserWallets to ensure all returned deployments have valid wallet data, or update the AutoTopUpDeployment type to reflect that walletId can be nullable.

  // Update the type to reflect reality
  export type AutoTopUpDeployment = {
    id: string;
-   walletId: number;
+   walletId: number | null;
    dseq: string;
-   address: string;
+   address: string | null;
    isOldWallet: boolean | null;
  };

Or, convert to INNER JOIN if deployments without wallets should be excluded:

        .from(this.table)
-       .leftJoin(Users, eq(this.table.userId, Users.id))
-       .leftJoin(UserWallets, eq(Users.id, UserWallets.userId))
+       .innerJoin(Users, eq(this.table.userId, Users.id))
+       .innerJoin(UserWallets, eq(Users.id, UserWallets.userId))
        .where(and(...clauses))
♻️ Duplicate comments (4)
apps/api/src/auth/services/ability/ability.service.ts (1)

30-42: Re-evaluate PaymentMethod manage rules – appear unused and potentially misleading.

These two "manage" rules for the PaymentMethod subject still look unreferenced in the current code paths (Stripe controllers are guarded via StripePayment + paying-user checks). Keeping unused ability rules around PaymentMethod can be confusing and might accidentally be relied on later in ways that don’t align with the actual authorization flow.

If they’re not wired anywhere, consider removing them (or clearly documenting the intended future use) to avoid divergent sources of truth for payment-method permissions.

apps/api/src/core/services/job-queue/job-queue.service.spec.ts (1)

95-100: Align logging expectations with reduced job payload once enqueue logging is trimmed.

The enqueue test still asserts that logger.info receives the full job object. If you adopt the earlier change to only log metadata (name/id/version), this expectation should be relaxed to those fields instead of the entire payload to avoid coupling tests to PII-heavy logging.

apps/api/src/core/services/job-queue/job-queue.service.ts (1)

89-94: Avoid logging full job payload to reduce PII exposure.

JOB_ENQUEUED still logs the entire job object, which for many jobs can include user identifiers or message content. At info level this is a privacy and noise concern; logs generally only need non-sensitive metadata.

Consider trimming this to metadata only, e.g.:

-    this.logger.info({
-      event: "JOB_ENQUEUED",
-      job,
-      jobId,
-      options
-    });
+    this.logger.info({
+      event: "JOB_ENQUEUED",
+      jobName: job.name,
+      jobId,
+      version: job.version,
+      options
+    });

This preserves traceability without leaking job payloads into logs.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (1)

49-49: Replace as any casts with proper types.

This cast (and similar ones at lines 110, 181, 216) violates the coding guideline "Never use type any". Import WalletSettingOutput and use it for proper typing, or adjust the generateWalletSetting seeder to return the correct type.

-      walletSettingRepository.updateById.mockResolvedValue(updatedSetting as any);
+      walletSettingRepository.updateById.mockResolvedValue(updatedSetting as WalletSettingOutput);
🧹 Nitpick comments (9)
apps/api/src/deployment/services/draining-deployment/draining-deployment.service.ts (2)

99-101: Redundant null check for address.

Since the wallet is fetched by address on line 97, if userWallet exists, userWallet.address should not be null or empty. This check might be defensive programming, but it's logically redundant in this context.

-    if (!userWallet || !userWallet.address) {
+    if (!userWallet) {
      return 0;
    }

126-126: Move constant calculation outside the loop.

blocksNeeded = targetHeight - currentHeight produces the same value for every iteration since both targetHeight and currentHeight are constant within the method. Calculate it once before the loop to improve readability and avoid redundant computation.

+   const blocksFromNowToTarget = targetHeight - currentHeight;
+
    for await (const deploymentSettings of this.deploymentSettingRepository.paginateAutoTopUpDeployments({ address, limit: 100 })) {
      // ...
      
      for (const { predictedClosedHeight, blockRate } of drainingDeployments) {
        if (predictedClosedHeight && predictedClosedHeight >= currentHeight && predictedClosedHeight <= targetHeight) {
-         const blocksNeeded = targetHeight - currentHeight;
-         const amountNeeded = Math.floor(blockRate * blocksNeeded);
+         const amountNeeded = Math.floor(blockRate * blocksFromNowToTarget);
          totalAmount += amountNeeded;
        }
      }
    }

Note: This suggestion assumes the current logic is correct. See the previous comment about potential logic issues with the blocks calculation.

apps/api/src/billing/services/wallet-balance-reload-check/README.md (1)

53-98: Add languages to fenced code blocks to satisfy markdownlint.

The three fenced blocks (sequence diagram, threshold formula, reload-amount formula) currently use bare ``` fences, which triggers MD040. You can keep them as plain text while satisfying the linter by tagging them, e.g.:

-```
+```text
  User enables auto-reload
  ...

and similarly for the formula blocks around lines 104 and 150.

Also applies to: 104-107, 150-152

apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts (1)

16-58: Wallet reload scheduling flow looks solid; consider cleaning up unused userId in cancel.

The transactional scheduleForWalletSetting logic (complete/cancel previous job, update autoReloadJobId, enqueue with a per-user singleton key, and fail fast on missing createdJobId) is coherent and matches the reload-check design.

Minor nit: cancel(userId: string, jobId: string) ignores userId. If there’s no caller need for it, you could drop that parameter; otherwise, consider using it for logging/consistency so the signature reflects actual behavior.

apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1)

21-279: Comprehensive handler tests; only minor complexity in setup helper.

The test suite gives very good coverage of the reload decision logic (threshold edge cases, minimum amount, next-check scheduling, scheduling failures, and all validation branches) and follows the project’s testing conventions (subject-based describe, setup factory, jest-mock-extended).

The only minor nit is that the userWithStripe derivation and related ternaries in setup are a bit hard to read. If this helper grows further, consider breaking that into a small, named helper or explicit if/else blocks for the different scenarios (no user, user with stripe ID, user with null stripe ID) to make future maintenance easier.

Also applies to: 282-373

apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)

48-65: Add explicit return type annotation for better type safety.

The method lacks an explicit return type, which would improve code clarity and catch potential type mismatches at compile time. Consider adding the return type based on the query structure.

-  async findInternalByUserIdWithRelations(userId: WalletSettingOutput["userId"]) {
+  async findInternalByUserIdWithRelations(userId: WalletSettingOutput["userId"]): Promise<
+    | (DbWalletSettingOutput & {
+        wallet: Pick<ApiPgTables["UserWallet"]["$inferSelect"], "address" | "isOldWallet">;
+        user: ApiPgTables["User"]["$inferSelect"];
+      })
+    | undefined
+  > {

Alternatively, define a dedicated type for this return shape to improve reusability.

apps/api/src/billing/controllers/stripe/stripe.controller.ts (1)

83-89: Consider using 403 instead of 500 for the user assertion.

Line 86 uses a 500 status code, but since getCurrentPayingUser({ strict: false }) can return undefined for users without a Stripe customer ID, a 403 (Forbidden) might be more semantically accurate - the user isn't authorized to make payments without proper configuration, rather than it being an internal server error.

-    assert(currentUser, 500, "Payment account not properly configured. Please contact support.");
+    assert(currentUser, 403, "Payment account not properly configured. Please contact support.");
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)

137-156: Consider extracting the hardcoded ability to a constant or documenting the rationale.

The method creates a MongoAbility inline with hardcoded permissions. While this is acceptable for a background job handler (no request-scoped auth context), it would benefit from a brief comment explaining why this approach is used, or extracting the ability definition to a class-level constant for clarity.

+  // Background jobs don't have request-scoped auth; use minimal required permissions
+  readonly #paymentMethodReadAbility = createMongoAbility([
+    { action: "read", subject: "PaymentMethod" }
+  ]);
+
   async #getDefaultPaymentMethod(user: PayingUser): Promise<Result<PaymentMethod, ValidationError>> {
     const paymentMethod = await this.stripeService.getDefaultPaymentMethod(
       user,
-      createMongoAbility([
-        {
-          action: "read",
-          subject: "PaymentMethod"
-        }
-      ])
+      this.#paymentMethodReadAbility
     );
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (1)

58-75: Ambiguous failure handling - empty object masks the reason.

When the update returns no result (line 70), the method returns {}, causing upsertWalletSetting to attempt a create operation (line 48). This could occur due to:

  • Permission denied via accessibleBy
  • Record deleted between read and update
  • Database constraint failure

If the cause is a permission issue or validation failure, the subsequent create will also fail. Consider distinguishing between "not found" (where create makes sense) and "update failed" (where propagating the error is better).

Consider returning a more explicit result or throwing an error if the update unexpectedly returns null:

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

   if (!next) {
-    return {};
+    throw new Error("Failed to update wallet setting");
   }

   return { prev, next };

This ensures that legitimate failures are surfaced rather than masked.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2cdd4d8 and 20f5527.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (23)
  • apps/api/drizzle/0023_clumsy_vertigo.sql (1 hunks)
  • apps/api/drizzle/meta/0023_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/_journal.json (1 hunks)
  • apps/api/src/auth/services/ability/ability.service.ts (2 hunks)
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts (2 hunks)
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (2 hunks)
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1 hunks)
  • apps/api/src/billing/services/balances/balances.service.ts (3 hunks)
  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts (6 hunks)
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts (4 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (6 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/README.md (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2 hunks)
  • apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts (1 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (11 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (6 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts (9 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.ts (4 hunks)
  • apps/api/src/dashboard/services/stats/stats.service.ts (1 hunks)
  • apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts (2 hunks)
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts (7 hunks)
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/api/drizzle/meta/0023_snapshot.json
  • apps/api/drizzle/0023_clumsy_vertigo.sql
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/dashboard/services/stats/stats.service.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
  • apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.ts
  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/billing/services/balances/balances.service.ts
  • apps/api/src/auth/services/ability/ability.service.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test

Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

**/*.spec.{ts,tsx}: Use <Subject>.name in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references
Use either a method name or a condition starting with 'when' for nested suite descriptions in tests
Use present simple, 3rd person singular for test descriptions without prepending 'should'

Files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts
🧠 Learnings (14)
📚 Learning: 2025-09-04T04:27:50.638Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/billing/services/managed-signer/managed-signer.service.ts:1-1
Timestamp: 2025-09-04T04:27:50.638Z
Learning: In the akash-network/console project, importing MsgCreateLease from "akashnetwork/akash-api/v1beta3" instead of v1beta4 is considered non-critical by the maintainers, likely due to backward compatibility or project-specific requirements.

Applied to files:

  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts
📚 Learning: 2025-09-04T04:27:40.858Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/billing/services/managed-signer/managed-signer.service.ts:98-107
Timestamp: 2025-09-04T04:27:40.858Z
Learning: In the Akash protocol, when processing MsgCreateLease messages, the bidId.dseq field is always required and guaranteed to be present at runtime, even though TypeScript types may suggest it could be undefined. The non-null assertion operator (!) is appropriate to use in this context.

Applied to files:

  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts
📚 Learning: 2025-10-15T16:39:55.348Z
Learnt from: jzsfkzm
Repo: akash-network/console PR: 2039
File: apps/deploy-web/tests/ui/change-wallets.spec.ts:4-10
Timestamp: 2025-10-15T16:39:55.348Z
Learning: In the Akash Console E2E tests using the context-with-extension fixture, the first wallet is automatically created during fixture setup via `importWalletToLeap` in `apps/deploy-web/tests/ui/fixture/wallet-setup.ts`, so tests that call `frontPage.createWallet()` are creating a second wallet to test wallet switching functionality.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-09-25T14:31:44.914Z
Learnt from: baktun14
Repo: akash-network/console PR: 1969
File: apps/deploy-web/src/pages/payment.tsx:179-191
Timestamp: 2025-09-25T14:31:44.914Z
Learning: The payment confirmation endpoint in apps/api/src/billing/http-schemas/stripe.schema.ts uses zod schema validation with `amount: z.number().gte(20, "Amount must be greater or equal to $20")` to ensure all payment requests meet the minimum amount requirement, preventing zero-amount or invalid payments from reaching Stripe.

Applied to files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-10-31T11:26:42.138Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.138Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: In apps/api job scheduling with JobQueueService, use .toISOString() for startAfter values instead of Date objects to ensure UTC timezone consistency when dates are persisted to the database. Date objects can be ambiguous about timezone during serialization, while ISO strings with .toISOString() explicitly represent UTC.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-11-25T17:45:44.790Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/no-jest-mock.mdc:0-0
Timestamp: 2025-11-25T17:45:44.790Z
Learning: Applies to **/*.spec.{ts,tsx} : Don't use `jest.mock()` in test files. Instead, use `jest-mock-extended` to create mocks and pass mocks as dependencies to the service under test

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-11-25T17:45:39.561Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/general.mdc:0-0
Timestamp: 2025-11-25T17:45:39.561Z
Learning: Applies to **/*.{ts,tsx,js} : Never use type `any` or cast to type `any`. Always define the proper TypeScript types.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:52.965Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/setup-instead-of-before-each.mdc:0-0
Timestamp: 2025-11-25T17:45:52.965Z
Learning: Applies to **/*.spec.{ts,tsx} : Use `setup` function instead of `beforeEach` in test files. The `setup` function must be at the bottom of the root `describe` block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-19T15:15:07.283Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2254
File: apps/api/test/functional/sign-and-broadcast-tx.spec.ts:4-4
Timestamp: 2025-11-19T15:15:07.283Z
Learning: In the Akash Network Console project, when tests use native Node.js fetch (available in Node 18+), fetch-mock should be used for HTTP mocking instead of nock, as nock does not support intercepting native fetch calls. This applies to apps/api/test/functional/sign-and-broadcast-tx.spec.ts and any other tests using native fetch.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:49.180Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/query-by-in-tests.mdc:0-0
Timestamp: 2025-11-25T17:45:49.180Z
Learning: Applies to {apps/deploy-web,apps/provider-console}/**/*.spec.tsx : Use `queryBy` methods instead of `getBy` methods in test expectations

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: The apps/api codebase consistently uses .toISOString() for startAfter values when scheduling jobs with JobQueueService to ensure UTC timezone consistency. This pattern is used throughout trial-started.handler.ts, trial-deployment-lease-created.handler.ts and other job scheduling code. Do not suggest changing to Date objects as ISO strings provide explicit UTC representation for database storage.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-07-24T17:00:52.361Z
Learnt from: baktun14
Repo: akash-network/console PR: 1725
File: apps/api/src/utils/constants.ts:5-5
Timestamp: 2025-07-24T17:00:52.361Z
Learning: In the Akash Network Console project, when cross-cutting concerns or broader refactoring issues are identified during PR review, the preferred approach is to create a separate GitHub issue to track the work rather than expanding the scope of the current PR. This maintains focus and allows for proper planning of architectural improvements.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
🧬 Code graph analysis (7)
apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts (1)
apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1)
  • UserWallets (5-18)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (4)
apps/api/src/auth/services/auth.service.ts (5)
  • Protected (62-78)
  • currentUser (13-15)
  • currentUser (17-24)
  • ability (44-46)
  • ability (48-50)
apps/api/src/billing/http-schemas/stripe.schema.ts (5)
  • PaymentMethodMarkAsDefaultInput (240-240)
  • PaymentMethodResponse (243-243)
  • PaymentMethodsResponse (242-242)
  • ConfirmPaymentRequest (244-244)
  • ConfirmPaymentResponse (247-247)
apps/api/src/core/lib/semaphore.decorator.ts (1)
  • Semaphore (3-28)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • ConfirmPaymentResponse (104-109)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (5)
apps/api/src/core/services/job-queue/job-queue.service.ts (1)
  • setup (185-192)
apps/api/test/seeders/user-wallet.seeder.ts (1)
  • UserWalletSeeder (6-30)
apps/api/test/seeders/payment-method.seeder.ts (1)
  • generatePaymentMethod (11-56)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (241-241)
apps/api/src/billing/services/balances/balances.service.ts (3)
apps/api/src/caching/helpers.ts (1)
  • Memoize (21-37)
apps/api/src/utils/constants.ts (1)
  • averageBlockTime (4-4)
apps/api/src/billing/http-schemas/balance.schema.ts (1)
  • GetBalancesResponseOutput (17-17)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (1)
apps/api/src/core/services/job-queue/job-queue.service.ts (2)
  • setup (185-192)
  • Job (199-207)
apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts (5)
apps/api/test/seeders/user-wallet.seeder.ts (1)
  • UserWalletSeeder (6-30)
apps/api/test/seeders/user.seeder.ts (1)
  • UserSeeder (5-45)
apps/stats-web/src/components/transactions/akash/MsgCreateDeployment.tsx (1)
  • MsgCreateDeployment (12-29)
apps/stats-web/src/components/transactions/akash/MsgAccountDeposit.tsx (1)
  • MsgAccountDeposit (10-19)
apps/stats-web/src/components/transactions/akash/MsgCreateLease.tsx (1)
  • MsgCreateLease (10-22)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (2)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • userId (92-135)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (2)
  • userId (58-75)
  • userId (77-105)
🪛 LanguageTool
apps/api/src/billing/services/wallet-balance-reload-check/README.md

[style] ~39-~39: ‘emergency situations’ might be wordy. Consider a shorter alternative.
Context: ... margin before funds run out - Prevents emergency situations ## When Does the Check Run? The handl...

(EN_WORDINESS_PREMIUM_EMERGENCY_SITUATIONS)

🪛 markdownlint-cli2 (0.18.1)
apps/api/src/billing/services/wallet-balance-reload-check/README.md

53-53: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


104-104: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


150-150: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (34)
apps/api/src/deployment/services/draining-deployment/draining-deployment.service.ts (1)

124-130: The implementation is correct; no logic change needed.

The filtering condition on line 125 properly identifies deployments that would close before the target date, and blocksNeeded = targetHeight - currentHeight correctly calculates the cost to extend all such deployments through the target date. Test cases explicitly validate this behavior.

apps/api/src/billing/services/balances/balances.service.ts (1)

9-21: Fiat balance helpers and StatsService integration look consistent.

The new getFullBalanceInFiat / toFiatAmount / #convertToFiatAmount / ensure2floatingDigits flow cleanly layers fiat conversion on top of existing u-denom balances, and the coin selection via DEPLOYMENT_GRANT_DENOM ("uakt""akash-network", otherwise "usd-coin") is coherent. Memoization with averageBlockTime matches getFullBalance and keeps concerns nicely localized in BalancesService.

Also applies to: 96-134

apps/api/src/billing/services/managed-signer/managed-signer.service.ts (1)

1-2: Spending detection and immediate reload scheduling are wired correctly.

Decoding messages once, detecting spending via .endsWith(MsgCreateDeployment.$type | MsgAccountDeposit.$type), and then delegating to walletReloadJobService.scheduleImmediate(userId) keeps the side effect tightly scoped to actual spend-related txs and reuses the centralized scheduling logic. Constructor wiring and imports look consistent with existing patterns.

Also applies to: 16-16, 40-42, 69-79

apps/api/src/core/services/job-queue/job-queue.service.ts (1)

108-116: Job completion API and JobMeta propagation through workers are consistent.

The new complete(name, id) helper and JOB_COMPLETED log mirror cancel nicely, and passing { id: job.id } into handler.handle(job.data, { id: job.id }) lines up with the JobMeta type. Making the second parameter optional in the JobHandler signature keeps backward compatibility while still providing job context where needed. Worker wiring and logging (JOB_STARTED / JOB_DONE / JOB_FAILED) remain coherent after the signature change.

Also applies to: 155-176, 218-225

apps/api/src/core/services/job-queue/job-queue.service.spec.ts (1)

19-25: Tests correctly cover new queue behavior and handler signature.

The specs now verify:

  • Queue options including retryLimit / retryDelayMax.
  • Rejection when multiple handlers target the same queue.
  • enqueue returning a jobId and calling the handler with (payload, { id }) from workers.
  • Execution context wiring via runWithContext.

All of that matches the updated JobQueueService implementation.

Also applies to: 51-65, 68-77, 152-181, 189-193, 210-228, 285-303

apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)

38-46: LGTM!

The updated findByUserId method correctly handles the case where no wallet setting is found by returning undefined, and properly transforms the result using toOutput.

apps/api/src/billing/controllers/stripe/stripe.controller.ts (3)

50-56: LGTM!

The markAsDefault method correctly uses the @Protected decorator with appropriate action/subject, retrieves the paying user, and delegates to the service layer.


58-67: LGTM!

Good implementation that addresses the previous review feedback - the method now properly asserts with a 404 when the payment method is not found.


69-79: LGTM!

The method correctly handles the case where no paying user exists (strict: false) by returning an empty array, providing a safe fallback.

apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts (3)

1-1: LGTM!

Correctly imports MsgAccountDeposit to support the new test cases for deposit message handling.


492-578: LGTM! Good test coverage for the new executeDerivedEncodedTxByUserId method.

The tests properly cover:

  1. Scheduling triggered for MsgCreateDeployment
  2. Scheduling triggered for MsgAccountDeposit
  3. No scheduling for non-spending messages (MsgCreateLease)

Test naming follows the coding guidelines (present simple, 3rd person singular).


630-656: LGTM!

The setup function correctly wires in the new WalletReloadJobService mock and registryMock, and the constructor invocation matches the expected dependency order.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (4)

8-16: LGTM!

The new imports correctly bring in the required types and seeders for the updated test dependencies.


67-103: LGTM!

The test correctly verifies that scheduleForWalletSetting is called with the expected wallet setting object containing id and userId when creating a new setting with auto-reload enabled.


205-232: LGTM!

The test properly verifies that walletReloadJobService.cancel is called with the correct userId and existingJobId when auto-reload is disabled.


276-319: LGTM!

The setup function follows all coding guidelines:

  • Located at bottom of root describe block
  • Creates object under test and returns it
  • Accepts single parameter with inline type definition
  • Avoids shared state
  • Uses jest-mock-extended for mocks

The new dependencies (userRepository, stripeService, walletReloadJobService) are correctly wired.

apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (5)

1-15: LGTM!

Clean imports. The Require utility type is now correctly imported from @src/core/types/require.type as suggested in previous review.


17-30: LGTM!

The type definitions are well-structured. ValidationError now has a consistent shape (event + message only), and the resource types properly constrain the required fields.


61-70: LGTM!

The handle method follows a clean Result-based pattern:

  • Collects resources with validation
  • On success: attempts reload and schedules next check
  • On failure: logs validation error and exits gracefully

165-209: LGTM! Solid reload logic with proper idempotency.

The implementation correctly:

  • Calculates future costs and determines if reload is needed
  • Uses Math.max to ensure minimum $20 reload
  • Applies idempotency key to prevent duplicate charges on retries
  • Logs outcomes (skipped/reloaded/failed)
  • Re-throws errors to trigger job retry

211-225: LGTM! Scheduling errors are now properly propagated.

The method addresses the previous review feedback by re-throwing scheduling errors, allowing pgboss to retry the job when scheduling the next check fails.

apps/api/src/billing/services/stripe/stripe.service.ts (6)

1-1: LGTM - new imports properly support the added functionality.

The new imports (AnyAbility, WithTransaction, PayingUser) are correctly used in the new method signatures and decorators.

Also applies to: 13-13, 17-17


33-33: LGTM - clean type definition.

The exported PaymentMethod type properly extends Stripe's payment method with the validated flag.


146-173: Transaction boundary acknowledged - timeout partially mitigates concern.

The external Stripe API calls now include a 3-second timeout (lines 150, 181), which addresses the connection pool concern raised in previous reviews. The Stripe update is sequenced after local DB operations rather than concurrent, reducing (but not eliminating) inconsistency risk.

Based on learnings, organization field updates to Stripe are considered non-critical and can proceed with this pattern. The timeout helps prevent long-running transactions from exhausting the connection pool.

Note: If Stripe is slow or fails after the timeout, the entire transaction will roll back. Consider monitoring for timeout-related rollbacks to assess if compensating transactions or eventual consistency patterns are needed in the future.


175-183: LGTM - timeout appropriately limits blocking.

The timeout on the Stripe customer update prevents long-running transactions. Errors correctly propagate to trigger transaction rollback.


185-238: LGTM - idempotency key handling is correct.

The conditional inclusion of idempotencyKey and the clearer variable naming (amountInCents) improve the code quality. The idempotency pattern correctly prevents duplicate payment intents.


106-106: LGTM - using the exported type improves consistency.

The return type now uses the exported PaymentMethod type alias defined on line 33.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (7)

6-10: LGTM - new imports support enhanced validation and scheduling.

The new dependencies enable Stripe payment validation and wallet reload job scheduling functionality.


12-16: LGTM - clean input type definition.

The WalletSettingInput interface makes optional fields explicit and serves as a clear contract for the upsert operation.


20-27: LGTM - new dependencies properly support validation and scheduling.

The constructor additions enable payment method validation via Stripe and job scheduling via the wallet reload service.


77-105: LGTM - good race condition handling with retry.

The unique violation catch and retry with #update properly handles concurrent creation attempts. The assertion on line 96 appropriately escalates if the retry also fails.


133-141: LGTM - scheduling logic correctly handles transitions.

The method properly:

  • Schedules a job when transitioning from disabled to enabled (line 134-136)
  • Cancels a job when transitioning to disabled (line 138-140)

Both conditions prevent redundant scheduling/cancellation operations.


43-56: Transaction boundary includes external API calls.

The @WithTransaction decorator wraps validation logic that calls stripeService.getDefaultPaymentMethod() (invoked via #validate on lines 67, 78). This external Stripe API call can hold database connections while waiting for the remote response, similar to the concern raised in stripe.service.ts.

Consider the same mitigation strategy used in stripe.service.ts: add a timeout to the Stripe call to prevent long-running transactions from exhausting the connection pool.

If Stripe calls become a bottleneck, consider refactoring to:

  1. Validate payment method availability outside the transaction
  2. Run the transaction only for database mutations
  3. Unschedule/reschedule jobs in a finally block or separate operation

For now, adding a timeout to the getDefaultPaymentMethod call in the validation would mitigate the immediate concern.

⛔ Skipped due to learnings
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

107-131: Type narrowing is correct and safe.

PayingUser is defined as Require<UserOutput, "stripeCustomerId">, which makes stripeCustomerId required. The assertion on lines 121–122 guarantees stripeCustomerId exists at runtime. The spread { ...user, stripeCustomerId } on line 126 properly narrows the type to satisfy PayingUser requirements without additional properties.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/api/src/billing/services/stripe/stripe.service.ts (1)

122-137: Strengthen the type guard for unexpanded payment methods.

The type check on line 134 doesn't guard against remote being a string ID. If Stripe's expand operation fails or is not properly configured, default_payment_method could be a string rather than an expanded object, which would cause runtime errors when attempting to spread it on line 135.

Apply this diff to add a proper type guard:

-  if (typeof remote === "object" && remote && local) {
+  if (remote && typeof remote === "object" && "type" in remote && local) {
     return { ...remote, validated: local.isValidated };
   }

This ensures remote is an expanded PaymentMethod object with expected properties before attempting to use it.

🧹 Nitpick comments (2)
apps/api/src/billing/services/balances/balances.service.ts (2)

96-99: Memoized full-balance wrapper – behaviorally fine, small design consideration

getFullBalanceMemoized simply delegates to getFullBalance and is memoized with ttlInSeconds: averageBlockTime. That’s a clean way to opt into caching at call sites while keeping the original method available.

If you anticipate that all external callers should always see the memoized behavior, consider (optionally) making getFullBalance internal/private and exposing only the memoized variant to avoid accidental use of the non‑cached path in the future.


116-142: Fiat conversion helpers are consistent; be aware of rounding and decimal assumptions

The fiat helpers (getFullBalanceInFiat*, toFiatAmount, #convertToFiatAmount, ensure2floatingDigits) are coherent and type‑safe, but there are a couple of implicit assumptions worth calling out:

  • toFiatAmount divides by 1_000_000, assuming the underlying token (both uakt and the non‑uakt case) uses 6 decimal places. That matches typical u* / USDC behavior, but if DEPLOYMENT_GRANT_DENOM ever points to a token with different decimals, this will silently mis‑scale values.
  • Each component (balance, deployments) is rounded to 2 decimals, and then total is computed as ensure2floatingDigits(balance + deployments). This can differ slightly from “round once at the very end” semantics due to double rounding. That’s usually fine for display, but if you need strict cent‑level consistency with some external system, you may want to round only once at the end.

Not blockers, but documenting these assumptions (or parameterizing decimals/coin mapping via config) would make the behavior more future‑proof.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 20f5527 and 3a03ea5.

📒 Files selected for processing (3)
  • apps/api/src/billing/controllers/wallet/wallet.controller.ts (1 hunks)
  • apps/api/src/billing/services/balances/balances.service.ts (4 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/src/billing/services/balances/balances.service.ts
  • apps/api/src/billing/controllers/wallet/wallet.controller.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
🧠 Learnings (2)
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-10-31T11:26:42.138Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.138Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/services/stripe/stripe.service.ts
🧬 Code graph analysis (2)
apps/api/src/billing/services/balances/balances.service.ts (3)
apps/api/src/billing/http-schemas/balance.schema.ts (1)
  • GetBalancesResponseOutput (17-17)
apps/api/src/caching/helpers.ts (1)
  • Memoize (21-37)
apps/api/src/utils/constants.ts (1)
  • averageBlockTime (4-4)
apps/api/src/billing/services/stripe/stripe.service.ts (5)
apps/api/src/billing/http-schemas/stripe.schema.ts (2)
  • PaymentMethod (241-241)
  • PaymentIntentResult (245-245)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • PaymentMethod (30-41)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • PayingUser (6-6)
apps/api/src/user/repositories/user/user.repository.ts (1)
  • UserOutput (10-12)
apps/api/src/core/services/tx/tx.service.ts (1)
  • WithTransaction (39-51)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (7)
apps/api/src/billing/controllers/wallet/wallet.controller.ts (1)

98-110: Memoized balance retrieval – confirm staleness is acceptable

getBalances now uses getFullBalanceMemoized, introducing a cache layer (TTL = averageBlockTime, currently 6 seconds) keyed by address and isOldWallet. Response shape and error behavior stay the same, but balances can now be briefly stale. Please confirm this staleness window is acceptable for callers (especially around recent refills/drains or auto‑reload flows).

apps/api/src/billing/services/balances/balances.service.ts (1)

9-21: StatsService injection wiring looks correct

Importing StatsService and injecting it as a private readonly dependency is consistent with existing patterns in this service and keeps the constructor order stable. No issues from a DI or typing perspective.

apps/api/src/billing/services/stripe/stripe.service.ts (5)

1-1: LGTM - Clean type extension and imports.

The new imports and PaymentMethod type definition are well-structured. The type extends Stripe's base type with a validated field, providing a clean API surface for payment method handling.

Also applies to: 13-13, 17-17, 32-33


106-106: LGTM - Improved type consistency.

Using the PaymentMethod type improves maintainability and consistency across the codebase.


139-152: LGTM - Proper error handling for non-existent payment methods.

The try-catch properly handles the case where a payment method doesn't exist, returning false instead of throwing an exception. This aligns with the boolean return type and provides better API ergonomics.


183-191: LGTM - Proper timeout handling for external API call.

The 3-second timeout helps prevent long-held database connections during Stripe API calls.


193-223: LGTM - Clean idempotency key implementation.

The conditional inclusion of the idempotency key is correctly implemented, and the renaming to amountInCents improves code clarity. This follows Stripe SDK patterns properly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/api/src/billing/services/stripe/stripe.service.ts (1)

161-188: Handle non-card payment methods properly.

Lines 175-177 assume all payment methods have a card.fingerprint, but this method may be called with non-card payment methods such as "link" type (enabled in createSetupIntent at line 57). Non-card payment methods don't have the card property, causing the assertion at line 177 to fail inappropriately.

Apply this diff to verify the payment method type:

   const fingerprint = remote.card?.fingerprint;
 
-  assert(fingerprint, 403, "Payment method fingerprint is missing");
+  assert(remote.type === "card" && fingerprint, 403, "Only card payment methods can be set as default at this time");

Alternatively, if you want to support non-card payment methods, implement appropriate unique identifier extraction for each type:

-  const fingerprint = remote.card?.fingerprint;
-
-  assert(fingerprint, 403, "Payment method fingerprint is missing");
+  let uniqueIdentifier: string;
+  
+  if (remote.type === "card") {
+    uniqueIdentifier = remote.card?.fingerprint;
+    assert(uniqueIdentifier, 403, "Card fingerprint is missing");
+  } else {
+    // Handle other payment method types or reject them
+    assert(false, 403, `Payment method type '${remote.type}' is not supported for default payment method`);
+  }
 
   const newLocal = await this.paymentMethodRepository.accessibleBy(ability, "create").createAsDefault({
     userId: user.id,
-    fingerprint,
+    fingerprint: uniqueIdentifier,
     paymentMethodId
   });

Based on past review comments that flagged this same issue.

🧹 Nitpick comments (1)
apps/api/src/billing/services/stripe/stripe.service.ts (1)

124-144: Consider more defensive type checking for the expanded payment method.

Line 136 checks typeof remote === "object" which prevents string IDs but doesn't verify the object has expected PaymentMethod properties. If Stripe's expansion fails unexpectedly, this could lead to runtime errors when accessing properties.

Apply this diff to add property verification:

-    if (typeof remote === "object" && remote && local) {
+    if (typeof remote === "object" && remote && "type" in remote && local) {
       return { ...remote, validated: local.isValidated };

This ensures remote is an expanded PaymentMethod object, not just any object.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a03ea5 and d19a220.

📒 Files selected for processing (2)
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts (2 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (7 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/src/billing/services/stripe/stripe.service.spec.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test

Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

**/*.spec.{ts,tsx}: Use <Subject>.name in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references
Use either a method name or a condition starting with 'when' for nested suite descriptions in tests
Use present simple, 3rd person singular for test descriptions without prepending 'should'

Files:

  • apps/api/src/billing/services/stripe/stripe.service.spec.ts
🧠 Learnings (1)
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/services/stripe/stripe.service.ts
🧬 Code graph analysis (2)
apps/api/src/billing/services/stripe/stripe.service.spec.ts (2)
packages/http-sdk/src/stripe/stripe.service.ts (1)
  • StripeService (18-94)
packages/logging/src/services/logger/logger.service.ts (1)
  • LoggerService (39-165)
apps/api/src/billing/services/stripe/stripe.service.ts (5)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • PaymentMethod (30-41)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • PayingUser (6-6)
apps/api/src/auth/services/auth.service.ts (2)
  • ability (44-46)
  • ability (48-50)
apps/api/src/user/repositories/user/user.repository.ts (1)
  • UserOutput (10-12)
apps/api/src/core/services/tx/tx.service.ts (1)
  • WithTransaction (39-51)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
apps/api/src/billing/services/stripe/stripe.service.ts (5)

1-33: LGTM!

The new imports and PaymentMethod type definition are well-structured. Exporting the type allows consumers to benefit from proper type inference when working with validated payment methods.


39-51: LGTM!

The constructor properly injects the new dependencies and sets the logger context. The dependency injection pattern is followed correctly.


146-159: LGTM!

The method properly handles non-existent payment methods by catching Stripe's resource_missing error and returning false, while re-throwing unexpected errors. This addresses the previous review feedback effectively.


190-253: LGTM!

The markRemotePaymentMethodAsDefault method properly updates the Stripe customer with a sensible timeout. The createPaymentIntent refactoring cleanly handles the optional idempotencyKey parameter, and the renamed amountInCents variable improves code clarity.


408-410: LGTM!

The simplified return statement has no functional impact and slightly reduces verbosity.

apps/api/src/billing/services/stripe/stripe.service.spec.ts (1)

1-1422: LGTM!

The test file properly injects the mock LoggerService into the StripeService constructor, aligning with the service's updated signature. All tests continue to exercise the service correctly with the new dependency.

baktun14
baktun14 previously approved these changes Dec 2, 2025
Implement daily wallet balance checks that automatically reload funds when
balance falls below 25% of 7-day deployment costs for auto top-up enabled
deployments. Includes immediate triggers on feature enable, deployment
creation, and deposits. Uses new WalletReloadJobService to manage job
scheduling and break dependency cycles.

refs #1779
@ygrishajev ygrishajev force-pushed the feature/auto-reload-handler branch from fd3b6e4 to de560e9 Compare December 3, 2025 09:34
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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/deployment/repositories/deployment-setting/deployment-setting.repository.ts (1)

51-68: Joins on UserWallets are not applied – Drizzle query builder must be reassigned

Drizzle's query builder is immutable by design; methods like innerJoin() and leftJoin() return a new builder rather than mutating in place. Calling these methods on query without reassigning means the joins are never actually added to the final query. The WHERE clauses still reference UserWallets.* columns, which will cause a runtime SQL error.

Reassign the query on each join call:

-      const query = this.pg
+      let query = this.pg
         .select({
           id: this.table.id,
           dseq: this.table.dseq,
           walletId: UserWallets.id,
           address: UserWallets.address,
           isOldWallet: UserWallets.isOldWallet
         })
         .from(this.table)
-        .leftJoin(Users, eq(this.table.userId, Users.id));
-
-      if (options.address) {
-        query.innerJoin(UserWallets, eq(Users.id, UserWallets.userId));
+        .leftJoin(Users, eq(this.table.userId, Users.id));
+
+      if (options.address) {
+        query = query.innerJoin(UserWallets, eq(Users.id, UserWallets.userId));
         clauses.push(eq(UserWallets.address, options.address));
       } else {
-        query.leftJoin(UserWallets, eq(Users.id, UserWallets.userId));
+        query = query.leftJoin(UserWallets, eq(Users.id, UserWallets.userId));
         clauses.push(isNotNull(UserWallets.address));
       }
♻️ Duplicate comments (4)
apps/api/src/core/services/job-queue/job-queue.service.ts (1)

89-94: Avoid logging full job payload to prevent leaking PII.

The JOB_ENQUEUED event currently logs the entire job object. For jobs like the NotificationJob example in the docstring (with to, subject, body), this risks logging email addresses and message content at info level.

Recommend logging only non-sensitive metadata (name, id, version) instead of the full payload.

Apply this diff to log only non-sensitive metadata:

     this.logger.info({
       event: "JOB_ENQUEUED",
-      job,
+      jobName: job.name,
       jobId,
-      options
+      version: job.version
     });
apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (1)

144-164: Good improvement on attach handler logging, but detach handler still logs fingerprint.

The attach handler correctly removed fingerprint from the logged payload (line 163), addressing the PII concern from the previous review. The isDefault flag is appropriate for logging.

However, the detach handler (lines 173-178, 207-212) still logs fingerprint at info level, which is sensitive payment metadata.

Consider removing or hashing the fingerprint in detach logs as well:

     this.logger.info({
       event: "PAYMENT_METHOD_DETACHED",
       paymentMethodId: paymentMethod.id,
-      customerId,
-      fingerprint
+      customerId
     });

Also applies to: 173-178, 207-212

apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)

70-77: createAsDefault remains concurrently unsafe; avoid Promise.all here

Running this.create(...) and #unmarkAsDefaultExcluding(...) in parallel can still yield two isDefault = true records when createAsDefault is called concurrently for the same user: both transactions can execute their unmark step before either insert is visible, and each then inserts a new default.

Given this is already known and was previously flagged as non-blocking, consider serializing the operations inside the transaction instead of using Promise.all, and ensure both use the same tx:

async createAsDefault(input: Omit<PaymentMethodInput, "id" | "isDefault">) {
-  return this.ensureTransaction(async tx => {
-    const id = uuidv4();
-    const [output] = await Promise.all([
-      this.create({
-        ...input,
-        isDefault: true,
-        id
-      }),
-      this.#unmarkAsDefaultExcluding(id, tx)
-    ]);
-
-    return this.toOutput(output);
-  });
+  return this.ensureTransaction(async tx => {
+    const id = uuidv4();
+
+    await this.#unmarkAsDefaultExcluding(id, tx);
+
+    const [output] = await tx
+      .insert(this.table)
+      .values({
+        ...input,
+        isDefault: true,
+        id
+      })
+      .returning();
+
+    return this.toOutput(output);
+  });
}

That way, each transaction atomically “unmarks others then inserts the new default,” reducing the chance of overlapping defaults under load.

Also applies to: 79-118

apps/api/src/billing/services/stripe/stripe.service.ts (1)

124-188: Marking non‑card payment methods as default will fail with a 403

In markPaymentMethodAsDefault, the branch that creates a local record for a previously unseen method assumes remote.card?.fingerprint exists:

const fingerprint = remote.card?.fingerprint;
assert(fingerprint, 403, "Payment method fingerprint is missing");

But createSetupIntent allows "card" and "link" types. For non‑card methods (e.g. type === "link"), remote.card is undefined, so this throws a 403 even though the payment method may be perfectly valid to use and set as default.

If you only intend to support cards as defaults for now, make that explicit and update the message; otherwise you’ll need a type‑specific identifier strategy for non‑card methods.

For a minimal fix:

-    const fingerprint = remote.card?.fingerprint;
-
-    assert(fingerprint, 403, "Payment method fingerprint is missing");
+    const isCard = remote.type === "card";
+    const fingerprint = isCard ? remote.card?.fingerprint : undefined;
+
+    assert(isCard && fingerprint, 403, "Only card payment methods can be set as default at this time");

Optionally, also narrow the setup intent’s payment_method_types if you don’t plan to support non‑card defaults in the near term.

Please verify behaviour by marking a non‑card (e.g. "link") payment method as default in a dev environment and ensure the API either works as intended or fails with a clear, documented 4xx error that matches your product requirements.

🧹 Nitpick comments (10)
apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts (1)

19-27: Default payment-method flag and partial unique index look correct

isDefault plus the partial unique index ensures at most one default payment method per user and keeps existing non-default rows unaffected. Including isDefault in the indexed columns is slightly redundant given the WHERE is_default = true filter but not harmful.

apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts (1)

17-18: Wallet reload scheduling tests and setup wiring look solid

The new executeDerivedEncodedTxByUserId tests correctly exercise wallet reload scheduling for spending vs non‑spending messages, and the extended setup neatly wires WalletReloadJobService and a typed Registry mock via jest-mock-extended. Constructor arguments for ManagedSignerService remain coherent, and the test style still aligns with the local testing guidelines.

Also applies to: 492-578, 593-656

apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts (1)

4-6: Comprehensive tests and setup for draining cost logic; be aware of coupling to current formula

The refactored tests:

  • Use typed mocks (jest-mock-extended + mockConfigService) and an inline setup at the bottom, matching the project’s testing conventions.
  • Correctly update paginateAutoTopUpDeployments mock signatures to { address?: string; limit: number }.
  • Add focused scenarios for calculateAllDeploymentCostUntilDate that exercise missing wallets, missing addresses, empty deployments, and boundary conditions on predictedClosedHeight.

These tests tightly encode the current formula (blocksNeeded = targetHeight - currentHeight). If you adjust the service to use predictedClosedHeight in the cost calculation, remember to update the expectations in calculateAllDeploymentCostUntilDate tests accordingly.

Also applies to: 15-16, 32-118, 129-193, 195-352, 355-382

apps/api/src/billing/services/balances/balances.service.ts (1)

9-10: StatsService fiat integration and memoized balance APIs look consistent

The new memoized getters and fiat helpers keep units coherent (uTokens → tokens → fiat) and reuse averageBlockTime for TTL, and the StatsService injection cleanly encapsulates conversion logic via #convertToFiatAmount. Overall the additions integrate well with existing balance calculations.

If you expect convertToFiatAmount to be relatively expensive (e.g., external I/O), you might optionally parallelize the two conversions in getFullBalanceInFiat:

const [balance, deployments] = await Promise.all([
  this.toFiatAmount(data.balance),
  this.toFiatAmount(data.deployments)
]);
const total = this.ensure2floatingDigits(balance + deployments);

Also applies to: 19-21, 96-143

apps/api/src/billing/services/wallet-balance-reload-check/README.md (1)

53-99: Add languages to fenced code blocks (markdownlint MD040) and optionally tighten wording

markdownlint is right that the three fenced blocks would benefit from explicit languages and will otherwise keep flagging MD040. For example:

-``` 
+```text
 User enables auto-reload
 ...
-```
+```text
 balance < 0.25 * costUntilTargetDateInFiat
-```
+```text
 reloadAmount = max(costUntilTargetDateInFiat - balance, $20)

You might also optionally shorten “emergency situations” (e.g., to “emergencies”) if you want to follow the style suggestion, but that’s purely cosmetic.

Also applies to: 104-107, 150-155

apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)

48-65: Consider adding explicit return type annotation.

The findInternalByUserIdWithRelations method relies on type inference for the complex return type with relations. Adding an explicit return type would improve readability and catch schema mismatches at compile time.

-  async findInternalByUserIdWithRelations(userId: WalletSettingOutput["userId"]) {
+  async findInternalByUserIdWithRelations(userId: WalletSettingOutput["userId"]): Promise<
+    | (DbWalletSettingOutput & {
+        wallet: { address: string | null; isOldWallet: boolean };
+        user: ApiPgTables["User"]["$inferSelect"];
+      })
+    | undefined
+  > {

Alternatively, define a dedicated type for this composite return shape for reuse.

apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1)

292-300: Simplify the user stripeCustomerId logic.

The nested ternary condition is hard to follow. Consider simplifying:

-    const userWithStripe =
-      input?.user && input.user.stripeCustomerId === null
-        ? user
-        : input?.user && input.user.stripeCustomerId
-          ? user
-          : user.stripeCustomerId
-            ? user
-            : { ...user, stripeCustomerId: faker.string.uuid() };
+    const userWithStripe = user.stripeCustomerId 
+      ? user 
+      : { ...user, stripeCustomerId: faker.string.uuid() };

The original logic appears to preserve null only when explicitly passed via input?.user, but the nested conditions are redundant since input?.user is already assigned to user on line 292.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (1)

49-49: Replace as any cast with proper typing.

Per coding guidelines, avoid using any. The generateWalletSetting return type should match WalletSettingOutput or the mock should be typed appropriately.

-      walletSettingRepository.updateById.mockResolvedValue(updatedSetting as any);
+      walletSettingRepository.updateById.mockResolvedValue(updatedSetting);

If there's a type mismatch, consider updating generateWalletSetting to return the correct type or use a type assertion to WalletSettingOutput instead of any. As per coding guidelines, never use type any.

apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (1)

5-7: Default-payment-method routes and OpenAPI wiring look consistent

Imports, route definitions, security, and schemas line up with StripeController.markAsDefault / getDefaultPaymentMethod and the zod schemas. One minor nit: markAsDefault returns c.json(undefined, 200) while the OpenAPI response has no content schema; you may want to switch this to 204 with c.body(null, 204) for tighter alignment with the documented empty body, similar to removePaymentMethod.

Also applies to: 45-70, 72-99

apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (1)

43-56: Upsert flow and scheduling are well structured; consider moving Stripe validation out of the transaction

The refactor to upsertWalletSetting with #update, #create, #validate, and #arrangeSchedule nicely separates concerns:

  • Validation enforces that threshold and amount are present when enabling auto‑reload and that the user has a Stripe customer and a default payment method.
  • #arrangeSchedule cleanly encapsulates scheduling/cancellation via WalletReloadJobService, and deleteWalletSetting now consistently cancels any existing job.

One thing to watch: #validate calls stripeService.getDefaultPaymentMethod from within a @WithTransaction() method. That means a network call to Stripe runs while a DB transaction is open, which can hold a connection longer than necessary if Stripe is slow.

If feasible, consider performing the “has default payment method” check before opening the transaction (or add a short timeout to getDefaultPaymentMethod) so the critical DB section only covers the actual create/update + scheduling work.

Please confirm, based on your TxService implementation and DB connection limits, that running the Stripe lookup inside upsertWalletSetting’s transaction won’t risk exhausting connections under load; if it does, it’s worth refactoring the validation step to occur before the transaction starts.

Also applies to: 58-80, 92-131, 133-152

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d19a220 and de560e9.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (46)
  • apps/api/drizzle.config.ts (1 hunks)
  • apps/api/drizzle/0023_clumsy_vertigo.sql (1 hunks)
  • apps/api/drizzle/meta/0023_snapshot.json (1 hunks)
  • apps/api/drizzle/meta/_journal.json (1 hunks)
  • apps/api/src/auth/services/ability/ability.service.ts (2 hunks)
  • apps/api/src/auth/services/auth.service.ts (2 hunks)
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts (2 hunks)
  • apps/api/src/billing/controllers/wallet/wallet.controller.ts (1 hunks)
  • apps/api/src/billing/http-schemas/stripe.schema.ts (3 hunks)
  • apps/api/src/billing/http-schemas/wallet.schema.ts (0 hunks)
  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts (1 hunks)
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1 hunks)
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts (2 hunks)
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2 hunks)
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (2 hunks)
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1 hunks)
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (5 hunks)
  • apps/api/src/billing/routes/stripe-webhook/stripe-webhook.router.ts (1 hunks)
  • apps/api/src/billing/services/balances/balances.service.ts (4 hunks)
  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts (6 hunks)
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts (4 hunks)
  • apps/api/src/billing/services/paying-user/paying-user.ts (1 hunks)
  • apps/api/src/billing/services/refill/refill.service.ts (1 hunks)
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (2 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts (2 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (7 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/README.md (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2 hunks)
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts (2 hunks)
  • apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts (1 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (11 hunks)
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (6 hunks)
  • apps/api/src/core/repositories/base.repository.ts (2 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts (9 hunks)
  • apps/api/src/core/services/job-queue/job-queue.service.ts (4 hunks)
  • apps/api/src/core/services/tx/tx.service.ts (1 hunks)
  • apps/api/src/core/types/require.type.ts (1 hunks)
  • apps/api/src/dashboard/services/stats/stats.service.ts (1 hunks)
  • apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts (4 hunks)
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts (7 hunks)
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.ts (1 hunks)
  • apps/api/test/seeders/balance.seeder.ts (2 hunks)
  • apps/api/test/seeders/database-payment-method.seeder.ts (1 hunks)
  • apps/api/test/seeders/payment-method.seeder.ts (2 hunks)
  • packages/http-sdk/src/stripe/stripe.service.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • packages/http-sdk/src/stripe/stripe.service.ts
  • apps/api/src/billing/http-schemas/wallet.schema.ts
🚧 Files skipped from review as they are similar to previous changes (14)
  • apps/api/test/seeders/balance.seeder.ts
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/dashboard/services/stats/stats.service.ts
  • apps/api/src/core/types/require.type.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
  • apps/api/src/auth/services/auth.service.ts
  • apps/api/src/auth/services/ability/ability.service.ts
  • apps/api/src/core/repositories/base.repository.ts
  • apps/api/src/billing/services/refill/refill.service.ts
  • apps/api/drizzle/meta/0023_snapshot.json
  • apps/api/src/billing/http-schemas/stripe.schema.ts
  • apps/api/src/core/services/tx/tx.service.ts
  • apps/api/drizzle/0023_clumsy_vertigo.sql
  • apps/api/src/billing/controllers/wallet/wallet.controller.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts
  • apps/api/drizzle.config.ts
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.ts
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts
  • apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts
  • apps/api/test/seeders/payment-method.seeder.ts
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts
  • apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts
  • apps/api/src/billing/services/balances/balances.service.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts
  • apps/api/test/seeders/database-payment-method.seeder.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
  • apps/api/src/billing/model-schemas/payment-method/payment-method.schema.ts
  • apps/api/src/billing/routes/stripe-webhook/stripe-webhook.router.ts
  • apps/api/src/billing/services/paying-user/paying-user.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
  • apps/api/src/core/services/job-queue/job-queue.service.ts
  • apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test

Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

**/*.spec.{ts,tsx}: Use <Subject>.name in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references
Use either a method name or a condition starting with 'when' for nested suite descriptions in tests
Use present simple, 3rd person singular for test descriptions without prepending 'should'

Files:

  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts
  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
🧠 Learnings (18)
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/services/stripe/stripe.service.ts
📚 Learning: 2025-09-04T04:27:50.638Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/billing/services/managed-signer/managed-signer.service.ts:1-1
Timestamp: 2025-09-04T04:27:50.638Z
Learning: In the akash-network/console project, importing MsgCreateLease from "akashnetwork/akash-api/v1beta3" instead of v1beta4 is considered non-critical by the maintainers, likely due to backward compatibility or project-specific requirements.

Applied to files:

  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-09-04T04:27:40.858Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/billing/services/managed-signer/managed-signer.service.ts:98-107
Timestamp: 2025-09-04T04:27:40.858Z
Learning: In the Akash protocol, when processing MsgCreateLease messages, the bidId.dseq field is always required and guaranteed to be present at runtime, even though TypeScript types may suggest it could be undefined. The non-null assertion operator (!) is appropriate to use in this context.

Applied to files:

  • apps/api/src/billing/services/managed-signer/managed-signer.service.spec.ts
📚 Learning: 2025-11-28T09:17:28.832Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2274
File: apps/api/drizzle/0023_sad_adam_warlock.sql:1-1
Timestamp: 2025-11-28T09:17:28.832Z
Learning: In the Akash Network console billing system, the `user_wallets.user_id` column is required (NOT NULL). All user wallets must be associated with a user ID, and there are no anonymous wallets without a user_id in production.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
  • apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts
📚 Learning: 2025-11-28T09:21:45.610Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2274
File: apps/api/drizzle/0023_sad_adam_warlock.sql:1-1
Timestamp: 2025-11-28T09:21:45.610Z
Learning: In the Akash Network console, `userSettings.userId` can be NULL for anonymous users, but `user_wallets.user_id` is always required (NOT NULL). Anonymous users are identified by userId=null in the userSettings table, not in the user_wallets table.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
📚 Learning: 2025-09-29T02:40:22.090Z
Learnt from: stalniy
Repo: akash-network/console PR: 1951
File: apps/api/src/user/services/user/user.service.ts:48-63
Timestamp: 2025-09-29T02:40:22.090Z
Learning: In the Akash Network Console codebase, the users table was originally created by Sequelize and later migrated to Drizzle. Due to this migration history, the unique constraint on the userId field is named "userSetting_userId_unique" rather than following the typical "users_user_id_unique" pattern.

Applied to files:

  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
  • apps/api/src/billing/model-schemas/wallet-setting/wallet-setting.schema.ts
📚 Learning: 2025-11-25T17:45:44.790Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/no-jest-mock.mdc:0-0
Timestamp: 2025-11-25T17:45:44.790Z
Learning: Applies to **/*.spec.{ts,tsx} : Don't use `jest.mock()` in test files. Instead, use `jest-mock-extended` to create mocks and pass mocks as dependencies to the service under test

Applied to files:

  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:52.965Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/setup-instead-of-before-each.mdc:0-0
Timestamp: 2025-11-25T17:45:52.965Z
Learning: Applies to **/*.spec.{ts,tsx} : Use `setup` function instead of `beforeEach` in test files. The `setup` function must be at the bottom of the root `describe` block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

Applied to files:

  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-25T17:45:58.258Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/test-descriptions.mdc:0-0
Timestamp: 2025-11-25T17:45:58.258Z
Learning: Applies to **/*.spec.{ts,tsx} : Use `<Subject>.name` in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references

Applied to files:

  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
📚 Learning: 2025-11-25T17:45:49.180Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/query-by-in-tests.mdc:0-0
Timestamp: 2025-11-25T17:45:49.180Z
Learning: Applies to {apps/deploy-web,apps/provider-console}/**/*.spec.tsx : Use `queryBy` methods instead of `getBy` methods in test expectations

Applied to files:

  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-11-19T15:15:07.283Z
Learnt from: ygrishajev
Repo: akash-network/console PR: 2254
File: apps/api/test/functional/sign-and-broadcast-tx.spec.ts:4-4
Timestamp: 2025-11-19T15:15:07.283Z
Learning: In the Akash Network Console project, when tests use native Node.js fetch (available in Node 18+), fetch-mock should be used for HTTP mocking instead of nock, as nock does not support intercepting native fetch calls. This applies to apps/api/test/functional/sign-and-broadcast-tx.spec.ts and any other tests using native fetch.

Applied to files:

  • apps/api/src/deployment/services/draining-deployment/draining-deployment.service.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-10-15T16:39:55.348Z
Learnt from: jzsfkzm
Repo: akash-network/console PR: 2039
File: apps/deploy-web/tests/ui/change-wallets.spec.ts:4-10
Timestamp: 2025-10-15T16:39:55.348Z
Learning: In the Akash Console E2E tests using the context-with-extension fixture, the first wallet is automatically created during fixture setup via `importWalletToLeap` in `apps/deploy-web/tests/ui/fixture/wallet-setup.ts`, so tests that call `frontPage.createWallet()` are creating a second wallet to test wallet switching functionality.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
📚 Learning: 2025-10-31T11:26:42.138Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.138Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/repositories/payment-method/payment-method.repository.ts
  • apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
📚 Learning: 2025-09-25T14:31:44.914Z
Learnt from: baktun14
Repo: akash-network/console PR: 1969
File: apps/deploy-web/src/pages/payment.tsx:179-191
Timestamp: 2025-09-25T14:31:44.914Z
Learning: The payment confirmation endpoint in apps/api/src/billing/http-schemas/stripe.schema.ts uses zod schema validation with `amount: z.number().gte(20, "Amount must be greater or equal to $20")` to ensure all payment requests meet the minimum amount requirement, preventing zero-amount or invalid payments from reaching Stripe.

Applied to files:

  • apps/api/src/billing/controllers/stripe/stripe.controller.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: The apps/api codebase consistently uses .toISOString() for startAfter values when scheduling jobs with JobQueueService to ensure UTC timezone consistency. This pattern is used throughout trial-started.handler.ts, trial-deployment-lease-created.handler.ts and other job scheduling code. Do not suggest changing to Date objects as ISO strings provide explicit UTC representation for database storage.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-07-24T17:00:52.361Z
Learnt from: baktun14
Repo: akash-network/console PR: 1725
File: apps/api/src/utils/constants.ts:5-5
Timestamp: 2025-07-24T17:00:52.361Z
Learning: In the Akash Network Console project, when cross-cutting concerns or broader refactoring issues are identified during PR review, the preferred approach is to create a separate GitHub issue to track the work rather than expanding the scope of the current PR. This maintains focus and allows for proper planning of architectural improvements.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
📚 Learning: 2025-09-04T04:21:26.067Z
Learnt from: stalniy
Repo: akash-network/console PR: 1868
File: apps/api/src/app/services/trial-deployment-lease-created/trial-deployment-lease-created.handler.ts:73-76
Timestamp: 2025-09-04T04:21:26.067Z
Learning: In apps/api job scheduling with JobQueueService, use .toISOString() for startAfter values instead of Date objects to ensure UTC timezone consistency when dates are persisted to the database. Date objects can be ambiguous about timezone during serialization, while ISO strings with .toISOString() explicitly represent UTC.

Applied to files:

  • apps/api/src/core/services/job-queue/job-queue.service.spec.ts
  • apps/api/src/core/services/job-queue/job-queue.service.ts
📚 Learning: 2025-11-25T17:45:39.561Z
Learnt from: CR
Repo: akash-network/console PR: 0
File: .cursor/rules/general.mdc:0-0
Timestamp: 2025-11-25T17:45:39.561Z
Learning: Applies to **/*.{ts,tsx,js} : Never use type `any` or cast to type `any`. Always define the proper TypeScript types.

Applied to files:

  • apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts
🧬 Code graph analysis (17)
apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (2)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • user (137-156)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • assertIsPayingUser (8-10)
apps/api/src/billing/services/stripe/stripe.service.spec.ts (1)
packages/logging/src/services/logger/logger.service.ts (1)
  • LoggerService (39-165)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (3)
apps/api/src/auth/services/auth.service.ts (1)
  • Protected (62-78)
apps/api/src/billing/http-schemas/stripe.schema.ts (5)
  • PaymentMethodMarkAsDefaultInput (240-240)
  • PaymentMethodResponse (243-243)
  • PaymentMethodsResponse (242-242)
  • ConfirmPaymentRequest (244-244)
  • ConfirmPaymentResponse (247-247)
apps/api/src/core/lib/semaphore.decorator.ts (1)
  • Semaphore (3-28)
apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (1)
apps/api/src/core/providers/postgres.provider.ts (1)
  • ApiPgTables (80-80)
apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts (3)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (2)
  • singleton (24-66)
  • WalletSettingOutput (19-22)
apps/api/src/core/services/job-queue/job-queue.service.ts (2)
  • singleton (11-197)
  • EnqueueOptions (227-227)
apps/api/src/billing/events/wallet-balance-reload-check.ts (1)
  • WalletBalanceReloadCheck (5-15)
apps/api/test/seeders/payment-method.seeder.ts (3)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (241-241)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • PaymentMethod (30-41)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (2)
apps/api/src/core/providers/postgres.provider.ts (1)
  • ApiPgTables (80-80)
apps/api/src/core/services/tx/tx.service.ts (1)
  • ApiTransaction (11-11)
apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts (2)
apps/api/src/user/model-schemas/user/user.schema.ts (1)
  • Users (8-28)
apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (1)
  • UserWallets (5-18)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (4)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (2)
  • WalletSettingInput (12-17)
  • WalletSettingOutput (19-22)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2)
  • error (158-163)
  • user (137-156)
apps/api/src/auth/services/auth.service.ts (2)
  • ability (44-46)
  • ability (48-50)
apps/api/src/core/repositories/base.repository.ts (1)
  • isUniqueViolation (237-239)
apps/api/test/seeders/database-payment-method.seeder.ts (1)
apps/api/src/billing/repositories/payment-method/payment-method.repository.ts (1)
  • PaymentMethodOutput (11-11)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (9)
apps/api/src/core/types/require.type.ts (1)
  • Require (1-3)
apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (1)
  • UserWalletOutput (18-22)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)
  • WalletSettingOutput (19-22)
apps/api/src/billing/services/paying-user/paying-user.ts (2)
  • PayingUser (6-6)
  • isPayingUser (12-14)
apps/api/src/billing/http-schemas/balance.schema.ts (1)
  • GetBalancesResponseOutput (17-17)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (241-241)
apps/api/src/core/services/job-queue/job-queue.service.ts (2)
  • JobPayload (209-209)
  • JobMeta (218-218)
apps/api/src/billing/events/wallet-balance-reload-check.ts (1)
  • WalletBalanceReloadCheck (5-15)
apps/api/src/core/services/job-queue/job-queue.service.spec.ts (1)
apps/api/src/core/services/job-queue/job-queue.service.ts (2)
  • setup (185-192)
  • Job (199-207)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (4)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • user (137-156)
apps/api/test/seeders/payment-method.seeder.ts (1)
  • generatePaymentMethod (11-56)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (241-241)
apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (5)
apps/api/src/billing/services/stripe/stripe.service.ts (2)
  • createSetupIntent (53-59)
  • getDefaultPaymentMethod (124-144)
apps/api/src/billing/controllers/stripe/stripe.controller.ts (3)
  • createSetupIntent (41-48)
  • markAsDefault (51-56)
  • getDefaultPaymentMethod (59-67)
packages/http-sdk/src/stripe/stripe.service.ts (1)
  • createSetupIntent (23-25)
apps/api/src/core/services/openapi-docs/openapi-security.ts (1)
  • SECURITY_BEARER_OR_API_KEY (5-5)
apps/api/src/billing/http-schemas/stripe.schema.ts (2)
  • PaymentMethodMarkAsDefaultInputSchema (10-14)
  • PaymentMethodResponseSchema (61-63)
apps/api/src/core/services/job-queue/job-queue.service.ts (1)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • job (72-90)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (2)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)
  • userId (92-135)
apps/api/src/billing/services/wallet-settings/wallet-settings.service.ts (2)
  • userId (58-75)
  • userId (77-105)
apps/api/src/billing/services/stripe/stripe.service.ts (5)
packages/http-sdk/src/stripe/stripe.types.ts (1)
  • PaymentMethod (30-41)
apps/api/src/billing/services/paying-user/paying-user.ts (1)
  • PayingUser (6-6)
apps/api/src/auth/services/auth.service.ts (2)
  • ability (44-46)
  • ability (48-50)
apps/api/src/user/repositories/user/user.repository.ts (1)
  • UserOutput (10-12)
apps/api/src/core/services/tx/tx.service.ts (1)
  • WithTransaction (39-51)
🪛 LanguageTool
apps/api/src/billing/services/wallet-balance-reload-check/README.md

[style] ~39-~39: ‘emergency situations’ might be wordy. Consider a shorter alternative.
Context: ... margin before funds run out - Prevents emergency situations ## When Does the Check Run? The handl...

(EN_WORDINESS_PREMIUM_EMERGENCY_SITUATIONS)

🪛 markdownlint-cli2 (0.18.1)
apps/api/src/billing/services/wallet-balance-reload-check/README.md

53-53: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


104-104: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


150-150: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
🔇 Additional comments (38)
apps/api/drizzle.config.ts (1)

5-9: Env‑driven DB URL with explicit guard looks good

Reading POSTGRES_DB_URI from process.env and failing fast when it’s missing is a solid, simple setup for drizzle‑kit, and the value is correctly wired into dbCredentials.url. No issues from a correctness or typing perspective here.

Also applies to: 16-16

apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts (2)

9-10: Technical implementation is correct; defer to existing review for breaking change confirmation.

The syntax and schema structure are correctly implemented. The past review comment comprehensively addresses the breaking change implications of adding .notNull() to userId. No new concerns identified beyond what was already flagged.


1-27: Schema structure is well-defined and follows best practices.

The schema implementation is clean and follows Drizzle ORM conventions properly:

  • Proper TypeScript types throughout (no any usage)
  • Appropriate constraints and defaults
  • Good use of the helper function for allowance fields to maintain DRY principle
  • Follows coding guidelines
apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts (2)

5-5: LGTM! Correct import cleanup.

Removing UserWalletInput from the imports is appropriate since it's no longer used in the file after the method signature simplification.


53-53: Type signature simplification is correct and improves type safety.

The initialize method parameter type of userId: string is safe and compatible with its single caller in refill.service.ts, which passes userId: UserWalletOutput["userId"] (a non-optional string). No type compatibility issues found.

apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (2)

9-10: Good type refinement for insert vs. update distinction.

Introducing DbCreateUserWalletInput as the concrete insert type and deriving DbUserWalletInput as its partial makes the type system more explicit about required vs. optional fields. This aligns well with the broader typing improvements across the billing repositories.


72-83: Method signature correctly aligns with database operations.

The create method signature using Pick<DbCreateUserWalletInput, "userId" | "address"> accurately reflects the database schema, where userId is required and address is optional. The implementation is sound, and all call sites—including the newly discovered one in wallet-initializer.service.ts:54—are type-safe and compatible with the signature.

apps/api/src/billing/routes/stripe-webhook/stripe-webhook.router.ts (1)

11-51: Stripe webhook handler wiring matches expected contract

The route definition and handler correctly require the stripe-signature header, pass the raw text/plain request body to CheckoutController.webhook, and return appropriate 400/200 responses. This aligns with Stripe’s signature verification needs and keeps the OpenAPI description in sync with runtime behavior.

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

31-40: LGTM!

The relations are correctly defined following Drizzle ORM conventions, with proper references to the foreign keys defined in the WalletSetting table schema.

apps/api/test/seeders/payment-method.seeder.ts (1)

58-61: LGTM!

The new seeder function correctly merges the validated field with the Stripe payment method data while maintaining proper type safety.

apps/api/src/billing/services/paying-user/paying-user.ts (1)

6-14: LGTM!

The PayingUser utilities are well-implemented:

  • Type predicate and assertion function follow TypeScript best practices
  • HTTP 402 (Payment Required) is semantically appropriate
  • Clean, focused implementation
apps/api/src/core/services/job-queue/job-queue.service.ts (2)

108-115: LGTM!

The new complete() method correctly delegates to pg-boss and logs the completion event with appropriate metadata.


218-224: LGTM!

The JobMeta type and updated JobHandler signature are well-designed:

  • JobMeta appropriately narrows to just the id field
  • Making the job parameter optional maintains backward compatibility

Note: Per past review comments, the breaking change to handler signatures was addressed in commit a45589e.

apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts (1)

147-147: LGTM!

The assertIsPayingUser precondition correctly narrows the user type and ensures payment setup before proceeding with Stripe operations.

apps/api/test/seeders/database-payment-method.seeder.ts (1)

3-20: LGTM!

The seeder correctly adopts the PaymentMethodOutput type from the repository and includes the new isDefault field, improving type consistency across the codebase.

apps/api/src/billing/services/stripe/stripe.service.spec.ts (1)

7-7: LGTM!

The test correctly reflects the updated StripeService constructor signature by adding the logger dependency, maintaining consistency with the service implementation.

Also applies to: 1422-1422

apps/api/src/billing/services/wallet-reload-job/wallet-reload-job.service.ts (2)

30-53: Transaction and job lifecycle management looks good.

The method correctly:

  • Wraps operations in a transaction for atomicity
  • Handles previous job cleanup (cancel or complete) before scheduling new job
  • Updates wallet setting with new job ID before enqueueing
  • Validates job creation and throws on failure

56-58: LGTM!

The cancel method correctly delegates to the job queue service with the appropriate job name.

apps/api/src/core/services/job-queue/job-queue.service.spec.ts (5)

22-25: LGTM!

Queue configuration expectations are correctly updated to match the new defaults (retryDelayMax: 300s, retryLimit: 5, policy: undefined).


68-76: LGTM!

Good addition of a test case verifying that duplicate handler registration for the same queue throws an appropriate error. This prevents silent conflicts when multiple handlers accidentally target the same queue.


95-101: LGTM!

Updated log expectations correctly capture the new jobId and options fields in the JOB_ENQUEUED event, providing better traceability for debugging and monitoring.


152-176: LGTM!

The test correctly reflects the updated job processing pattern with single-item job arrays and context object containing { id: job.id }. The mock implementation aligns with the PgBoss.WorkHandler signature.


300-303: Type cast is acceptable for mock compatibility.

The cast to ExecutionContextService["runWithContext"] is necessary to satisfy the mock's type requirements while maintaining the correct callback signature (() => Promise<unknown>).

apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)

38-46: LGTM!

The explicit Promise<WalletSettingOutput | undefined> return type improves type safety and aligns with the consistent pattern of returning undefined when no record is found.

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

21-72: LGTM!

Comprehensive test for the primary reload trigger scenario. The test validates:

  • Cost calculation with 7-day lookahead
  • Next check scheduling for 1 day
  • Payment intent creation with correct amount and idempotency key
  • Proper event logging

The assertions use toBeCloseTo appropriately for date comparisons.


282-373: LGTM on setup function structure.

The setup function correctly:

  • Is placed at the bottom of the root describe block
  • Accepts a single parameter with inline type definition
  • Returns all necessary test doubles and data
  • Avoids shared state
  • Does not have a specified return type

As per coding guidelines, this follows the expected pattern.

apps/api/src/billing/controllers/stripe/stripe.controller.ts (4)

50-56: LGTM!

The markAsDefault method correctly:

  • Uses the @Protected decorator for authorization
  • Retrieves the paying user for customer context
  • Passes ability for access control in the service layer

58-67: LGTM!

The 404 assertion is now correctly implemented, addressing the past review comment. The method properly validates that a default payment method exists before returning.


69-79: LGTM!

Good defensive handling for non-paying users. Using strict: false and returning an empty array when no paying user exists provides a graceful fallback.


84-89: LGTM!

The ownership check is now properly awaited, addressing the critical security issue from past review comments. The assert(await this.stripe.hasPaymentMethod(...)) pattern correctly evaluates the resolved boolean value.

apps/api/src/billing/services/wallet-settings/wallet-settings.service.spec.ts (5)

8-10: LGTM!

New imports correctly reflect the updated service dependencies for Stripe and wallet reload job integration.


98-103: LGTM!

The test correctly verifies that walletReloadJobService.scheduleForWalletSetting is called with the expected wallet setting data when creating a new setting with auto-reload enabled.


197-202: LGTM!

Good test coverage for the scheduling flow when updating an existing setting to enable auto-reload. The expectation correctly validates the setting ID and user ID are passed to the scheduler.


231-231: LGTM!

Correct verification that the job is cancelled when auto-reload is disabled, using the user ID and existing job ID.


276-319: LGTM on setup function structure.

The setup function correctly follows guidelines:

  • Positioned at the bottom of the describe block
  • Single parameter with inline type definition
  • Creates and returns all necessary test doubles
  • Uses jest-mock-extended mock() function
  • No shared state between tests

The addition of stripeService, walletReloadJobService, and userRepository properly reflects the updated service dependencies.

apps/api/src/billing/routes/stripe-payment-methods/stripe-payment-methods.router.ts (1)

127-148: Delete route now correctly returns 204 with empty body

The delete handler’s c.body(null, 204) matches the documented 204 response and is consistent with typical REST semantics. No issues here.

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

17-31: End-to-end auto‑reload flow and error handling look solid

The resource-collection, validation, reload decision (balance vs threshold), and follow-up scheduling logic are coherent and defensive:

  • Validation errors short-circuit with structured logging via ValidationError, avoiding noisy retries for misconfigured wallets.
  • Only paying users with initialized wallets, enabled auto‑reload, and a default payment method proceed.
  • Reload amount respects a minimum of $20 and uses a job-scoped idempotency key, which should prevent double-charging on job retries.
  • Balance-reload and next-check scheduling both log failures and rethrow so the job framework can retry.

I don’t see correctness or concurrency issues worth blocking on here.

Also applies to: 61-90, 92-156, 165-209, 211-229

apps/api/src/billing/services/stripe/stripe.service.ts (1)

108-123: New payment-method helpers and idempotent createPaymentIntent look good

  • getPaymentMethods now cleanly merges remote methods with local validation status and sorts by created descending.
  • hasPaymentMethod correctly handles “resource missing” as false while rethrowing other Stripe errors.
  • markRemotePaymentMethodAsDefault centralizes the customer update with a short timeout.
  • createPaymentIntent now takes an amount in major units, converts to cents once, and threads an optional idempotencyKey through to Stripe, which fits the way you’re using it from the auto‑reload handler.

Assuming existing tests around Stripe interactions pass, this looks correct and aligns with typical Stripe usage.

Please re-run the existing StripeService test suite to confirm the updated createPaymentIntent signature and options still match the Stripe SDK expectations.

Also applies to: 146-160, 190-231

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (5)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (3)

41-47: Consider tightening date assertions or controlling now more explicitly.

Using new Date() in both the SUT and the spec with toBeCloseTo(..., -3) gives a very generous tolerance window; it’s unlikely to be flaky but the negative precision is unusual and hard to read. You might consider:

  • Freezing time with fake timers and asserting exact equality, or
  • Using a small positive precision and comparing timestamps in seconds instead of milliseconds.

This is non-blocking; current assertions are acceptable but a bit surprising.

Please double-check Jest’s toBeCloseTo precision semantics in your version to ensure the current usage matches your intended tolerance.


290-322: Simplify userWithStripe derivation for readability.

The nested ternary logic for userWithStripe is correct but hard to scan. Consider a more direct formulation such as:

const baseUser = input?.user ?? UserSeeder.create();

const userWithStripe =
  input?.user && input.user.stripeCustomerId === null
    ? baseUser // explicitly test non-paying user case
    : baseUser.stripeCustomerId
      ? baseUser
      : { ...baseUser, stripeCustomerId: faker.string.uuid() };

or an explicit if/else chain. This keeps the important distinction (“explicit null means non-paying; otherwise ensure it’s a paying user”) but is easier to reason about.


323-381: Minor cleanup: unify core imports for consistency.

You import JobMeta from @src/core and JobPayload from "../../../core". Both should resolve to the same module; it would be clearer to import both from the same path (preferably the alias form) to avoid confusion.

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

42-49: Hard-coded timing and amount constants are reasonable, but consider centralizing if reused.

The daily check interval, 7‑day coverage horizon, 25% threshold, and $20 minimum reload amount are encoded as private fields, which is fine here. If these values are shared with HTTP validation (e.g., the $20 floor in stripe.schema.ts) or are likely to change, it might be worth centralizing them in a config/constants module to avoid drift.


137-156: Default payment method retrieval is simple and matches the “not found” behavior.

#getDefaultPaymentMethod scopes the call via createMongoAbility, returns Ok on a found method, and a clear DEFAULT_PAYMENT_METHOD_NOT_FOUND error otherwise. This dovetails with the spec that logs this specific validation error.

If StripeService.getDefaultPaymentMethod can throw for transport-layer issues rather than returning undefined, you may want to add a narrow try/catch around it and convert known “expected” failures into ValidationError results, leaving unexpected errors to bubble up. Not required for this PR if that service already normalizes its errors.

Please confirm how StripeService.getDefaultPaymentMethod behaves on network/Stripe API errors in your environment; if it throws, consider whether you want those to fail the job or be logged as validation errors.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de560e9 and 21068b5.

📒 Files selected for processing (2)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • 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.handler.spec.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test

Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

**/*.spec.{ts,tsx}: Use <Subject>.name in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references
Use either a method name or a condition starting with 'when' for nested suite descriptions in tests
Use present simple, 3rd person singular for test descriptions without prepending 'should'

Files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
🧠 Learnings (4)
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
📚 Learning: 2025-10-31T11:26:42.138Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.138Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
📚 Learning: 2025-09-25T14:31:44.914Z
Learnt from: baktun14
Repo: akash-network/console PR: 1969
File: apps/deploy-web/src/pages/payment.tsx:179-191
Timestamp: 2025-09-25T14:31:44.914Z
Learning: The payment confirmation endpoint in apps/api/src/billing/http-schemas/stripe.schema.ts uses zod schema validation with `amount: z.number().gte(20, "Amount must be greater or equal to $20")` to ensure all payment requests meet the minimum amount requirement, preventing zero-amount or invalid payments from reaching Stripe.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
📚 Learning: 2025-10-15T16:39:55.348Z
Learnt from: jzsfkzm
Repo: akash-network/console PR: 2039
File: apps/deploy-web/tests/ui/change-wallets.spec.ts:4-10
Timestamp: 2025-10-15T16:39:55.348Z
Learning: In the Akash Console E2E tests using the context-with-extension fixture, the first wallet is automatically created during fixture setup via `importWalletToLeap` in `apps/deploy-web/tests/ui/fixture/wallet-setup.ts`, so tests that call `frontPage.createWallet()` are creating a second wallet to test wallet switching functionality.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
🧬 Code graph analysis (2)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (9)
apps/api/src/core/types/require.type.ts (1)
  • Require (1-3)
apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts (1)
  • UserWalletOutput (18-22)
apps/api/src/billing/repositories/wallet-settings/wallet-settings.repository.ts (1)
  • WalletSettingOutput (19-22)
apps/api/src/billing/services/paying-user/paying-user.ts (2)
  • PayingUser (6-6)
  • isPayingUser (12-14)
apps/api/src/billing/http-schemas/balance.schema.ts (1)
  • GetBalancesResponseOutput (17-17)
apps/api/src/billing/services/stripe/stripe.service.ts (1)
  • PaymentMethod (33-33)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)
  • PaymentMethod (241-241)
apps/api/src/core/services/job-queue/job-queue.service.ts (2)
  • JobPayload (209-209)
  • JobMeta (218-218)
apps/api/src/billing/events/wallet-balance-reload-check.ts (1)
  • WalletBalanceReloadCheck (5-15)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (3)
apps/api/src/core/services/job-queue/job-queue.service.ts (2)
  • JobPayload (209-209)
  • JobMeta (218-218)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2)
  • job (72-90)
  • user (137-156)
apps/api/src/billing/events/wallet-balance-reload-check.ts (1)
  • WalletBalanceReloadCheck (5-15)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (14)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (5)

21-80: Strong, behavior-driven coverage for the reload path.

The happy-path test around "triggers reload when balance is below 25% of cost" nicely exercises cost horizon, thresholding, Stripe intent fields (including idempotency key), and scheduling of the next check; it aligns well with the handler’s logic and the business rules.


82-107: Minimum-amount reload case is covered correctly.

The "triggers reload with minimum amount when needed amount is below minimum" scenario cleanly validates the $20 floor behavior and the Stripe call payload; this complements the threshold test above and matches the handler’s Math.max(..., MIN_RELOAD_AMOUNT) logic.


109-157: Skip-path tests are accurate and readable.

The two tests for "does not trigger reload when balance equals/above 25% of cost" correctly assert that no payment intent is created and the WALLET_BALANCE_RELOAD_SKIPPED event is logged with the expected payload, matching the balance >= threshold branch.


186-208: Good coverage of scheduling failure behavior.

The "logs error and throws when scheduling next check fails" test matches the implementation’s behavior of logging ERROR_SCHEDULING_NEXT_CHECK and rethrowing, ensuring PgBoss can retry the job. This regression-guards the design choice discussed in earlier review comments.


210-287: Validation-error scenarios are thoroughly exercised.

The tests for missing wallet setting, disabled auto-reload, uninitialized wallet, missing Stripe customer ID, and missing default payment method align with ValidationError events and the logger payloads in the handler. This gives good confidence that all early-exit paths are observable and won’t silently fail.

apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (9)

1-32: Imports, types, and supporting aliases are well structured.

The use of Result<…, ValidationError>, InitializedWallet, ActionableWalletSetting, and AllResources gives a clear, typed pipeline for the handler; bringing in Require from the shared type utility avoids duplication noted in earlier reviews.


17-30: ValidationError and resource typing correctly match the current usage.

ValidationError containing just event and message, combined with Resources/AllResources, aligns with how errors are propagated and logged (#finishWithValidationError) and with what the specs assert on. This resolves the earlier mismatch around an unused source field.


61-70: Main handle orchestration is straightforward and matches tests.

handle cleanly separates:

  • Resource collection and validation,
  • Conditional reload with logging, and
  • Scheduling the next check,

and uses Result for control flow. The early‑return on validation errors via #finishWithValidationError is simple and aligns with the spec behaviors.


72-90: Resource collection is cohesive and correctly ordered.

#collectResources first validates wallet and user state, then retrieves the default payment method, then fiat balance; this order minimizes external calls when basic invariants fail. Returning a fully-populated AllResources object makes the downstream reload logic easy to reason about.


92-135: Wallet and user validation logic is solid and mirrors the spec expectations.

The #getValidWalletResources checks:

  • Existence of wallet setting,
  • autoReloadEnabled,
  • Non-empty wallet address, and
  • isPayingUser(user) (Stripe customer ID),

and returns specific event/message pairs for each failure. These match the test suite’s validation paths and prevent unnecessary downstream calls when the wallet/user is not reloadable.


158-163: Validation error logging is consistent and structured.

#finishWithValidationError simply forwards the event and message and attaches userId, matching the tests and avoiding partial state changes. This is straightforward and easy to monitor.


165-209: Reload decision logic, amount calculation, and idempotent Stripe call look correct.

Key aspects look solid:

  • Horizon and cost: computes a 7‑day reloadTargetDate and uses calculateAllDeploymentCostUntilDate + toFiatAmount to derive costUntilTargetDateInFiat.
  • Thresholding: reload only when costUntilTargetDateInFiat > 0 and balance < 0.25 * cost, which matches the specs.
  • Amount: Math.max(costUntilTargetDateInFiat - balance, MIN_RELOAD_AMOUNT_IN_USD) implements the “top up to coverage, but at least $20” rule.
  • Idempotency: idempotencyKey: \${WalletBalanceReloadCheck.name}.${resources.job.id}`` ensures PgBoss retries won’t double‑charge Stripe as long as the job ID is stable across retries.
  • Logging: logs a clear WALLET_BALANCE_RELOADED on success and WALLET_BALANCE_RELOAD_FAILED with the error on failure, and rethrows to signal a job failure.

Overall this matches the PR’s intent and the associated specs.

If you haven’t already, please verify PgBoss’s behavior around job IDs on retries—idempotency hinges on the job ID being stable so that the same idempotency key is reused for the same logical reload.


211-225: Scheduling behavior and error handling are appropriate for reliability.

#scheduleNextCheck:

  • Schedules via walletReloadJobService.scheduleForWalletSetting with an ISO startAfter based on #calculateNextCheckDate(), and
  • Logs ERROR_SCHEDULING_NEXT_CHECK and rethrows on failure.

This ensures that scheduling failures don’t silently disable auto‑reload and allows PgBoss to retry the job. The corresponding spec confirms this behavior, which is a good reliability stance.


227-229: Next-check date calculation is clear and consistent with tests.

#calculateNextCheckDate uses the same millisecondsInDay basis as the tests, keeping the daily cadence logic easy to understand.

@ygrishajev ygrishajev force-pushed the feature/auto-reload-handler branch from 21068b5 to bc659a9 Compare December 3, 2025 10:32
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

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

46-49: Consider centralizing the $20 minimum reload amount with Stripe payment validation.

#MIN_RELOAD_AMOUNT_IN_USD = 20 duplicates the business rule enforced in the Stripe schema at apps/api/src/billing/http-schemas/stripe.schema.ts line 69 (amount: z.number().gte(20, ...)). Extract this constant to a single source of truth in the billing module and import it both here and in the schema to prevent divergence if this threshold changes in the future.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21068b5 and bc659a9.

📒 Files selected for processing (2)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1 hunks)
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{ts,tsx,js}: Never use type any or cast to type any. Always define the proper TypeScript types.
Never use deprecated methods from libraries.
Don't add unnecessary comments to the code.

Files:

  • 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.handler.spec.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test

Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

**/*.spec.{ts,tsx}: Use <Subject>.name in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references
Use either a method name or a condition starting with 'when' for nested suite descriptions in tests
Use present simple, 3rd person singular for test descriptions without prepending 'should'

Files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
🧠 Learnings (4)
📚 Learning: 2025-11-12T16:36:02.543Z
Learnt from: baktun14
Repo: akash-network/console PR: 2203
File: apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx:161-168
Timestamp: 2025-11-12T16:36:02.543Z
Learning: In apps/deploy-web/src/components/onboarding/steps/PaymentMethodContainer/PaymentMethodContainer.tsx, the organization field captured during payment method setup is internal metadata. Errors from stripe.updateCustomerOrganization should be logged to Sentry but not surfaced to users, and the flow should continue even if the organization update fails, as it's non-critical and not something users can fix.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
📚 Learning: 2025-10-31T11:26:42.138Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.138Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
📚 Learning: 2025-09-25T14:31:44.914Z
Learnt from: baktun14
Repo: akash-network/console PR: 1969
File: apps/deploy-web/src/pages/payment.tsx:179-191
Timestamp: 2025-09-25T14:31:44.914Z
Learning: The payment confirmation endpoint in apps/api/src/billing/http-schemas/stripe.schema.ts uses zod schema validation with `amount: z.number().gte(20, "Amount must be greater or equal to $20")` to ensure all payment requests meet the minimum amount requirement, preventing zero-amount or invalid payments from reaching Stripe.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
📚 Learning: 2025-10-15T16:39:55.348Z
Learnt from: jzsfkzm
Repo: akash-network/console PR: 2039
File: apps/deploy-web/tests/ui/change-wallets.spec.ts:4-10
Timestamp: 2025-10-15T16:39:55.348Z
Learning: In the Akash Console E2E tests using the context-with-extension fixture, the first wallet is automatically created during fixture setup via `importWalletToLeap` in `apps/deploy-web/tests/ui/fixture/wallet-setup.ts`, so tests that call `frontPage.createWallet()` are creating a second wallet to test wallet switching functionality.

Applied to files:

  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
🧬 Code graph analysis (1)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (8)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (3)
  • job (72-90)
  • error (158-163)
  • user (137-156)
apps/api/src/billing/events/wallet-balance-reload-check.ts (1)
  • WalletBalanceReloadCheck (5-15)
apps/api/test/seeders/user-wallet.seeder.ts (1)
  • UserWalletSeeder (6-30)
apps/api/test/seeders/user.seeder.ts (1)
  • UserSeeder (5-45)
apps/api/test/seeders/wallet-setting.seeder.ts (1)
  • generateWalletSetting (5-18)
packages/http-sdk/src/stripe/stripe.service.ts (1)
  • StripeService (18-94)
apps/api/test/seeders/balance.seeder.ts (1)
  • generateBalance (21-34)
apps/api/test/seeders/payment-method.seeder.ts (1)
  • generatePaymentMethod (11-56)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts (1)

61-90: Core handler flow, validation, and error handling look solid.

  • Resource collection validates wallet setting existence, auto‑reload flag, wallet initialization, and paying user status, returning structured ValidationErrors.
  • Default payment method lookup is gated by CASL ability and correctly short‑circuits with a validation error if none is found.
  • Reload logic uses a 7‑day cost horizon, 25% coverage threshold, a minimum reload of $20, and an idempotency key per job; success and failure are both logged with useful context.
  • Next‑check scheduling now logs and rethrows errors, ensuring pgboss can retry when scheduling fails while keeping the payment step idempotent via Stripe.

No issues from a correctness or robustness standpoint on these paths.

Also applies to: 165-209, 211-229

apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts (1)

41-64: Replace undocumented negative numDigits in toBeCloseTo with an explicit, documented tolerance check.

The current assertions use toBeCloseTo with negative numDigits (-3), which is not documented in Jest's official API. While the formula Math.abs(expected - received) < 10**(-numDigits) / 2 does work mathematically (producing a ~500ms tolerance), relying on undocumented behavior makes tests fragile.

However, the suggested refactor in the original comment (using toBeLessThan(millisecondsInHour)) is not equivalent and would weaken the test by allowing a 1-hour tolerance instead of ~500ms.

Instead, use an explicit, documented approach:

- expect(reloadTargetDate.getTime()).toBeCloseTo(expectedReloadDate.getTime(), -3);
+ const reloadDeltaMs = Math.abs(reloadTargetDate.getTime() - expectedReloadDate.getTime());
+ expect(reloadDeltaMs).toBeLessThan(1000); // Within 1 second

Or use toBeCloseTo with a documented positive numDigits that achieves the desired ~500ms tolerance (e.g., toBeCloseTo(..., 2) with scaled values). This maintains the original test strictness while relying only on documented Jest behavior.

@ygrishajev ygrishajev merged commit de10e23 into main Dec 3, 2025
65 of 66 checks passed
@ygrishajev ygrishajev deleted the feature/auto-reload-handler branch December 3, 2025 10:53
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.

3 participants

Comments