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

Skip to content

Conversation

@BilalG1
Copy link
Collaborator

@BilalG1 BilalG1 commented Nov 7, 2025

No description provided.

@vercel
Copy link

vercel bot commented Nov 7, 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 12, 2025 7:45pm
stack-dashboard Ready Ready Preview Comment Nov 12, 2025 7:45pm
stack-demo Ready Ready Preview Comment Nov 12, 2025 7:45pm
stack-docs Ready Ready Preview Comment Nov 12, 2025 7:45pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 7, 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.

✨ 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

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.

@BilalG1 BilalG1 marked this pull request as ready for review November 12, 2025 19:35
@claude
Copy link
Contributor

claude bot commented Nov 12, 2025

Claude finished @BilalG1's task —— View job


PR Review: Subscription and OTP Refunds

I've completed my comprehensive review of this pull request implementing refund functionality for subscriptions and one-time purchases.

Review Checklist:

  • Read repository guidelines (CLAUDE.md)
  • Review database migration and schema changes
  • Examine new API endpoints and business logic
  • Analyze UI components and user experience
  • Check test coverage and quality
  • Review security considerations
  • Assess performance implications
  • Provide comprehensive feedback

🔍 Review Summary

This 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 Issues

1. Misleading Error Handling (High Priority)

File: apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx:85-86

if (!purchase.stripePaymentIntentId) {
  throw new KnownErrors.OneTimePurchaseNotFound(body.id);
}

Issue: This throws OneTimePurchaseNotFound when the purchase exists but lacks a payment intent ID. This will confuse users and make debugging difficult.

Fix: Create and use a more specific error like OneTimePurchaseNotRefundable or check the existing TestModePurchaseNonRefundable pattern.

2. Database Consistency Risk (High Priority)

File: apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx:88-98

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.

⚠️ Security Concerns

3. Missing Refund Status Validation

Issue: No check to prevent double refunds of the same purchase.
Recommendation: Add validation to check if refundedAt is already set before processing refunds.

4. Webhook Security Gap

File: apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx
Issue: No handling for charge.dispute.created or refund-related webhooks to maintain consistency.
Recommendation: Consider adding webhook handlers for refund events.

🟡 Medium Priority Issues

5. Incomplete Subscription Refund Logic

File: apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx:66-73

await prisma.subscription.update({
  where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: body.id } },
  data: {
    status: SubscriptionStatus.canceled,
    cancelAtPeriodEnd: true,
    currentPeriodEnd: new Date(), // ⚠️ This ends the subscription immediately
  },
});

Issue: Setting currentPeriodEnd: new Date() immediately ends the subscription, which may not be the desired behavior for refunds.

6. Missing Refund Amount Validation

Issue: No validation of refund amounts or partial refund handling.

🟢 Positive Aspects

Excellent Test Coverage

  • Comprehensive tests for different scenarios (test mode, missing purchases, successful refunds)
  • Good error case handling in tests
  • Integration tests cover the full flow

Clean Database Schema Changes

  • Simple, nullable refundedAt timestamp field
  • Proper migration with no breaking changes

Good UI/UX Implementation

  • Clear refund confirmation dialog with warning about irreversibility
  • Proper disabled states for non-refundable items
  • Good visual feedback with proper styling

Proper Error Types

  • Good use of existing KnownErrors pattern
  • Appropriate HTTP status codes (400 for business logic errors, 404 for not found)

📝 Recommendations

  1. Fix the critical error handling and database consistency issues before merging
  2. Add refund status validation to prevent double refunds
  3. Consider webhook handling for refund events from Stripe
  4. Review subscription refund timing logic - should subscriptions end immediately or at period end?
  5. Add integration tests for the webhook scenarios
  6. Consider adding audit logging for refund operations

🏁 Conclusion

This is a solid foundation for refund functionality with good test coverage and UI integration. The main concerns are around error handling accuracy and database consistency, which should be addressed before merging to production.

Recommendation: Request changes to address the critical issues, then approve.


@greptile-apps
Copy link
Contributor

greptile-apps bot commented Nov 12, 2025

Greptile Overview

Greptile Summary

This PR adds refund functionality for subscription and one-time purchase transactions. The implementation includes:

  • New refundedAt field on OneTimePurchase model to track refund status
  • Refund API endpoint that integrates with Stripe to process refunds
  • UI components in the dashboard to initiate refunds with confirmation dialogs
  • Three new error types for handling refund edge cases
  • Filtering logic to exclude refunded purchases from owned products

Critical issues found:

  • Race condition in refund flow where database is updated before Stripe API call completes, potentially leaving inconsistent state if Stripe fails
  • Missing validation to prevent double-refunding the same purchase
  • Empty refund.created webhook handler that doesn't process refund events from Stripe

Confidence Score: 2/5

  • This PR has critical race conditions that could cause data inconsistency
  • The race condition between DB updates and Stripe API calls is a critical bug that could leave purchases marked as refunded when no actual refund occurred (or vice versa). Missing double-refund prevention could allow refunding the same purchase multiple times.
  • Pay close attention to apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx which contains the race condition bugs

Important Files Changed

File Analysis

Filename Score Overview
apps/backend/src/app/api/latest/internal/payments/transactions/refund/route.tsx 3/5 Adds refund API endpoint for subscriptions and one-time purchases; race condition between Stripe refund and DB update, missing double-refund prevention
apps/backend/src/lib/payments.tsx 5/5 Filters out refunded one-time purchases when getting owned products
apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx 4/5 Added stub for refund.created webhook handling but no implementation

Sequence Diagram

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

14 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +88 to +98
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,
},
});
Copy link
Contributor

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.

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

Comment on lines +65 to +72
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(),
},
Copy link
Contributor

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.

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

Comment on lines +75 to +82
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();
Copy link
Contributor

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.

Comment on lines +107 to +109
if (event.type === "refund.created") {
const refund = event.data.object;
}
Copy link
Contributor

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.

Comment on lines +241 to +244
const refundRes = await niceBackendFetch("/api/latest/internal/payments/transactions/refund", {
accessType: "admin",
method: "POST",
body: { type: "one-time-purchase", id: purchaseTransaction.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 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

@BilalG1 BilalG1 requested a review from N2D4 November 12, 2025 22:59
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