Skip to content

feat(billing): create a wallet for every account and make start-trial idempotent - #3513

Merged
baktun14 merged 4 commits into
mainfrom
feat/billing-wallet-per-account-activation
Jul 24, 2026
Merged

feat(billing): create a wallet for every account and make start-trial idempotent#3513
baktun14 merged 4 commits into
mainfrom
feat/billing-wallet-per-account-activation

Conversation

@baktun14

@baktun14 baktun14 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Why

Managed wallets were only created when a trial started, and provisioning bundled row creation, address derivation, and slow on-chain grant authorization into one non-idempotent step. An interrupted or concurrent start-trial left half-provisioned rows with NULL addresses that surfaced to clients (source of Sentry 7542732522) or, on failure, deleted the row entirely.

Closes CON-744
Related to CON-743 (frontend guard, #3512)

What

Every registered account gets a wallet row with a derived address; a new activated_at column becomes the activation signal (replaces "row exists"), so all client-facing semantics stay unchanged.

  • Migration: add user_wallets.activated_at timestamptz + backfill activated_at = created_at for rows with an address. Additive and non-blocking; NULL-address orphans stay unactivated and self-heal on next start-trial.
  • UserService.registerUser idempotently ensures a wallet (row + derived address, no chain transactions) on every login — lazy backfill for existing users. Failures are logged and never block login.
  • Start-trial is now idempotent: getOrCreate + address heal, then a conditional-UPDATE claim on activated_at. The concurrent loser gets a 409 (absorbed by the client's mutation retry). On grant failure only the activation stamp rolls back — the row and address are kept (previously the row was deleted). TrialStarted fires exactly once, on a successful claim.
  • GET /v1/wallets returns only activated wallets, so a wallet without grants can never reach the client.
  • First purchase without a trial activates the wallet (RefillService stamps activated_at).
  • Consumers that treated row-existence or createdAt as "trial started" were updated to the activation signal:
    • fee-refill cron only refills activated wallets (otherwise every registered account would receive on-chain fee grants),
    • trial-abuse fingerprint check only matches activated trial wallets,
    • the trial top-up minimum only applies to activated wallets (first purchase keeps today's behavior),
    • trial windows anchor to activated_at instead of createdAt (registration date no longer approximates trial start).

Verified locally against a real Postgres + built server: migration backfill via the production migrator; start-trial on a fresh account creates the row with a real derived address; a failed grant returns 500 while keeping the row and unsetting the claim; retries reuse the same row; /v1/wallets stays empty until activation and returns the wallet after; re-running start-trial on an activated wallet returns 200 with the same state. The claim race (exactly one winner among 5 concurrent claims) is covered by an integration test on real Postgres.

Summary by CodeRabbit

  • New Features
    • Added wallet activation tracking (activatedAt) and updated wallet/refill/trial flows to focus on activated wallets.
    • Introduced atomic activation claiming to prevent duplicate/concurrent activation and enable re-claim if activation is later cleared.
    • Automatically ensures/initializes wallets during user registration and trial start.
  • Bug Fixes
    • Trial windows and allowance expiration are now anchored to activatedAt when available.
    • Trial-user selection and refill authorization now exclude non-activated wallets.
  • Documentation
    • Documented an additional 409 response for trial provisioning already in progress.
  • Tests
    • Expanded coverage for concurrent activation claiming, activated vs non-activated behavior, and activation-aware trial/refill logic.

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.42%. Comparing base (757d28e) to head (55de7f5).
⚠️ Report is 6 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3513      +/-   ##
==========================================
- Coverage   73.85%   73.42%   -0.43%     
==========================================
  Files        1139     1078      -61     
  Lines       29704    27794    -1910     
  Branches     7445     7042     -403     
==========================================
- Hits        21939    20409    -1530     
+ Misses       6854     6520     -334     
+ Partials      911      865      -46     
Flag Coverage Δ *Carryforward flag
api 87.45% <100.00%> (+0.94%) ⬆️
deploy-web 63.68% <ø> (+0.10%) ⬆️
log-collector ?
notifications 93.84% <ø> (ø)
provider-console 81.38% <ø> (ø) Carriedforward from d669721
provider-inventory ?
provider-proxy 88.17% <ø> (ø) Carriedforward from d669721
tx-signer 86.72% <ø> (ø)

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

Files with missing lines Coverage Δ
...ng/model-schemas/user-wallet/user-wallet.schema.ts 75.00% <ø> (ø)
...repositories/user-wallet/user-wallet.repository.ts 81.35% <100.00%> (+0.99%) ⬆️
.../services/managed-signer/managed-signer.service.ts 100.00% <100.00%> (ø)
...managed-user-wallet/managed-user-wallet.service.ts 100.00% <100.00%> (+7.01%) ⬆️
.../api/src/billing/services/refill/refill.service.ts 97.77% <100.00%> (-0.23%) ⬇️
...vices/trial-validation/trial-validation.service.ts 65.00% <100.00%> (ø)
...s/wallet-initializer/wallet-initializer.service.ts 100.00% <100.00%> (+10.81%) ⬆️
...ng/services/wallet-reader/wallet-reader.service.ts 94.11% <100.00%> (+17.64%) ⬆️
...eployment-setting/deployment-setting.repository.ts 96.29% <ø> (ø)
...-deployments/top-up-managed-deployments.service.ts 96.72% <100.00%> (ø)
... and 2 more

... and 77 files with indirect coverage changes

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

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aadad125-b14b-47b2-93d5-c9f46335aae0

📥 Commits

Reviewing files that changed from the base of the PR and between d669721 and 55de7f5.

📒 Files selected for processing (1)
  • apps/api/src/billing/services/refill/refill.service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/billing/services/refill/refill.service.ts

📝 Walkthrough

Walkthrough

Changes

Wallet activation lifecycle

Layer / File(s) Summary
Activation schema and fixtures
apps/api/drizzle/..., apps/api/src/billing/model-schemas/..., apps/api/test/seeders/*
Adds nullable activation storage, migration metadata, schema mapping, and activation-aware seed data.
Atomic activation claims
apps/api/src/billing/repositories/user-wallet/*
Adds conditional activation claiming and updates draining-wallet and trial-window queries to use activation state.
Wallet provisioning and trial activation
apps/api/src/billing/services/wallet-initializer/*, apps/api/src/billing/routes/start-trial/*
Ensures wallet rows and addresses, claims activation during trial provisioning, clears failed claims, and documents the 409 response.
Top-up, trial, and wallet visibility
apps/api/src/billing/services/{refill,managed-user-wallet,trial-validation,wallet-reader}/*, apps/api/test/functional/*
Uses activation time for trial calculations and excludes unactivated wallets from applicable validation, refill, and listing paths.
User and deployment integration
apps/api/src/{user,deployment}/..., apps/api/src/billing/services/managed-signer/*
Ensures wallets during registration, requires activated trial wallets in fingerprint queries, and passes activation timestamps through deployment fee-grant flows.

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

Possibly related PRs

Suggested reviewers: stalniy

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/billing-wallet-per-account-activation

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts (1)

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

Trial-window/expiration logic duplicated across two services and already diverging. Both ManagedUserWalletService.refillWalletFees and RefillService.refillWalletFees independently compute trialStartedAt = activatedAt ?? createdAt, an "in trial window" boolean, and an expiration date — the same rule maintained in two places with different comparison styles.

  • apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts#L108-L114: extract this trial-window/expiration calculation into a small shared helper (e.g. on ManagedUserWalletService or a standalone util) that both services call.
  • apps/api/src/billing/services/refill/refill.service.ts#L56-L61: replace this duplicate calc (currently using raw Date > and redundant trialStartedAt && guards) with a call to the shared helper instead of maintaining a second, subtly different implementation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts`
around lines 108 - 114, Extract the trial-window and expiration calculation from
ManagedUserWalletService.refillWalletFees into a shared helper, preserving the
activatedAt-or-createdAt start time and existing date-fns comparison behavior.
Update
apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts:108-114
to use the helper, and replace the duplicate calculation in
apps/api/src/billing/services/refill/refill.service.ts:56-61 with the same
helper, removing its raw Date comparison and redundant guards.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/drizzle/0033_long_solo.sql`:
- Line 2: Update the backfill statement so the timestamp without time zone in
created_at is explicitly interpreted as UTC before assigning it to timestamptz
activated_at; preserve the existing non-null row filter and ensure the migration
is independent of the session TimeZone.

In `@apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts`:
- Around line 85-93: Update claimActivation to enforce wallet authorization at
the mutation site rather than relying on callers resolving the wallet first.
Thread the scoped repository or authorization context into claimActivation, or
perform an explicit ownership/access check before the update, while preserving
the existing atomic activatedAt condition and return behavior.

---

Nitpick comments:
In
`@apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts`:
- Around line 108-114: Extract the trial-window and expiration calculation from
ManagedUserWalletService.refillWalletFees into a shared helper, preserving the
activatedAt-or-createdAt start time and existing date-fns comparison behavior.
Update
apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts:108-114
to use the helper, and replace the duplicate calculation in
apps/api/src/billing/services/refill/refill.service.ts:56-61 with the same
helper, removing its raw Date comparison and redundant guards.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f3e783fb-82ac-420b-b87a-9fe1f5817ba7

📥 Commits

Reviewing files that changed from the base of the PR and between d3f0179 and 8b37eb4.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (29)
  • apps/api/drizzle/0033_long_solo.sql
  • apps/api/drizzle/meta/0033_snapshot.json
  • apps/api/drizzle/meta/_journal.json
  • apps/api/src/billing/model-schemas/user-wallet/user-wallet.schema.ts
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.integration.ts
  • apps/api/src/billing/repositories/user-wallet/user-wallet.repository.ts
  • apps/api/src/billing/routes/start-trial/start-trial.router.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
  • apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.spec.ts
  • apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts
  • apps/api/src/billing/services/refill/refill.service.spec.ts
  • apps/api/src/billing/services/refill/refill.service.ts
  • apps/api/src/billing/services/trial-validation/trial-validation.service.spec.ts
  • apps/api/src/billing/services/trial-validation/trial-validation.service.ts
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.spec.ts
  • apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts
  • apps/api/src/billing/services/wallet-reader/wallet-reader.service.spec.ts
  • apps/api/src/billing/services/wallet-reader/wallet-reader.service.ts
  • apps/api/src/deployment/repositories/deployment-setting/deployment-setting.repository.ts
  • apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.spec.ts
  • apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.ts
  • apps/api/src/user/repositories/user/user.repository.integration.ts
  • apps/api/src/user/repositories/user/user.repository.ts
  • apps/api/src/user/services/user/user.service.integration.ts
  • apps/api/src/user/services/user/user.service.spec.ts
  • apps/api/src/user/services/user/user.service.ts
  • apps/api/test/functional/wallets-refill.spec.ts
  • apps/api/test/seeders/auto-top-up-deployment.seeder.ts
  • apps/api/test/seeders/user-wallet.seeder.ts

Comment thread apps/api/drizzle/0033_long_solo.sql Outdated
- Backfill activated_at (timestamptz) by interpreting created_at (timestamp
  without time zone) as UTC, so migration is independent of session TimeZone.
- claimActivation enforces ability at the mutation site via whereAccessibleBy,
  matching updateById/deleteById (passes through when no ability is set).
- RefillService.refillWalletFees delegates to ManagedUserWalletService
  .refillWalletFees, removing the duplicated trial-window/expiration calc.
@baktun14

Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit review in d669721:

  • Migration timezone (0033_long_solo.sql): backfill uses created_at AT TIME ZONE 'UTC' so it's independent of session TimeZone. Verified on a throwaway Postgres under a non-UTC session.
  • claimActivation authorization: wraps the update where in whereAccessibleBy(...), matching updateById/deleteById; cron (no ability) passes through unchanged.
  • Duplicated trial-window calc (nitpick): RefillService.refillWalletFees now delegates to ManagedUserWalletService.refillWalletFees, removing the second, diverging copy.

Unit tests (52) pass, lint clean, typecheck clean for the changed files. Functional/integration coverage runs in CI.

Comment thread apps/api/src/billing/services/refill/refill.service.ts
@baktun14
baktun14 added this pull request to the merge queue Jul 24, 2026
Merged via the queue into main with commit 627ad0c Jul 24, 2026
57 checks passed
@baktun14
baktun14 deleted the feat/billing-wallet-per-account-activation branch July 24, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants