-
Notifications
You must be signed in to change notification settings - Fork 477
subscription and otp refunds #1006
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: payment-subscription-renewal-transactions
Are you sure you want to change the base?
subscription and otp refunds #1006
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…nsactions' into transaction-refunds
…tack-auth into transaction-refunds
|
Claude finished @BilalG1's task —— View job PR Review: Subscription and OTP RefundsI've completed my comprehensive review of this pull request implementing refund functionality for subscriptions and one-time purchases. Review Checklist:
🔍 Review SummaryThis PR adds comprehensive refund functionality with good test coverage and proper UI integration. However, there are several issues that should be addressed before merging. ❌ Critical Issues1. Misleading Error Handling (High Priority)File: if (!purchase.stripePaymentIntentId) {
throw new KnownErrors.OneTimePurchaseNotFound(body.id);
}Issue: This throws Fix: Create and use a more specific error like 2. Database Consistency Risk (High Priority)File: await prisma.oneTimePurchase.update({
where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } },
data: { refundedAt: new Date() },
});
await stripe.refunds.create({
payment_intent: purchase.stripePaymentIntentId,
// ...
});Issue: Database is updated before Stripe refund is processed. If Stripe fails, the database will incorrectly show the purchase as refunded. Fix: Either use a database transaction to rollback on Stripe failure, or process the Stripe refund first, then update the database.
|
Greptile OverviewGreptile SummaryThis PR adds refund functionality for subscription and one-time purchase transactions. The implementation includes:
Critical issues found:
Confidence Score: 2/5
Important Files ChangedFile Analysis
Sequence DiagramsequenceDiagram
participant UI as Dashboard UI
participant API as Refund API
participant DB as Database
participant Stripe as Stripe API
UI->>API: POST /refund (type, id)
API->>DB: Find subscription/purchase
alt Subscription Refund
API->>DB: Find subscription invoices
API->>Stripe: Retrieve invoice with payments
Stripe-->>API: Invoice + payment data
API->>Stripe: Create refund (payment_intent)
Stripe-->>API: Refund created
API->>DB: Update subscription (canceled)
DB-->>API: Success
else One-Time Purchase Refund
alt Test Mode Purchase
API-->>UI: Error: TEST_MODE_PURCHASE_NON_REFUNDABLE
else Already Refunded (Missing Check)
Note over API,Stripe: ⚠️ No validation for double refund
else Valid Purchase
API->>DB: Update purchase (refundedAt)
DB-->>API: Success
API->>Stripe: Create refund (payment_intent)
Note over API,Stripe: ⚠️ If Stripe fails, DB already updated
Stripe-->>API: Refund created
end
end
API-->>UI: { success: true }
Note over Stripe,DB: refund.created webhook stub exists but not implemented
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
14 files reviewed, 4 comments
| await prisma.oneTimePurchase.update({ | ||
| where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } }, | ||
| data: { refundedAt: new Date() }, | ||
| }); | ||
| await stripe.refunds.create({ | ||
| payment_intent: purchase.stripePaymentIntentId, | ||
| metadata: { | ||
| tenancyId: auth.tenancy.id, | ||
| purchaseId: purchase.id, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Race condition: DB update happens before Stripe refund call. If Stripe refund fails, the purchase is marked as refunded in DB but no actual refund occurred.
| await prisma.oneTimePurchase.update({ | |
| where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } }, | |
| data: { refundedAt: new Date() }, | |
| }); | |
| await stripe.refunds.create({ | |
| payment_intent: purchase.stripePaymentIntentId, | |
| metadata: { | |
| tenancyId: auth.tenancy.id, | |
| purchaseId: purchase.id, | |
| }, | |
| }); | |
| const stripe = await getStripeForAccount({ tenancy: auth.tenancy }); | |
| if (!purchase.stripePaymentIntentId) { | |
| throw new KnownErrors.OneTimePurchaseNotFound(body.id); | |
| } | |
| await stripe.refunds.create({ | |
| payment_intent: purchase.stripePaymentIntentId, | |
| metadata: { | |
| tenancyId: auth.tenancy.id, | |
| purchaseId: purchase.id, | |
| }, | |
| }); | |
| await prisma.oneTimePurchase.update({ | |
| where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } }, | |
| data: { refundedAt: new Date() }, | |
| }); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx
Line: 88:98
Comment:
**logic:** Race condition: DB update happens before Stripe refund call. If Stripe refund fails, the purchase is marked as refunded in DB but no actual refund occurred.
```suggestion
const stripe = await getStripeForAccount({ tenancy: auth.tenancy });
if (!purchase.stripePaymentIntentId) {
throw new KnownErrors.OneTimePurchaseNotFound(body.id);
}
await stripe.refunds.create({
payment_intent: purchase.stripePaymentIntentId,
metadata: {
tenancyId: auth.tenancy.id,
purchaseId: purchase.id,
},
});
await prisma.oneTimePurchase.update({
where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } },
data: { refundedAt: new Date() },
});
```
How can I resolve this? If you propose a fix, please make it concise.| await stripe.refunds.create({ payment_intent: paymentIntentId }); | ||
| await prisma.subscription.update({ | ||
| where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } }, | ||
| data: { | ||
| status: SubscriptionStatus.canceled, | ||
| cancelAtPeriodEnd: true, | ||
| currentPeriodEnd: new Date(), | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Same race condition: DB update happens before Stripe refund. If Stripe call fails after line 65, subscription is incorrectly marked as canceled.
| await stripe.refunds.create({ payment_intent: paymentIntentId }); | |
| await prisma.subscription.update({ | |
| where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } }, | |
| data: { | |
| status: SubscriptionStatus.canceled, | |
| cancelAtPeriodEnd: true, | |
| currentPeriodEnd: new Date(), | |
| }, | |
| await stripe.refunds.create({ payment_intent: paymentIntentId }); | |
| await prisma.subscription.update({ | |
| where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } }, | |
| data: { | |
| status: SubscriptionStatus.canceled, | |
| cancelAtPeriodEnd: true, | |
| currentPeriodEnd: new Date(), | |
| }, | |
| }); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx
Line: 65:72
Comment:
**logic:** Same race condition: DB update happens before Stripe refund. If Stripe call fails after line 65, subscription is incorrectly marked as canceled.
```suggestion
await stripe.refunds.create({ payment_intent: paymentIntentId });
await prisma.subscription.update({
where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } },
data: {
status: SubscriptionStatus.canceled,
cancelAtPeriodEnd: true,
currentPeriodEnd: new Date(),
},
});
```
How can I resolve this? If you propose a fix, please make it concise.| const purchase = await prisma.oneTimePurchase.findUnique({ | ||
| where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } }, | ||
| }); | ||
| if (!purchase) { | ||
| throw new KnownErrors.OneTimePurchaseNotFound(body.id); | ||
| } | ||
| if (purchase.creationSource === "TEST_MODE") { | ||
| throw new KnownErrors.TestModePurchaseNonRefundable(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Missing check: no validation prevents refunding the same purchase multiple times (no check for existing refundedAt value)
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx
Line: 75:82
Comment:
**logic:** Missing check: no validation prevents refunding the same purchase multiple times (no check for existing `refundedAt` value)
How can I resolve this? If you propose a fix, please make it concise.| if (event.type === "refund.created") { | ||
| const refund = event.data.object; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Empty refund.created webhook handler has no implementation
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx
Line: 107:109
Comment:
**style:** Empty `refund.created` webhook handler has no implementation
How can I resolve this? If you propose a fix, please make it concise.| const refundRes = await niceBackendFetch("/api/latest/internal/payments/transactions/refund", { | ||
| accessType: "admin", | ||
| method: "POST", | ||
| body: { type: "one-time-purchase", id: purchaseTransaction.id }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| const refundRes = await niceBackendFetch("/api/latest/internal/payments/transactions/refund", { | |
| accessType: "admin", | |
| method: "POST", | |
| body: { type: "one-time-purchase", id: purchaseTransaction.id }, | |
| const productGrant = purchaseTransaction.entries.find((e: any) => e.type === 'product_grant'); | |
| const oneTimePurchaseId = productGrant?.one_time_purchase_id; | |
| const refundRes = await niceBackendFetch("/api/latest/internal/payments/transactions/refund", { | |
| accessType: "admin", | |
| method: "POST", | |
| body: { type: "one-time-purchase", id: oneTimePurchaseId }, |
The test passes the wrong ID to the refund endpoint. It uses the transaction ID (purchaseTransaction.id) but the endpoint expects the OneTimePurchase ID (extracted from the transaction entry).
View Details
Analysis
Incorrect ID passed to OneTimePurchase refund endpoint in test
What fails: Test in transactions-refund.test.ts line 244 passes transaction ID (purchaseTransaction.id) to refund endpoint, but endpoint expects OneTimePurchase ID from transaction entry
How to reproduce: Run test "refunds non-test mode one-time purchases created via Stripe webhooks" - it passes transaction UUID to /api/latest/internal/payments/transactions/refund but endpoint calls prisma.oneTimePurchase.findUnique({ where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } } })
Result: Test would fail with 404 ONE_TIME_PURCHASE_NOT_FOUND error because transaction ID doesn't match any OneTimePurchase record
Expected: Should extract one_time_purchase_id from the product_grant entry in transaction.entries array, per Prisma composite key docs
No description provided.