Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 11, 2025

Stripe Payment Integration - Phase 1 ✅ COMPLETE

Implementation Checklist

Database Schema Updates ✅

  • Add PaymentAttempt model with PENDING/SUCCESS/FAILED status tracking
  • Add Refund model with PENDING/COMPLETED/FAILED status
  • Add Stripe-related fields to Store model (stripeAccountId, stripeSecretKey, stripePublishableKey)
  • Update Order model with paidAt timestamp field and payment relations
  • Add PaymentAttemptStatus and RefundStatus enums
  • Create SQL migration file

Core Payment Service ✅

  • Create payment.service.ts with createCheckoutSession method
  • Implement processRefund method with idempotency support
  • Add getPaymentIntent helper method
  • Add Stripe SDK initialization with error handling
  • Support multi-currency (USD, BDT, EUR, GBP, etc.)
  • Stripe Connect support for multi-tenant payments
  • Integrate payment service with existing order-processing.service.ts

Webhook Infrastructure ✅

  • Create /api/webhooks/stripe/route.ts with signature verification
  • Implement handleCheckoutCompleted event handler
  • Implement handlePaymentSucceeded event handler
  • Implement handlePaymentFailed event handler
  • Implement handleChargeRefunded event handler
  • Add audit logging for payment events

Payment API Routes ✅

  • Create /api/payments/create-session/route.ts
  • Add authorization checks (user must own store via organization membership)
  • Validate order existence and ownership
  • Return sessionId and sessionUrl for redirect
  • Handle order status validation (no payment for paid/canceled/refunded orders)
  • Integrate with existing /api/orders/[id]/refund route

Frontend Components ✅

  • Create CheckoutButton component with loading states
  • Add toast notifications for errors
  • Implement Stripe Checkout redirect

Environment Configuration ✅

  • Add STRIPE_SECRET_KEY to .env.example
  • Add STRIPE_PUBLISHABLE_KEY to .env.example
  • Add STRIPE_WEBHOOK_SECRET to .env.example
  • Document test vs production keys
  • Update DATABASE_URL to PostgreSQL (SQLite removed)

Documentation ✅

  • Create comprehensive implementation guide (STRIPE_PAYMENT_INTEGRATION.md)
  • Document testing procedures with test cards
  • Add webhook setup instructions (local + production)
  • Document multi-currency support
  • Add Stripe Connect usage guide
  • Document security considerations
  • Create implementation summary (IMPLEMENTATION_SUMMARY.md)
  • Document code review fixes (CODE_REVIEW_FIXES.md)

Code Quality & Validation ✅

  • Run type-check to validate TypeScript (0 errors)
  • Run lint to check code quality (0 errors in new files)
  • Build project to verify compilation (successful)
  • Store memory facts for future development
  • Follow existing code patterns and conventions

Code Review Fixes ✅ (18 Comments Addressed)

  • Security (3): Webhook secret validation, multi-tenant filtering, error sanitization
  • Reliability (5): Payment intent null handling, race condition prevention, update verification, atomic refunds, improved idempotency
  • Data Integrity (4): Complete inventory restoration, variant handling, amount tracking, audit logs
  • Code Quality (5): Stripe Connect pattern, currency conversion TODO, API options, redirect fallback, error handling
  • Documentation (1): Comprehensive CODE_REVIEW_FIXES.md with all changes

Deployment Fixes ✅

  • Vercel Build Fix: Changed Stripe env validation from module-level to runtime-only
  • Prisma Version Fix: Updated build script to use local Prisma CLI (v6.19.0) instead of npx (v7.1.0)

Vercel Deployment Fix (Commit [commit_hash])

Issue

Vercel builds were failing due to:

  1. Module-level Stripe validation: STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET were validated at module initialization, causing build failures when environment variables weren't set
  2. Prisma version mismatch: Build script used npx prisma generate which pulled Prisma v7, incompatible with project's v6.19.0

Solution

1. Runtime-Only Validation

Changed Stripe environment variable validation from module-level (throws at import) to runtime (throws when routes/services are used):

Before (breaks build):

if (!process.env.STRIPE_SECRET_KEY) {
  throw new Error("STRIPE_SECRET_KEY is not defined");
}
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {...});

After (build-safe):

let stripe: Stripe | null = null;
if (stripeSecretKey && stripeSecretKey !== 'sk_test_...') {
  stripe = new Stripe(stripeSecretKey, {...});
}

// In route handlers/service methods:
if (!stripe) {
  throw new Error("Stripe not initialized");
}

2. Local Prisma CLI

Changed build script from npx prisma generate to node node_modules/.bin/prisma generate to use project's local Prisma v6.19.0 instead of npx's v7.1.0.

Build Status

Build now succeeds with placeholder environment variables from .env.example
Runtime validation ensures Stripe features fail gracefully with 503 errors when not configured
Backward compatible with existing deployments that have proper Stripe credentials

Code Review Changes Summary (Commit 3811e36)

Critical Fixes

  1. Payment Intent Null Issue: Store session.id initially, update with payment_intent in webhook
  2. Race Condition: Check order status before updating to prevent duplicate processing
  3. Refund Atomicity: Create PENDING record before Stripe call, update after success
  4. Inventory Restoration: Complete implementation with variant handling, status updates, and logs
  5. Idempotency: Improved key generation with random suffix to prevent collisions

Security Enhancements

  1. Webhook Validation: Added STRIPE_WEBHOOK_SECRET check at module initialization
  2. Multi-Tenant Security: Added storeId filter to order lookups
  3. Error Sanitization: Generic error messages to prevent exposing internal details

Data Integrity

  1. Audit Logs: Added storeId to all audit entries + refund completion logs
  2. Amount Tracking: Changed refundedAmount to use increment for cumulative tracking
  3. Update Verification: Check that updateMany affected exactly 1 record

API Improvements

  1. Stripe Connect: Use platform instance with stripeAccount option
  2. GetPaymentIntent: Correct options parameter usage
  3. Variant Support: Include variant data in queries for proper inventory restoration

Frontend

  1. Redirect Fallback: 5-second timeout to handle failed redirects (popup blockers)

Future Work Documented

  1. Currency Conversion: Added TODO for zero-decimal (JPY, KRW) and three-decimal (KWD, BHD) currencies

Documentation Files

  • STRIPE_PAYMENT_INTEGRATION.md (365 lines) - Complete implementation guide
  • IMPLEMENTATION_SUMMARY.md (410 lines) - Architecture and decisions
  • CODE_REVIEW_FIXES.md (290 lines) - Detailed fix documentation

What's Ready for Testing

All code is implemented, code review comments addressed, and Vercel deployment issues fixed. Ready for manual testing:

  1. Database Migration - Run: npx prisma migrate deploy
  2. Stripe Configuration - Add API keys to environment variables in Vercel
  3. Webhook Setup - Configure webhook endpoint in Stripe Dashboard
  4. Payment Flow - Test with card 4242 4242 4242 4242
  5. Declined Payment - Test with card 4000 0000 0000 0002
  6. Refund Processing - Test full and partial refunds via API
  7. Multi-Currency - Test checkout in BDT, EUR, GBP
  8. Inventory Restoration - Verify inventory updates with variants
  9. Webhook Replay - Test duplicate webhook handling
  10. Concurrent Refunds - Verify idempotency key uniqueness

All code review comments addressed + Vercel deployment fixed! 🚀

Original prompt

This section details on the original issue you should resolve

<issue_title>[Phase 1] Stripe Payment Integration</issue_title>
<issue_description>## Priority: P0
Phase: 1
Epic: #26 Payment Integration
Estimate: 2 days
Type: Story

Context

Stripe Payment Integration provides the payment processing infrastructure for StormCom, enabling secure credit/debit card payments, handling asynchronous webhooks for payment confirmation, and supporting refund processing. This implementation must handle idempotency, webhook signature verification, and seamless integration with the Order Processing API.

According to the research documentation, payment processing requires atomic transaction handling to prevent order creation without payment confirmation, proper webhook security to prevent spoofing attacks, and comprehensive error handling for declined payments.

Acceptance Criteria

  • Stripe Checkout Session: Create Stripe Checkout sessions with order metadata, line items, success/cancel URLs, support for 10+ currencies (USD, BDT, EUR, GBP)
  • Webhook Handler: Process checkout.session.completed, payment_intent.succeeded, charge.refunded events with signature verification, idempotency key validation
  • Payment Attempts Tracking: Log all payment attempts (PENDING → SUCCESS/FAILED) in PaymentAttempt table with Stripe payment intent ID, amount, currency, error codes
  • Refund Processing: Issue full/partial refunds via Stripe API with idempotency keys, update order refundable balance, restore inventory on refund
  • Error Handling: Handle declined cards (insufficient funds, expired, invalid), network failures with exponential backoff retry (3 attempts max), webhook delivery failures
  • Security: Verify webhook signatures using Stripe secret, use HTTPS for all webhook endpoints, sanitize webhook payload before processing
  • Multi-Currency Support: Handle BDT (Bangladeshi Taka) via Stripe, convert display prices to customer currency, store amounts in smallest currency unit (paisa for BDT)
  • Performance: Process webhook in <2 seconds, create checkout session in <500ms, refund processing <3 seconds
  • Multi-Tenancy: Each store has unique Stripe Connect account ID, API keys stored encrypted in Store model, all payments filtered by storeId
  • Testing: Stripe test mode integration, test card numbers for success/decline scenarios, webhook replay testing via Stripe CLI

Technical Implementation

1. Stripe Checkout Session Creation

// src/lib/services/payment.service.ts
import Stripe from "stripe";
import { prisma } from "@/lib/prisma";

if (!process.env.STRIPE_SECRET_KEY) {
  throw new Error("STRIPE_SECRET_KEY is not defined");
}

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: "2024-11-20.acacia",
  typescript: true,
});

export class PaymentService {
  async createCheckoutSession(params: {
    orderId: string;
    storeId: string;
    successUrl: string;
    cancelUrl: string;
  }) {
    const { orderId, storeId, successUrl, cancelUrl } = params;

    // Fetch order with items
    const order = await prisma.order.findUnique({
      where: { id: orderId, storeId },
      include: {
        items: {
          include: {
            product: { select: { name: true, images: true } },
            variant: { select: { name: true } },
          },
        },
        store: { select: { name: true, currency: true, stripeAccountId: true } },
        customer: { select: { email: true } },
      },
    });

    if (!order) {
      throw new Error("Order not found");
    }

    // Create line items for Stripe
    const lineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = order.items.map((item) => ({
      price_data: {
        currency: order.store.currency || "usd",
        product_data: {
          name: item.product.name,
          description: item.variant?.name,
          images: item.product.images.slice(0, 1), // First image only
        },
        unit_amount: Math.round(item.price * 100), // Convert to cents
      },
      quantity: item.quantity,
    }));

    // Create checkout session
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ["card"],
      line_items: lineItems,
      mode: "payment",
      success_url: successUrl,
      cancel_url: cancelUrl,
      client_reference_id: orderId,
      customer_email: order.customer.email,
      metadata: {
        orderId,
        storeId,
        orderNumber: order.orderNumber,
      },
      ...(order.store.stripeAccountId && {
        stripe_account: order.store.stripeAccountId, // Stripe Connect
      }),
    });

    // Create pending payment attempt
    await prisma.paymentAttempt.create({
      data: {
        orderId,
        storeId,
        provider: "STRIPE",
        amount: order.totalAmount,
        currency: order.store.currency || "USD",
        st...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes CodeStorm-Hub/stormcomui#27

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

@vercel
Copy link

vercel bot commented Dec 11, 2025

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

Project Deployment Preview Comments Updated (UTC)
stormcomui Ready Ready Preview Comment Dec 11, 2025 0:34am

…mponents

Co-authored-by: rafiqul4 <124497017+rafiqul4@users.noreply.github.com>
Co-authored-by: rafiqul4 <124497017+rafiqul4@users.noreply.github.com>
Copilot AI and others added 2 commits December 11, 2025 11:43
Co-authored-by: rafiqul4 <124497017+rafiqul4@users.noreply.github.com>
Co-authored-by: rafiqul4 <124497017+rafiqul4@users.noreply.github.com>
Copilot AI changed the title [WIP] Add Stripe payment integration in Phase 1 Add Stripe payment integration with webhook handling and refund processing Dec 11, 2025
Copilot AI requested a review from rafiqul4 December 11, 2025 11:48
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements comprehensive Stripe payment integration for the StormCom multi-tenant e-commerce platform, enabling secure checkout sessions, webhook-driven payment confirmation, and refund processing with multi-currency support and Stripe Connect for marketplace functionality.

Key Changes:

  • Database Schema: New PaymentAttempt and Refund models for tracking payment lifecycle with Stripe integration points; added Stripe Connect fields to Store model and paidAt timestamp to Order
  • Payment Service: Complete payment processing service with checkout session creation, refund management with idempotency, and multi-currency support (USD, BDT, EUR, GBP, etc.)
  • Webhook Handler: Secure webhook endpoint with signature verification processing payment lifecycle events (checkout completion, payment success/failure, refunds)
  • API Routes: Authenticated checkout session creation endpoint with organization membership validation
  • Frontend Component: React checkout button with loading states and error handling for Stripe redirect flow

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
src/lib/services/payment.service.ts Core payment service implementing checkout session creation, refund processing with Stripe API integration and Stripe Connect support
src/lib/services/order-processing.service.ts Simplified refund logic to delegate to payment service, replacing 112 lines of inline Stripe code with service call
src/components/checkout-button.tsx Client-side checkout button component handling session creation API calls and Stripe redirect flow
src/app/api/webhooks/stripe/route.ts Webhook handler with signature verification processing checkout completion, payment success/failure, and refund events
src/app/api/payments/create-session/route.ts Protected API endpoint for creating Stripe checkout sessions with organization membership authorization
prisma/schema.prisma Added PaymentAttempt and Refund models with status enums; extended Store with Stripe Connect fields and Order with payment timestamp
prisma/migrations/20251211112500_add_stripe_payment_integration/migration.sql PostgreSQL migration creating PaymentAttempt and Refund tables with indexes on orderId, storeId, externalId, and status
docs/STRIPE_PAYMENT_INTEGRATION.md Complete implementation guide covering setup, configuration, testing, and usage examples
docs/IMPLEMENTATION_SUMMARY.md Detailed summary of implementation decisions, architecture, testing strategy, and success criteria
.env.example Added Stripe configuration variables (secret key, publishable key, webhook secret) with documentation

Comment on lines +136 to +144
await prisma.order.update({
where: { id: orderId },
data: {
status: "PAID",
paymentStatus: "PAID",
paidAt: new Date(),
stripePaymentIntentId: paymentIntentId,
},
});
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

Potential race condition: The order status is updated to PAID in the webhook handler, but there's no check if it's already been paid. If the webhook is replayed or delivered multiple times (which Stripe may do), this could cause duplicate processing or inconsistent state. Add a check to ensure the order is in an appropriate state (e.g., PENDING) before transitioning to PAID.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 3811e36. Added order status check before updating to PAID. If already PAID, the webhook handler logs and returns early to prevent duplicate processing.

Comment on lines 114 to 120
if (order.store.stripeSecretKey && order.store.stripeAccountId) {
// Use store-specific Stripe instance for Stripe Connect
const storeStripe = new Stripe(order.store.stripeSecretKey, {
apiVersion: "2025-11-17.clover",
typescript: true,
});
session = await storeStripe.checkout.sessions.create(sessionOptions);
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

When using store-specific Stripe credentials, the store's stripeAccountId is not included in the session creation options. For Stripe Connect, you typically need to either use the store's Stripe instance OR add stripeAccount to requestOptions when using the platform instance. Currently, a new Stripe instance is created with store credentials but the session is created without the account context, which may not work as intended for connected accounts.

Suggested change
if (order.store.stripeSecretKey && order.store.stripeAccountId) {
// Use store-specific Stripe instance for Stripe Connect
const storeStripe = new Stripe(order.store.stripeSecretKey, {
apiVersion: "2025-11-17.clover",
typescript: true,
});
session = await storeStripe.checkout.sessions.create(sessionOptions);
if (order.store.stripeAccountId) {
// Use platform Stripe instance with stripeAccount for Stripe Connect
session = await stripe.checkout.sessions.create(sessionOptions, {
stripeAccount: order.store.stripeAccountId,
});

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 3811e36. Changed to use platform Stripe instance with stripeAccount option in requestOptions instead of creating store-specific instances, following Stripe Connect best practices.

Comment on lines 257 to 288
async function handleChargeRefunded(charge: Stripe.Charge) {
const paymentIntentId = charge.payment_intent as string;

console.log(`[Stripe Webhook] Processing charge.refunded for payment_intent ${paymentIntentId}`);

try {
// Find refund record by payment intent
const refund = await prisma.refund.findFirst({
where: {
paymentAttempt: {
externalId: paymentIntentId,
},
status: "PENDING",
},
orderBy: {
createdAt: "desc",
},
});

if (refund) {
await prisma.refund.update({
where: { id: refund.id },
data: {
status: "COMPLETED",
processedAt: new Date(),
},
});

console.log(`[Stripe Webhook] Refund completed: ${refund.id}`);
} else {
console.log(`[Stripe Webhook] No pending refund found for payment_intent ${paymentIntentId}`);
}
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

The handleChargeRefunded function doesn't extract the orderId or storeId to add to the audit log. The refund lookup finds a pending refund by payment intent, but doesn't use the order information for logging. This makes it difficult to track which order was refunded in audit logs. Consider fetching the order details and creating an audit log entry similar to handleCheckoutCompleted.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 3811e36. Added order details to refund lookup and created audit log entry for REFUND_COMPLETED action with storeId, orderId, and refund details.

Comment on lines +174 to +175
},
},
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

The order.items query doesn't include the variant relationship needed for inventory restoration. Line 172 includes product but not variant, yet the inventory restoration logic needs to check for variants to restore the correct inventory (see the old implementation that was removed which checked item.variantId). Add variant to the include statement on line 172.

Suggested change
},
},
variant: true,
},

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 3811e36. Added variant with inventoryQty to the items include statement. This data is now used for proper variant inventory restoration logic.

return { success: true, refund };
} catch (error) {
console.error('Refund processing error:', error);
throw error;
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

The error handling catches all errors and re-throws them (line 411), but the calling code in the API route also catches errors and returns generic error responses. This means the specific error from processRefund (like "Cannot refund X. Available balance: Y") will be exposed to the client. While helpful for debugging, consider whether detailed balance information should be exposed in production, as it could reveal business logic to malicious actors.

Suggested change
throw error;
throw new Error('Refund processing failed');

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 3811e36. Changed error handling to throw generic "Refund processing failed" message instead of exposing detailed balance information to clients.

Comment on lines 147 to 158
await prisma.auditLog.create({
data: {
action: "PAYMENT_COMPLETED",
entityType: "Order",
entityId: orderId,
changes: JSON.stringify({
paymentIntentId,
amount: session.amount_total,
currency: session.currency,
}),
},
});
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

The audit log creation is missing critical fields. The AuditLog model typically requires userId and storeId for proper multi-tenant tracking and audit trails. Currently, only action, entityType, entityId, and changes are provided. Without userId, you cannot track who initiated the payment, and without storeId, multi-tenant filtering becomes difficult.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 3811e36. Added storeId to audit log entries by fetching order details first. Now properly tracks which store the payment belongs to for multi-tenant audit trails.

Comment on lines 252 to 289
// Create refund record in database
const refundRecord = await prisma.refund.create({
data: {
orderId,
storeId: order.storeId,
paymentAttemptId: paymentAttempt.id,
amount,
status: refund.status === "succeeded" ? "COMPLETED" : "PENDING",
externalId: refund.id,
reason,
processedAt: refund.status === "succeeded" ? new Date() : null,
},
});

// Update order status and restore inventory if fully refunded
if (amount === refundableBalance) {
await prisma.$transaction(async (tx) => {
// Update order status to REFUNDED
await tx.order.update({
where: { id: orderId },
data: {
status: "REFUNDED",
refundedAmount: order.totalAmount,
},
});

// Restore inventory for each order item
for (const item of order.items) {
if (item.product) {
await tx.product.update({
where: { id: item.product.id },
data: { inventoryQty: { increment: item.quantity } },
});
}
}
});
}

Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

The transaction for full refunds updates order status and restores inventory, but this is not atomic with the Stripe refund API call that happens before it (lines 238-250). If the Stripe refund succeeds but the database transaction fails, you'll have issued a refund without updating your records. Consider restructuring to create the refund record first with PENDING status, then update to COMPLETED in the webhook handler when charge.refunded is received.

Suggested change
// Create refund record in database
const refundRecord = await prisma.refund.create({
data: {
orderId,
storeId: order.storeId,
paymentAttemptId: paymentAttempt.id,
amount,
status: refund.status === "succeeded" ? "COMPLETED" : "PENDING",
externalId: refund.id,
reason,
processedAt: refund.status === "succeeded" ? new Date() : null,
},
});
// Update order status and restore inventory if fully refunded
if (amount === refundableBalance) {
await prisma.$transaction(async (tx) => {
// Update order status to REFUNDED
await tx.order.update({
where: { id: orderId },
data: {
status: "REFUNDED",
refundedAmount: order.totalAmount,
},
});
// Restore inventory for each order item
for (const item of order.items) {
if (item.product) {
await tx.product.update({
where: { id: item.product.id },
data: { inventoryQty: { increment: item.quantity } },
});
}
}
});
}
// Create refund record in database with PENDING status before calling Stripe
const refundRecord = await prisma.refund.create({
data: {
orderId,
storeId: order.storeId,
paymentAttemptId: paymentAttempt.id,
amount,
status: "PENDING",
externalId: null,
reason,
processedAt: null,
},
});
// Issue refund via Stripe API
const refund = await stripe.refunds.create(
{
payment_intent: paymentAttempt.externalId,
amount: Math.round(amount * 100), // Stripe expects amount in cents
reason: reason as any,
metadata: {
refundRecordId: refundRecord.id,
orderId: orderId,
},
},
{
idempotencyKey: params.idempotencyKey,
}
);
// Optionally, update the refund record with the Stripe refund ID (externalId)
await prisma.refund.update({
where: { id: refundRecord.id },
data: {
externalId: refund.id,
},
});
// NOTE: Do NOT update order status or inventory here.
// This should be handled in the Stripe webhook handler when the refund is confirmed.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 3811e36. Refactored to create refund record with PENDING status before Stripe API call. Record is updated with externalId and status after Stripe response. Inventory restoration only happens if refund succeeds immediately, otherwise handled by webhook.

where: { id: orderId },
data: {
status: "REFUNDED",
refundedAmount: order.totalAmount,
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

The refundedAmount field is set to order.totalAmount for full refunds, but this doesn't account for scenarios where multiple partial refunds could eventually total the full amount. The field should be incremented rather than set to a fixed value. Consider using: refundedAmount: { increment: amount } to properly track cumulative refunds.

Suggested change
refundedAmount: order.totalAmount,
refundedAmount: { increment: amount },

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 3811e36. Changed refundedAmount from set to increment: { increment: amount } to properly track cumulative refunds across multiple partial refunds.

Comment on lines 162 to 163
const order = await prisma.order.findUnique({
where: { id: orderId },
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

Missing storeId in the where clause for the order lookup. While multi-tenancy filtering is not strictly necessary here since we're fetching by unique ID, it's a security best practice to always filter by storeId to prevent potential data leakage. The repository guidelines emphasize always filtering by both userId AND organizationId (or storeId) for multi-tenant queries.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 3811e36. Changed to findFirst with storeId filter following repository multi-tenancy security best practices to prevent potential data leakage.

Comment on lines 53 to 64
// Redirect to Stripe Checkout
window.location.href = sessionUrl;
} catch (error) {
console.error("[CheckoutButton] Error:", error);

const errorMessage = error instanceof Error
? error.message
: "Failed to start checkout";

toast.error(errorMessage);
setLoading(false);
}
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

The CheckoutButton component doesn't reset the loading state if the redirect fails or is blocked. After setting window.location.href (line 54), if the redirect doesn't happen (e.g., popup blocker, navigation error), the button will remain in the loading state indefinitely. While rare, consider adding a timeout fallback or error handling for failed redirects.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 3811e36. Added 5-second timeout fallback that checks if document still has focus, then resets loading state and shows error toast if redirect failed (e.g., popup blocker).

@rafiqul4
Copy link
Collaborator

@copilot apply changes based on the comments in this thread

…handling, refund atomicity, and inventory restoration

Co-authored-by: rafiqul4 <124497017+rafiqul4@users.noreply.github.com>
Co-authored-by: rafiqul4 <124497017+rafiqul4@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Dec 11, 2025

@copilot apply changes based on the comments in this thread

All 18 code review comments have been addressed in commit 3811e36. Key fixes include:

Security & Validation:

  • Webhook secret validation at startup
  • Multi-tenant security with storeId filtering
  • Error message sanitization

Payment Processing:

  • Fixed payment_intent null handling (store session.id initially)
  • Corrected Stripe Connect pattern (platform instance + stripeAccount option)
  • Added race condition prevention in webhooks

Refund Processing:

  • Made refund operations atomic (PENDING → Stripe → COMPLETED)
  • Complete inventory restoration with variant handling
  • Improved idempotency key generation
  • Fixed cumulative refundedAmount tracking

Audit & Monitoring:

  • Added storeId to all audit logs
  • Created audit logs for refund completion
  • Verified update counts

Frontend:

  • Added redirect fallback timeout

Detailed documentation available in docs/CODE_REVIEW_FIXES.md.

@rafiqul4
Copy link
Collaborator

@copilot fix vercel deploiyment issue

…e local Prisma CLI

Co-authored-by: rafiqul4 <124497017+rafiqul4@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Dec 11, 2025

@copilot fix vercel deploiyment issue

Fixed in commit 793166b. The Vercel deployment issues were caused by:

  1. Module-level Stripe validation: Environment variable checks at module initialization failed builds when Stripe keys weren't set. Changed to runtime-only validation - modules import successfully, but routes return 503 if Stripe isn't configured.

  2. Prisma version mismatch: Build script used npx prisma generate which pulled v7.1.0, incompatible with project's v6.19.0. Changed to use local Prisma CLI: node node_modules/.bin/prisma generate.

Build now succeeds with placeholder values from .env.example. Set actual Stripe credentials in Vercel environment variables for production.

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

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants