Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@BilalG1
Copy link
Collaborator

@BilalG1 BilalG1 commented Nov 12, 2025

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 12, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit 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)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch transaction-refunds-in-table

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.

❤️ Share

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

@vercel
Copy link

vercel bot commented Nov 12, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
stack-backend Ready Ready Preview Comment Nov 13, 2025 3:39am
stack-dashboard Ready Ready Preview Comment Nov 13, 2025 3:39am
stack-demo Ready Ready Preview Comment Nov 13, 2025 3:39am
stack-docs Ready Ready Preview Comment Nov 13, 2025 3:39am

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Nov 12, 2025

Greptile Overview

Greptile Summary

This PR adds refund tracking to the transaction table by storing refundedAt timestamps and populating the adjusted_by field with refund transaction references.

  • Added refundedAt field to Subscription model via migration
  • Enhanced refund endpoint with double-refund prevention using new error types
  • Transaction builder now generates adjusted_by entries pointing to virtual refund transaction IDs
  • Dashboard displays "Refunded" badges and prevents refunding already-refunded transactions
  • Added test coverage for double refund attempts and adjusted_by field validation

Critical issues found:

  • Subscription refunds don't check for TEST_MODE (unlike one-time purchases)
  • Database update happens before Stripe API call in one-time purchase refunds, risking data inconsistency if Stripe fails

Confidence Score: 2/5

  • This PR has critical logic bugs that could cause production issues with refunds
  • Two critical bugs identified: (1) missing test mode validation for subscriptions allows refunding test subscriptions which will fail at Stripe, (2) database-before-Stripe ordering creates inconsistent state if Stripe refund fails. These are functional bugs that will cause runtime errors.
  • Pay close attention to apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx - both critical bugs are in this file

Important Files Changed

File Analysis

Filename Score Overview
apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx 2/5 Added refund validation checks and refundedAt timestamps. Critical issues: missing test mode check for subscriptions, and database updates before Stripe calls creating potential data inconsistency.
apps/backend/src/app/api/latest/internal/payments/transactions/transaction-builder.ts 4/5 Added buildRefundAdjustments helper that populates adjusted_by array when refunds exist. Logic correctly finds product_grant entry and generates refund transaction IDs.
apps/dashboard/src/components/data-table/transaction-table.tsx 5/5 Added "Refunded" badge display in Details column and prevents re-refunding via UI. Clean implementation with proper null checks.

Sequence Diagram

sequenceDiagram
    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
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Additional Comments (1)

  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

Edit Code Review Agent Settings | Greptile

Comment on lines +34 to +36
const subscription = await prisma.subscription.findUnique({
where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } },
select: { refundedAt: true },
Copy link
Contributor

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)

Suggested change
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.

Copy link

@vercel vercel bot left a 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:

  1. Call POST /api/latest/internal/payments/transactions/refund with {"type": "one-time-purchase", "id": "valid-purchase-id"}
  2. Simulate Stripe API failure (network timeout, API error) after database update on line 102-105
  3. Database shows refundedAt set, 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

@BilalG1 BilalG1 requested a review from N2D4 November 13, 2025 00:02
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;
Copy link

Choose a reason for hiding this comment

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

Suggested change
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:

  1. Create a server-granted subscription via POST /api/latest/payments/products/user/{userId} with product_id and quantity
  2. View the transaction in the dashboard transactions table
  3. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants