Welcome to the Jose Madrid Salsa developer docs — explore features, APIs, and deployment guides.
Jose Madrid SalsaJMS Docs

Payment Integration

Multi-provider payment processing with Stripe and PayPal, webhook handling, refund management, tax calculation, and comprehensive error recovery

Payment Integration

This guide covers payment processing integration with Stripe and PayPal, webhook configuration, refund handling, tax calculation, and error management.

Overview

The platform supports multiple payment providers for maximum flexibility and geographic coverage:

  • Stripe — Primary payment processor for cards, Apple Pay, Google Pay
  • PayPal — Secondary processor for PayPal and Venmo payments

Both providers are configured in a provider registry pattern, allowing seamless switching and multi-provider support.

Environment Variables

Stripe Configuration

STRIPE_SECRET_KEY="sk_test_..."              # Secret key for backend API calls
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..." # Public key for client-side Stripe.js
STRIPE_WEBHOOK_SECRET="whsec_..."            # Webhook signature verification secret

PayPal Configuration

PAYPAL_CLIENT_ID="AQ..."                     # OAuth client ID for backend calls
PAYPAL_CLIENT_SECRET="..."                   # OAuth client secret (keep private)
NEXT_PUBLIC_PAYPAL_CLIENT_ID="AQ..."         # Client ID for client-side SDK
PAYPAL_WEBHOOK_ID="WH-..."                   # Webhook endpoint ID

Payment Flow Diagram

User initiates checkout

[Select payment method]

   ┌─────────────────────────┐
   │ Stripe or PayPal SDK    │
   │ Authenticate payment    │
   └─────────┬───────────────┘

      [Confirm payment]

    ┌────────────────────┐
    │ POST /api/checkout/complete
    │ - Verify payment   │
    │ - Update order     │
    │ - Create shipment  │
    └────────┬───────────┘

   [Webhook verification]

  ┌──────────────────────┐
  │ POST /api/webhooks/  │
  │ [stripe|paypal]      │
  │ - Validate signature │
  │ - Update DB idempotently
  │ - Send notifications │
  └──────────────────────┘

Stripe Integration

Payment Intent Creation

File: lib/payments/providers/stripe.ts

export async function createPaymentIntent(
  amount: number,
  currency: string,
  orderId: string,
  metadata?: Record<string, string>
) {
  const paymentIntent = await stripe.paymentIntents.create({
    amount: Math.round(amount * 100), // Convert to cents
    currency,
    metadata: {
      orderId,
      ...metadata
    },
    automatic_payment_methods: {
      enabled: true
    }
  })

  return {
    clientSecret: paymentIntent.client_secret,
    intentId: paymentIntent.id
  }
}

Payment Confirmation (Client-side)

File: app/(public)/checkout/page.tsx

const handlePayment = async () => {
  const { error, paymentIntent } = await stripe.confirmCardPayment(
    clientSecret,
    {
      payment_method: {
        card: cardElement,
        billing_details: {
          name: `${firstName} ${lastName}`,
          email,
          address: {
            line1: address,
            city,
            state,
            postal_code: zip,
            country: 'US'
          }
        }
      }
    }
  )

  if (error) {
    setError(error.message)
    return
  }

  if (paymentIntent?.status === 'succeeded') {
    router.push(`/order-confirmation?orderId=${orderId}`)
  }
}

Webhook Configuration

Stripe Webhook Events

File: lib/stripe/webhooks.ts

The webhook handler processes Stripe events with idempotent database updates:

export async function handleStripeWebhook(event: Stripe.Event) {
  // Check if event already processed (idempotency)
  const existingEvent = await db.webhookEvent.findUnique({
    where: { stripeEventId: event.id }
  })

  if (existingEvent) {
    return { received: true }
  }

  // Process event
  switch (event.type) {
    case 'payment_intent.succeeded':
      await handlePaymentIntentSucceeded(event.data.object)
      break

    case 'payment_intent.payment_failed':
      await handlePaymentIntentFailed(event.data.object)
      break

    case 'payment_intent.canceled':
      await handlePaymentIntentCanceled(event.data.object)
      break

    case 'charge.refunded':
      await handleChargeRefunded(event.data.object)
      break
  }

  // Record event as processed
  await db.webhookEvent.create({
    data: {
      stripeEventId: event.id,
      type: event.type,
      processedAt: new Date()
    }
  })

  return { received: true }
}

Supported Webhook Events

EventHandlerAction
payment_intent.succeededhandlePaymentIntentSucceededMark order PAID, create shipment, send confirmation email
payment_intent.payment_failedhandlePaymentIntentFailedMark order FAILED, send payment retry email
payment_intent.canceledhandlePaymentIntentCanceledMark order CANCELLED, notify customer
charge.refundedhandleChargeRefundedUpdate order status, create refund record, notify customer

Webhook Endpoint

Endpoint: POST /api/webhooks/stripe

URL: https://yourdomain.com/api/webhooks/stripe

Setup in Stripe Dashboard:

  1. Navigate to Developers → Webhooks
  2. Click Add endpoint
  3. Paste webhook URL
  4. Select events: payment_intent.*, charge.refunded
  5. Copy Signing Secret → STRIPE_WEBHOOK_SECRET

PayPal Integration

PayPal SDK Setup

File: components/checkout/PayPalProvider.tsx

import { PayPalScriptProvider } from '@paypal/checkout-js-sdk'

export function PayPalCheckoutProvider({ children }: { children: React.ReactNode }) {
  return (
    <PayPalScriptProvider
      options={{
        clientId: process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID!,
        currency: 'USD',
        vault: false,
        components: 'buttons,funding-eligibility',
        intent: 'capture'
      }}
    >
      {children}
    </PayPalScriptProvider>
  )
}

PayPal Button Component

File: components/checkout/PayPalButton.tsx

import { PayPalButtons } from '@paypal/checkout-js-sdk'

export function PayPalButton({ orderId, amount, onSuccess, onError }: PayPalButtonProps) {
  return (
    <PayPalButtons
      createOrder={async () => {
        const response = await fetch('/api/paypal/create-order', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ orderId, amount })
        })
        return response.json().then(d => d.orderId)
      }}
      onApprove={async (data) => {
        const response = await fetch('/api/paypal/capture-order', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ orderId: data.orderID })
        })
        onSuccess(await response.json())
      }}
      onError={onError}
    />
  )
}

Venmo Support

File: components/checkout/VenmoButton.tsx

PayPal SDK automatically includes Venmo when available (US users on mobile).

import { PayPalButtons, FUNDING } from '@paypal/checkout-js-sdk'

export function VenmoButton({ orderId, amount, onSuccess, onError }: VenmoButtonProps) {
  return (
    <PayPalButtons
      fundingSource={FUNDING.VENMO}
      createOrder={async () => {
        // Same as PayPal flow
      }}
      onApprove={onSuccess}
      onError={onError}
    />
  )
}

Refund Process

Refund API Endpoint

Endpoint: POST /api/admin/orders/[id]/refund

Request:

interface RefundRequest {
  amount?: number        // Optional: partial refund (cents). If omitted, full refund
  reason?: string       // Reason for refund
  notifyCustomer?: boolean // Default: true
}

Response:

interface RefundResponse {
  refundId: string
  chargeId: string
  amount: number
  status: 'succeeded' | 'failed'
  message?: string
  error?: string
}

Refund Implementation

File: app/api/admin/orders/[id]/refund/route.ts

export async function POST(
  req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params
  const body = await req.json()
  const { amount, reason, notifyCustomer = true } = body

  // Verify authorization
  const session = await auth()
  if (!session?.user?.email || session.user.role !== 'ADMIN') {
    return Response.json({ error: 'Unauthorized' }, { status: 403 })
  }

  try {
    // Fetch order
    const order = await db.order.findUnique({
      where: { id },
      include: { items: true }
    })

    if (!order) {
      return Response.json({ error: 'Order not found' }, { status: 404 })
    }

    // Verify Stripe payment
    if (!order.stripePaymentId) {
      return Response.json(
        { error: 'Order was not paid via Stripe' },
        { status: 400 }
      )
    }

    // Fetch charge
    const charge = await stripe.charges.retrieve(order.stripePaymentId)

    // Calculate refundable amount
    const refundableAmount = order.total - (charge.amount_refunded / 100)
    const refundAmount = amount ? amount / 100 : refundableAmount

    if (refundAmount > refundableAmount) {
      return Response.json(
        { error: `Maximum refundable: $${refundableAmount.toFixed(2)}` },
        { status: 400 }
      )
    }

    // Create refund
    const refund = await stripe.refunds.create({
      charge: order.stripePaymentId,
      amount: Math.round(refundAmount * 100),
      metadata: { orderId: order.id, reason }
    })

    // Update order
    await db.order.update({
      where: { id },
      data: {
        paymentStatus: refund.status === 'succeeded' ? 'REFUNDED' : 'FAILED',
        refundedAmount: {
          increment: refund.amount / 100
        }
      }
    })

    // Notify customer
    if (notifyCustomer) {
      await sendRefundEmail(order, refund.amount / 100)
    }

    // Create audit log
    await createAuditLog(session.user.id, 'order.refund', {
      orderId: id,
      refundId: refund.id,
      amount: refund.amount / 100
    })

    return Response.json({
      refundId: refund.id,
      chargeId: order.stripePaymentId,
      amount: refund.amount / 100,
      status: refund.status
    })
  } catch (error) {
    console.error('Refund error:', error)

    // Handle Stripe-specific errors
    if (error instanceof Stripe.errors.StripeInvalidRequestError) {
      return Response.json(
        { error: error.message, code: error.code },
        { status: 400 }
      )
    }

    return Response.json(
      { error: 'Refund failed' },
      { status: 500 }
    )
  }
}

Tax Calculation

Stripe Tax Integration

File: lib/tax-calculator.ts

The platform uses Stripe Tax API for accurate tax calculations across US states:

export async function calculateTax(
  lineItems: Array<{ amount: number; quantity: number }>,
  address: {
    line1: string
    city: string
    state: string
    postalCode: string
    country: string
  }
) {
  const calculation = await stripe.tax.calculations.create({
    line_items: lineItems,
    customer_details: {
      address: {
        line1: address.line1,
        city: address.city,
        state: address.state,
        postal_code: address.postalCode,
        country: address.country
      }
    },
    expand: ['line_items', 'tax_breakdown']
  })

  return {
    taxAmount: calculation.tax_amount_exclusive / 100,
    breakdown: calculation.tax_breakdown
  }
}

Tax Code

Product tax code: txcd_30011000 (Food & Beverage - prepared foods)

Set on Stripe Product:

const product = await stripe.products.create({
  name: 'Salsa Kit',
  tax_code: 'txcd_30011000'
})

Error Handling

Client-side Payment Errors

Common Stripe error codes:

CodeMessageResolution
payment_intent_authentication_failure3D Secure authentication requiredPrompt user to authenticate with bank
card_declinedCard was declinedAsk customer to try different payment method
rate_limitAPI rate limit exceededRetry with exponential backoff
api_connection_errorNetwork connectivity issueRetry request with exponential backoff
card_errorCard processor errorContact support or try different card

Server-side Recovery

const withRetry = async (
  fn: () => Promise<any>,
  maxRetries = 3,
  backoff = 1000
) => {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn()
    } catch (error) {
      if (i === maxRetries - 1) throw error

      // Exponential backoff
      const delay = backoff * Math.pow(2, i)
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }
}

// Usage
await withRetry(() => stripe.paymentIntents.retrieve(intentId))

Payment Methods

Supported payment methods and providers:

MethodProviderAvailability
VisaStripeWorldwide
MastercardStripeWorldwide
American ExpressStripeWorldwide
Apple PayStripeiOS Safari
Google PayStripeAndroid Chrome, Safari
PayPalPayPal200+ countries
VenmoPayPalUS mobile only

Type Definitions

File: lib/stripe/types.ts

export interface CheckoutSessionRequest {
  orderId: string
  amount: number
  currency: string
  successUrl: string
  cancelUrl: string
  customerEmail: string
}

export interface CheckoutSessionResponse {
  sessionId: string
  url: string
}

export interface PaymentMetadata {
  orderId: string
  customerId: string
  invoiceId?: string
}

export interface RefundRequest {
  chargeId: string
  amount?: number
  reason?: string
}

export interface RefundResponse {
  refundId: string
  status: string
  amount: number
}

export type WebhookEventType =
  | 'payment_intent.succeeded'
  | 'payment_intent.payment_failed'
  | 'charge.refunded'
  | 'charge.dispute.created'

export interface WebhookEventData {
  id: string
  type: WebhookEventType
  data: Record<string, any>
  timestamp: number
}

Key Files

FilePurpose
lib/stripe.tsStripe client initialization and configuration
lib/stripe/types.tsTypeScript type definitions for Stripe integration
lib/stripe/webhooks.tsStripe webhook event handling and idempotency
lib/payments/providers/stripe.tsStripe provider implementation
lib/payments/providers/paypal.tsPayPal provider implementation
lib/payments/registry.tsPayment provider registry and factory
lib/tax-calculator.tsTax calculation via Stripe Tax API
app/api/checkout/route.tsCheckout initiation (create order + payment intent)
app/api/checkout/complete/route.tsCheckout completion (verify payment + confirm order)
app/api/webhooks/stripe/route.tsStripe webhook endpoint
app/api/webhooks/paypal/route.tsPayPal webhook endpoint
app/api/admin/orders/[id]/refund/route.tsRefund processing API
app/(public)/checkout/page.tsxCheckout page with payment form
components/checkout/PayPalProvider.tsxPayPal SDK provider wrapper
components/checkout/PayPalButton.tsxPayPal payment button
components/checkout/VenmoButton.tsxVenmo payment button
components/checkout/PaymentMethodSelector.tsxPayment method selection UI

How is this guide?

Edit on GitHub

Last updated on

On this page