fix(billing): delegate coupon wallet top-up to Stripe webhook for reliable payment processing#2730
fix(billing): delegate coupon wallet top-up to Stripe webhook for reliable payment processing#2730ygrishajev wants to merge 1 commit intomainfrom
Conversation
📝 WalkthroughWalkthroughThis PR adds invoice-driven processing for Stripe payments: a repository method to find transactions by invoice ID, webhook flows to top up wallets from invoice events with new helper methods, locking and update logic, expanded integration tests for invoice and locking scenarios, and refactors coupon application to create invoice items and pending transactions with rollback on error. Changes
Sequence Diagram(s)sequenceDiagram
participant Webhook as Stripe Webhook
participant Service as StripeWebhookService
participant Repo as StripeTransactionRepository
participant Stripe as Stripe API
participant Refill as RefillService
participant DB as Database
Webhook->>Service: invoice.payment_succeeded(event)
Service->>Repo: findByInvoiceId(invoiceId)
Repo->>DB: query stripeTransaction where stripeInvoiceId
DB-->>Repo: transaction row (or none)
Repo-->>Service: StripeTransactionOutput (or undefined)
alt transaction found
Service->>Stripe: retrieve charge (if chargeId present)
Stripe-->>Service: charge details (card, receiptUrl)
Service->>Service: topUpWalletFromTransaction(transaction, charge)
Service->>Repo: findOneByAndLock(transactionId)
Repo->>DB: lock & fetch transaction
DB-->>Repo: locked transaction
Repo-->>Service: locked transaction
Service->>Repo: update transaction fields (status, charge ids, card info, receiptUrl, paymentIntentId)
Repo->>DB: persist updates
Service->>Refill: refillService.topUp(userId, amount)
Refill->>DB: apply wallet credit
DB-->>Refill: success
Refill-->>Service: confirmation
else no matching transaction
Service-->>Service: log INVOICE_NO_MATCHING_TRANSACTION and return
end
sequenceDiagram
participant Client as Client
participant StripeSvc as StripeService
participant Stripe as Stripe API
participant Repo as StripeTransactionRepository
participant Webhook as Stripe Webhook
Client->>StripeSvc: applyCoupon(userId, coupon)
StripeSvc->>Stripe: create draft invoice (with discount)
Stripe-->>StripeSvc: invoice
StripeSvc->>Repo: create StripeTransaction(type: coupon_claim, status: pending, stripeInvoiceId)
Repo-->>StripeSvc: StripeTransactionOutput
StripeSvc->>Stripe: invoiceItems.create(amount, customer, invoice)
alt item created
Stripe-->>StripeSvc: invoice item
StripeSvc->>Stripe: finalize invoice
Stripe-->>Webhook: invoice.payment_succeeded (later)
StripeSvc-->>Client: return success (pending)
else item creation fails
StripeSvc->>Stripe: voidInvoice(invoice)
Stripe-->>StripeSvc: invoice voided
StripeSvc-->>Client: throw error
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 Comment |
39c06da to
0585089
Compare
There was a problem hiding this comment.
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/stripe/stripe.service.ts (1)
403-432:⚠️ Potential issue | 🟠 MajorPotential race condition: transaction record created after invoice finalization.
The invoice is finalized at line 411, which may trigger the
invoice.payment_succeededwebhook. However, the transaction record with thestripeInvoiceIdis created at lines 422-432, after finalization. If the webhook fires before the transaction insert completes,findByInvoiceIdwill returnundefinedand the wallet top-up will be silently skipped.Consider creating the transaction record (in
pendingstatus) before finalizing the invoice, or ensure the webhook handler has retry logic for this case.🔧 Proposed fix: Create transaction before invoice finalization
await this.invoiceItems.create({ amount: amountToAdd, customer: currentUser.stripeCustomerId, invoice: invoice.id, currency: "usd", description: "Akash Network Console" }); + await this.stripeTransactionRepository.create({ + userId: currentUser.id, + type: "coupon_claim", + status: "pending", + amount: amountToAdd, + currency: coupon.currency ?? "usd", + stripeCouponId: coupon.id, + stripePromotionCodeId: updateField === "promotion_code" ? updateId : undefined, + stripeInvoiceId: invoice.id, + description: `Coupon: ${coupon.name || coupon.id}` + }); + invoice = await this.invoices.finalizeInvoice(invoice.id); this.loggerService.info({ event: "INVOICE_FINALIZED_AND_PAID", userId: currentUser.id, invoiceId: invoice.id, status: invoice.status, amountDue: invoice.amount_due, amountPaid: invoice.amount_paid }); - await this.stripeTransactionRepository.create({ - userId: currentUser.id, - type: "coupon_claim", - status: "pending", - amount: amountToAdd, - currency: coupon.currency ?? "usd", - stripeCouponId: coupon.id, - stripePromotionCodeId: updateField === "promotion_code" ? updateId : undefined, - stripeInvoiceId: invoice.id, - description: `Coupon: ${coupon.name || coupon.id}` - }); return { coupon: couponOrPromotion, amountAdded: amountToAdd / 100 };
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts`:
- Around line 216-225: After acquiring the lock with
this.stripeTransactionRepository.findOneByAndLock in stripe-webhook.service.ts,
add a defensive null/undefined check for the returned transaction variable
before proceeding (e.g., before the transaction?.status check and any subsequent
call to updateById); if transaction is missing, log an event (e.g.,
"TRANSACTION_NOT_FOUND_AFTER_LOCK") with params.transactionId and return early
to avoid calling updateById on a non-existent record. Ensure you reference the
same repository method (findOneByAndLock) and the transaction variable so the
guard covers all code paths that assume a present transaction.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #2730 +/- ##
==========================================
- Coverage 52.12% 51.38% -0.74%
==========================================
Files 1055 1020 -35
Lines 27972 27170 -802
Branches 6334 6250 -84
==========================================
- Hits 14580 13961 -619
+ Misses 12918 12735 -183
Partials 474 474
*This pull request uses carry forward flags. Click here to find out more.
🚀 New features to boost your workflow:
|
…iable payment processing Move wallet crediting from the synchronous applyCoupon flow to the invoice.payment_succeeded webhook handler. This leverages Stripe's built-in retry mechanism to ensure users receive credits even if the initial top-up call fails transiently.
0585089 to
ea5c25a
Compare
apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.integration.ts
Show resolved
Hide resolved
baktun14
left a comment
There was a problem hiding this comment.
Let's make sure to test this thoroughly on beta before releasing to prod 🙏
Move wallet crediting from the synchronous applyCoupon flow to the invoice.payment_succeeded webhook handler. This leverages Stripe's built-in retry mechanism to ensure users receive credits even if the initial top-up call fails transiently.
Summary by CodeRabbit
New Features
Bug Fixes / Reliability
Tests