Skip to content

fix(billing): preserve trial status for coupon claim invoice top-ups#2780

Open
ygrishajev wants to merge 1 commit intomainfrom
fix/billing
Open

fix(billing): preserve trial status for coupon claim invoice top-ups#2780
ygrishajev wants to merge 1 commit intomainfrom
fix/billing

Conversation

@ygrishajev
Copy link
Contributor

@ygrishajev ygrishajev commented Feb 18, 2026

Why

Coupon claim invoices should not end the user trial period. Previously, all invoice-based wallet top-ups would unconditionally end the trial, which incorrectly affected users redeeming coupons.

What

  • Added endTrial option to RefillService.topUpWallet (defaults to true for backward compatibility)
  • StripeWebhookService.tryToTopUpWalletFromInvoice now sets endTrial=false for coupon_claim transactions
  • Plumbed endTrial through topUpWalletFromTransaction and updateTransactionAndTopUp
  • Updated existing test assertions and added a new test for endTrial: false in RefillService

Summary by CodeRabbit

  • Bug Fixes

    • Wallet top-ups now consistently respect an explicit "end trial" flag; coupon-based redemptions do not end trials, while other top-ups end trials by default.
  • Tests

    • Added tests ensuring the trial-ending flag is correctly propagated through the top-up flow.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

Adds an optional endTrial option to RefillService.topUpWallet, threads it through Stripe webhook handlers and related methods, and updates tests and call sites to pass the flag (defaulting to true when omitted) so wallet refreshes receive the endTrial intent.

Changes

Cohort / File(s) Summary
RefillService Implementation
apps/api/src/billing/services/refill/refill.service.ts
topUpWallet now accepts options?: { endTrial?: boolean } and calls refreshUserWalletLimits with options.endTrial ?? true.
RefillService Tests
apps/api/src/billing/services/refill/refill.service.spec.ts
Added test asserting refreshUserWalletLimits is invoked with { endTrial: false } when topUpWallet is called with that option.
StripeWebhookService Implementation
apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
Added endTrial?: boolean to topUpWalletFromTransaction / updateTransactionAndTopUp params; compute endTrial from transaction type (false for coupon_claim) and pass through to refillService.topUpWallet.
StripeWebhookService Integration Tests
apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.integration.ts
Updated test call sites to pass the new third options argument to topUpWallet with appropriate endTrial values (undefined or false).

Sequence Diagram

sequenceDiagram
    participant Webhook as Stripe Webhook Handler
    participant SWS as StripeWebhookService
    participant RS as RefillService
    participant BS as BalancesService

    Webhook->>SWS: receive event (invoice / payout / transaction)
    SWS->>SWS: determine endTrial = (transaction.type !== "coupon_claim")
    SWS->>SWS: updateTransactionAndTopUp(..., endTrial)
    SWS->>RS: topUpWallet(amount, userId, { endTrial })
    RS->>BS: refreshUserWalletLimits(wallet, { endTrial })
    BS-->>RS: limits refreshed / trial state updated
    RS-->>SWS: top-up complete
    SWS-->>Webhook: acknowledge processing
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped a flag from webhook to refill,
endTrial tucked in every bill,
Coupons sneak by with a gentle wink,
While other trials stop in a blink—
Hooray, the balances hum and trill! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: preserving trial status for coupon claim invoice top-ups.
Description check ✅ Passed The description includes both required sections: Why explaining the issue and What detailing the implementation changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix/billing

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

@ygrishajev ygrishajev enabled auto-merge February 18, 2026 19:30
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.

🧹 Nitpick comments (1)
apps/api/src/billing/services/refill/refill.service.spec.ts (1)

40-51: Test description should not start with "should".

Per coding guidelines, test descriptions should use present simple, 3rd person singular without prepending "should".

✏️ Suggested rename
-    it("should not end trial when endTrial option is false", async () => {
+    it("does not end trial when endTrial option is false", async () => {

As per coding guidelines: "Use present simple, 3rd person singular for test descriptions without prepending 'should'".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/billing/services/refill/refill.service.spec.ts` around lines 40
- 51, Update the test description string in the it(...) block in
refill.service.spec.ts to use present simple 3rd person singular without
"should": replace "should not end trial when endTrial option is false" with
"does not end trial when endTrial option is false" (the test around
service.topUpWallet and the expectation on
balancesService.refreshUserWalletLimits). Ensure the updated description is used
for the it(...) call that references service.topUpWallet,
userWalletRepository.findOneBy, managedUserWalletService.authorizeSpending, and
balancesService.refreshUserWalletLimits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/api/src/billing/services/refill/refill.service.spec.ts`:
- Around line 40-51: Update the test description string in the it(...) block in
refill.service.spec.ts to use present simple 3rd person singular without
"should": replace "should not end trial when endTrial option is false" with
"does not end trial when endTrial option is false" (the test around
service.topUpWallet and the expectation on
balancesService.refreshUserWalletLimits). Ensure the updated description is used
for the it(...) call that references service.topUpWallet,
userWalletRepository.findOneBy, managedUserWalletService.authorizeSpending, and
balancesService.refreshUserWalletLimits.

@codecov
Copy link

codecov bot commented Feb 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 51.29%. Comparing base (9f4ae3d) to head (b20a4dc).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2780      +/-   ##
==========================================
- Coverage   52.08%   51.29%   -0.80%     
==========================================
  Files        1043     1008      -35     
  Lines       27454    26623     -831     
  Branches     6359     6266      -93     
==========================================
- Hits        14300    13655     -645     
+ Misses      12681    12504     -177     
+ Partials      473      464       -9     
Flag Coverage Δ *Carryforward flag
api 76.91% <100.00%> (+<0.01%) ⬆️
deploy-web 37.39% <ø> (ø) Carriedforward from 9f4ae3d
log-collector ?
notifications 85.56% <ø> (ø) Carriedforward from 9f4ae3d
provider-console 81.48% <ø> (ø) Carriedforward from 9f4ae3d
provider-proxy 82.41% <ø> (ø) Carriedforward from 9f4ae3d
tx-signer ?

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

Files with missing lines Coverage Δ
.../api/src/billing/services/refill/refill.service.ts 97.95% <100.00%> (ø)
.../services/stripe-webhook/stripe-webhook.service.ts 77.19% <100.00%> (+0.13%) ⬆️

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

Coupon claim invoices should not end the user's trial period. This
passes endTrial=false for coupon_claim transactions through the
webhook-refill pipeline and updates tests accordingly.
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/billing/services/refill/refill.service.spec.ts`:
- Around line 40-51: Update the test description string to follow the naming
guideline (present simple, 3rd person singular, no "should"); locate the test
case that calls service.topUpWallet(amountUsd, userId, { endTrial: false }) and
asserts balancesService.refreshUserWalletLimits was called with (existingWallet,
{ endTrial: false }), then change the it(...) description from "should not end
trial when endTrial option is false" to a present-simple form such as "does not
end trial when endTrial option is false" (or similar phrasing in 3rd person
singular) so the test name conforms to the project convention while leaving the
test body (including references to UserWalletSeeder.create,
userWalletRepository.findOneBy, managedUserWalletService.authorizeSpending,
balancesService.retrieveDeploymentLimit, and
balancesService.refreshUserWalletLimits) unchanged.

Comment on lines +40 to +51
it("should not end trial when endTrial option is false", async () => {
const { service, userWalletRepository, managedUserWalletService, balancesService } = setup();
const existingWallet = UserWalletSeeder.create({ userId });
userWalletRepository.findOneBy.mockResolvedValue(existingWallet);
managedUserWalletService.authorizeSpending.mockResolvedValue();
balancesService.retrieveDeploymentLimit.mockResolvedValue(5000);
balancesService.refreshUserWalletLimits.mockResolvedValue();

await service.topUpWallet(amountUsd, userId, { endTrial: false });

expect(balancesService.refreshUserWalletLimits).toHaveBeenCalledWith(existingWallet, { endTrial: false });
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Rename test description to meet naming guideline.

The new test name starts with “should”. Please use present simple, 3rd person singular without “should”.

✏️ Proposed fix
-it("should not end trial when endTrial option is false", async () => {
+it("does not end trial when endTrial option is false", async () => {

As per coding guidelines: Use present simple, 3rd person singular for test descriptions without prepending 'should'.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("should not end trial when endTrial option is false", async () => {
const { service, userWalletRepository, managedUserWalletService, balancesService } = setup();
const existingWallet = UserWalletSeeder.create({ userId });
userWalletRepository.findOneBy.mockResolvedValue(existingWallet);
managedUserWalletService.authorizeSpending.mockResolvedValue();
balancesService.retrieveDeploymentLimit.mockResolvedValue(5000);
balancesService.refreshUserWalletLimits.mockResolvedValue();
await service.topUpWallet(amountUsd, userId, { endTrial: false });
expect(balancesService.refreshUserWalletLimits).toHaveBeenCalledWith(existingWallet, { endTrial: false });
});
it("does not end trial when endTrial option is false", async () => {
const { service, userWalletRepository, managedUserWalletService, balancesService } = setup();
const existingWallet = UserWalletSeeder.create({ userId });
userWalletRepository.findOneBy.mockResolvedValue(existingWallet);
managedUserWalletService.authorizeSpending.mockResolvedValue();
balancesService.retrieveDeploymentLimit.mockResolvedValue(5000);
balancesService.refreshUserWalletLimits.mockResolvedValue();
await service.topUpWallet(amountUsd, userId, { endTrial: false });
expect(balancesService.refreshUserWalletLimits).toHaveBeenCalledWith(existingWallet, { endTrial: false });
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/billing/services/refill/refill.service.spec.ts` around lines 40
- 51, Update the test description string to follow the naming guideline (present
simple, 3rd person singular, no "should"); locate the test case that calls
service.topUpWallet(amountUsd, userId, { endTrial: false }) and asserts
balancesService.refreshUserWalletLimits was called with (existingWallet, {
endTrial: false }), then change the it(...) description from "should not end
trial when endTrial option is false" to a present-simple form such as "does not
end trial when endTrial option is false" (or similar phrasing in 3rd person
singular) so the test name conforms to the project convention while leaving the
test body (including references to UserWalletSeeder.create,
userWalletRepository.findOneBy, managedUserWalletService.authorizeSpending,
balancesService.retrieveDeploymentLimit, and
balancesService.refreshUserWalletLimits) unchanged.

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

Comments