-
Notifications
You must be signed in to change notification settings - Fork 477
show transaction refunds in table #1013
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: transaction-refunds
Are you sure you want to change the base?
Conversation
|
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 Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. ✨ 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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile OverviewGreptile SummaryThis PR adds refund tracking to the transaction table by storing
Critical issues found:
Confidence Score: 2/5
Important Files ChangedFile Analysis
Sequence DiagramsequenceDiagram
participant Admin as Admin Dashboard
participant API as Refund API
participant DB as Database
participant Stripe as Stripe API
participant TxBuilder as Transaction Builder
Admin->>API: POST /refund {type, id}
API->>DB: Find purchase/subscription
alt Purchase not found
DB-->>API: null
API-->>Admin: 404 Not Found
else Already refunded
DB-->>API: {refundedAt: timestamp}
API-->>Admin: 400 Already Refunded
else Test mode purchase
DB-->>API: {creationSource: TEST_MODE}
API-->>Admin: 400 Non-Refundable
else Valid refund request
DB-->>API: Purchase data
alt One-time purchase path
API->>DB: Update refundedAt
API->>Stripe: Create refund
Stripe-->>API: Refund success
else Subscription path
API->>DB: Find subscription invoices
API->>Stripe: Retrieve invoice & payments
Stripe-->>API: Payment intent ID
API->>Stripe: Create refund
Stripe-->>API: Refund success
API->>DB: Update subscription (cancel + refundedAt)
end
API-->>Admin: 200 Success
Note over Admin,TxBuilder: Later when fetching transactions
Admin->>API: GET /transactions
API->>DB: Fetch purchases/subscriptions
DB-->>API: Data with refundedAt
API->>TxBuilder: Build transaction objects
TxBuilder->>TxBuilder: buildRefundAdjustments()
Note over TxBuilder: Populates adjusted_by array<br/>with refund transaction ID
TxBuilder-->>API: Transactions with adjusted_by
API-->>Admin: Display with "Refunded" badge
end
|
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.
Additional Comments (1)
-
apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx, line 102-105 (link)logic: database is updated before calling Stripe API - if Stripe refund fails, database will incorrectly show the purchase as refunded. swap order to call Stripe first:
7 files reviewed, 2 comments
| const subscription = await prisma.subscription.findUnique({ | ||
| where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } }, | ||
| select: { refundedAt: true }, |
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: the initial query only selects refundedAt, but subscriptions also have a creationSource field that needs checking (subscriptions can be TEST_MODE like one-time purchases)
| const subscription = await prisma.subscription.findUnique({ | |
| where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } }, | |
| select: { refundedAt: true }, | |
| const subscription = await prisma.subscription.findUnique({ | |
| where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } }, | |
| select: { refundedAt: true, creationSource: true }, | |
| }); |
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: 34:36
Comment:
**logic:** the initial query only selects `refundedAt`, but subscriptions also have a `creationSource` field that needs checking (subscriptions can be TEST_MODE like one-time purchases)
```suggestion
const subscription = await prisma.subscription.findUnique({
where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } },
select: { refundedAt: true, creationSource: true },
});
```
How can I resolve this? If you propose a fix, please make it concise.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.
Additional Suggestion:
One-time purchase refunds are saved to the database before creating the refund in Stripe, causing an inconsistent state if the Stripe refund fails.
View Details
📝 Patch Details
diff --git a/apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx b/apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx
index 6d030d10..83a56b4a 100644
--- a/apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx
+++ b/apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx
@@ -99,10 +99,6 @@ export const POST = createSmartRouteHandler({
if (!purchase.stripePaymentIntentId) {
throw new KnownErrors.OneTimePurchaseNotFound(body.id);
}
- 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: {
@@ -110,6 +106,10 @@ export const POST = createSmartRouteHandler({
purchaseId: purchase.id,
},
});
+ await prisma.oneTimePurchase.update({
+ where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } },
+ data: { refundedAt: new Date() },
+ });
}
return {
Analysis
Inconsistent refund state when Stripe API fails for one-time purchases
What fails: OneTimePurchaseRefund updates database before creating Stripe refund, causing inconsistent state when Stripe API fails
How to reproduce:
- Call POST
/api/latest/internal/payments/transactions/refundwith{"type": "one-time-purchase", "id": "valid-purchase-id"} - Simulate Stripe API failure (network timeout, API error) after database update on line 102-105
- Database shows
refundedAtset, but no refund exists in Stripe
Result: Purchase marked as refunded in database but actual refund never created in Stripe. Subsequent refund attempts fail with OneTimePurchaseAlreadyRefunded error.
Expected: Operations should be atomic - either both succeed or database remains unchanged, matching the subscription refund pattern (lines 75-84)
Fix: Reordered operations to create Stripe refund first (line ~106), then update database (line ~102), ensuring consistent state on failures per Stripe error handling best practices
| const canRefund = !!target && !transaction.test_mode; | ||
| const alreadyRefunded = transaction.adjusted_by.length > 0; | ||
| const productEntry = transaction.entries.find(isProductGrantEntry); | ||
| const canRefund = !!target && !transaction.test_mode && !alreadyRefunded && productEntry?.price_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 canRefund = !!target && !transaction.test_mode && !alreadyRefunded && productEntry?.price_id; | |
| const canRefund = !!target && !transaction.test_mode && !alreadyRefunded; |
The refund button is now disabled for transactions with null price_id, but the backend allows refunding such transactions. This prevents refunding valid purchases created via server-side product grant without specifying a price_id.
View Details
Analysis
Frontend prevents refunding server-granted subscriptions with null price_id
What fails: RefundActionCell.canRefund() in transaction-table.tsx checks productEntry?.price_id, preventing refund attempts for server-granted subscriptions that have price_id: null
How to reproduce:
- Create a server-granted subscription via
POST /api/latest/payments/products/user/{userId}withproduct_idandquantity - View the transaction in the dashboard transactions table
- Observe refund button is disabled despite subscription being a valid purchase
Result: Refund button disabled in UI, users cannot attempt refunds Expected: Refund button should be enabled, allowing users to attempt refunds (backend will handle validation and return appropriate errors for non-refundable items)
Technical details: Server-granted subscriptions use priceId: undefined in grantProductToCustomer() which stores null in the database, while frontend incorrectly assumes all refundable transactions must have a price_id. Backend refund logic in /api/latest/internal/payments/transactions/refund/route.tsx does not check price_id, creating a frontend/backend inconsistency.
https://www.loom.com/share/40ea4388af9a49f5aac28e8fcb7ca3e8