> ## Documentation Index
> Fetch the complete documentation index at: https://twigz.arqqin.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Payments

> Stripe integration for secure payment processing with marketplace commissions

## Overview

The Payments API provides secure payment processing through Stripe with automatic marketplace commission handling. It supports PaymentIntents for modern payment flows, handles platform fees, and integrates seamlessly with the order creation system.

**Location:** `convex/customers/payments.ts`

## Payment Flow

<Steps>
  <Step title="Create Payment Intent">
    Generate Stripe PaymentIntent with cart totals and commission validation
  </Step>

  <Step title="Process Payment">
    Handle payment on frontend using Stripe Elements or Payment Sheet
  </Step>

  <Step title="Webhook Processing">
    Automatic order creation via Stripe webhooks on successful payment
  </Step>

  <Step title="Order Fulfillment">
    Store receives order and begins fulfillment process
  </Step>
</Steps>

## Create Payment Intent

Creates a Stripe PaymentIntent for cart checkout with platform commission validation.

<ParamField path="customerId" type="Id<'customers'>" required>
  Customer ID making the purchase
</ParamField>

<ParamField path="storeId" type="Id<'stores'>" required>
  Store ID receiving the order
</ParamField>

<ParamField path="subtotal" type="number" required>
  Order subtotal before fees
</ParamField>

<ParamField path="deliveryFee" type="number" required>
  Delivery fee amount
</ParamField>

<ParamField path="platformFee" type="number" required>
  Platform commission fee
</ParamField>

<ParamField path="totalAmount" type="number" required>
  Total order amount (subtotal + delivery + platform fee)
</ParamField>

<ParamField path="currency" type="string">
  Currency code (default: "usd")
</ParamField>

<ParamField path="cartItems" type="Array<object>" required>
  Cart items for payment metadata
</ParamField>

<ParamField path="deliveryType" type="'delivery' | 'pickup'" required>
  Delivery method
</ParamField>

<ParamField path="deliveryAddress" type="object">
  Delivery address details (required for delivery orders)
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const paymentSetup = await convex.action(api.customers.payments.createPaymentIntent, {
    customerId: "c123456789",
    storeId: "j123456789",
    subtotal: 45.97,
    deliveryFee: 5.00,
    platformFee: 3.20,
    totalAmount: 54.17,
    currency: "usd",
    cartItems: [
      { 
        productId: "p123456789", 
        productName: "Pizza Margherita", 
        quantity: 2, 
        price: 22.99 
      }
    ],
    deliveryType: "delivery",
    deliveryAddress: {
      fullAddress: "123 Main St, Apt 4B",
      city: "ct123456789",
      area: "ar123456789"
    }
  });
  ```

  ```javascript JavaScript theme={null}
  const paymentSetup = await convex.action("customers.payments.createPaymentIntent", {
    customerId: "c123456789",
    storeId: "j123456789",
    subtotal: 45.97,
    deliveryFee: 5.00,
    platformFee: 3.20,
    totalAmount: 54.17,
    currency: "usd",
    cartItems: [
      { 
        productId: "p123456789", 
        productName: "Pizza Margherita", 
        quantity: 2, 
        price: 22.99 
      }
    ],
    deliveryType: "delivery"
  });
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      f"{CONVEX_URL}/api/action",
      json={
          "path": "customers.payments.createPaymentIntent",
          "args": {
              "customerId": "c123456789",
              "storeId": "j123456789",
              "subtotal": 45.97,
              "deliveryFee": 5.00,
              "platformFee": 3.20,
              "totalAmount": 54.17,
              "currency": "usd",
              "cartItems": [
                  {
                      "productId": "p123456789",
                      "productName": "Pizza Margherita",
                      "quantity": 2,
                      "price": 22.99
                  }
              ],
              "deliveryType": "delivery"
          }
      },
      headers={"Authorization": f"Bearer {token}"}
  )

  payment_setup = response.json()
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "clientSecret": "pi_1234567890_secret_abcdefghijklmnop",
    "paymentIntentId": "pi_1234567890",
    "totalAmount": 54.17
  }
  ```
</ResponseExample>

## Get Payment Intent Status

Retrieve the current status of a Stripe PaymentIntent.

<ParamField path="paymentIntentId" type="string" required>
  Stripe PaymentIntent ID to check
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const status = await convex.action(api.customers.payments.getPaymentIntentStatus, {
    paymentIntentId: "pi_123456789"
  });
  ```

  ```javascript JavaScript theme={null}
  const status = await convex.action("customers.payments.getPaymentIntentStatus", {
    paymentIntentId: "pi_123456789"
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "status": "succeeded",
    "amount": 54.17,
    "currency": "usd",
    "metadata": {
      "customerId": "c123456789",
      "storeId": "j123456789",
      "deliveryType": "delivery"
    }
  }
  ```
</ResponseExample>

## Cancel Payment Intent

Cancel a Stripe PaymentIntent if user cancels checkout.

<ParamField path="paymentIntentId" type="string" required>
  Stripe PaymentIntent ID to cancel
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await convex.action(api.customers.payments.cancelPaymentIntent, {
    paymentIntentId: "pi_123456789"
  });
  ```

  ```javascript JavaScript theme={null}
  const result = await convex.action("customers.payments.cancelPaymentIntent", {
    paymentIntentId: "pi_123456789"
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "status": "canceled"
  }
  ```
</ResponseExample>

## Complete Payment Integration

Here's a complete payment integration with Stripe Elements:

<CodeGroup>
  ```typescript React with Stripe Elements theme={null}
  import React, { useState, useEffect } from 'react';
  import { loadStripe } from '@stripe/stripe-js';
  import {
    Elements,
    CardElement,
    useStripe,
    useElements
  } from '@stripe/react-stripe-js';
  import { useAction } from 'convex/react';
  import { api } from './convex/_generated/api';

  const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);

  interface PaymentFormProps {
    customerId: string;
    storeId: string;
    subtotal: number;
    deliveryFee: number;
    platformFee: number;
    totalAmount: number;
    cartItems: any[];
    deliveryType: 'delivery' | 'pickup';
    deliveryAddress?: any;
    onSuccess: (orderId: string) => void;
    onError: (error: string) => void;
  }

  function PaymentForm({
    customerId,
    storeId,
    subtotal,
    deliveryFee,
    platformFee,
    totalAmount,
    cartItems,
    deliveryType,
    deliveryAddress,
    onSuccess,
    onError
  }: PaymentFormProps) {
    const stripe = useStripe();
    const elements = useElements();
    const [processing, setProcessing] = useState(false);
    const [clientSecret, setClientSecret] = useState<string | null>(null);

    const createPaymentIntent = useAction(api.customers.payments.createPaymentIntent);
    const createOrder = useMutation(api.customers.cartToOrder.createOrderFromCart);

    // Create PaymentIntent when component mounts
    useEffect(() => {
      const setupPayment = async () => {
        try {
          const paymentSetup = await createPaymentIntent({
            customerId,
            storeId,
            subtotal,
            deliveryFee,
            platformFee,
            totalAmount,
            currency: "usd",
            cartItems,
            deliveryType,
            deliveryAddress
          });

          setClientSecret(paymentSetup.clientSecret);
        } catch (error) {
          console.error('Payment setup failed:', error);
          onError('Failed to initialize payment');
        }
      };

      setupPayment();
    }, []);

    const handleSubmit = async (event: React.FormEvent) => {
      event.preventDefault();

      if (!stripe || !elements || !clientSecret) {
        return;
      }

      setProcessing(true);

      const card = elements.getElement(CardElement);
      if (!card) {
        setProcessing(false);
        return;
      }

      try {
        // Confirm payment with Stripe
        const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
          payment_method: {
            card: card,
            billing_details: {
              name: 'Customer Name', // You can get this from user profile
            },
          }
        });

        if (error) {
          console.error('Payment failed:', error);
          onError(error.message || 'Payment failed');
        } else if (paymentIntent.status === 'succeeded') {
          // Payment successful - create order
          const order = await createOrder({
            customerId,
            storeId,
            deliveryAddress: deliveryAddress || {
              fullAddress: "Pickup from store",
              city: "" as any,
              area: "" as any,
              phoneNumber: "N/A"
            },
            deliveryType,
            deliveryFee,
            paymentMethod: "card",
            paymentIntentId: paymentIntent.id,
            paymentStatus: "paid"
          });

          onSuccess(order.orderId);
        }
      } catch (error) {
        console.error('Payment processing error:', error);
        onError('Payment processing failed');
      } finally {
        setProcessing(false);
      }
    };

    return (
      <form onSubmit={handleSubmit} className="space-y-6">
        {/* Order Summary */}
        <div className="bg-gray-50 p-4 rounded-lg">
          <h3 className="font-medium text-gray-900 mb-2">Order Summary</h3>
          <div className="space-y-2 text-sm">
            <div className="flex justify-between">
              <span>Subtotal</span>
              <span>${subtotal.toFixed(2)}</span>
            </div>
            <div className="flex justify-between">
              <span>Delivery Fee</span>
              <span>${deliveryFee.toFixed(2)}</span>
            </div>
            <div className="flex justify-between">
              <span>Platform Fee</span>
              <span>${platformFee.toFixed(2)}</span>
            </div>
            <div className="border-t pt-2 flex justify-between font-medium">
              <span>Total</span>
              <span>${totalAmount.toFixed(2)}</span>
            </div>
          </div>
        </div>

        {/* Card Element */}
        <div className="bg-white p-4 border rounded-lg">
          <label className="block text-sm font-medium text-gray-700 mb-2">
            Card Information
          </label>
          <CardElement
            options={{
              style: {
                base: {
                  fontSize: '16px',
                  color: '#424770',
                  '::placeholder': {
                    color: '#aab7c4',
                  },
                },
              },
            }}
          />
        </div>

        {/* Submit Button */}
        <button
          type="submit"
          disabled={!stripe || processing}
          className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
        >
          {processing ? (
            <>
              <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
              </svg>
              Processing Payment...
            </>
          ) : (
            `Pay $${totalAmount.toFixed(2)}`
          )}
        </button>
      </form>
    );
  }

  // Main component with Stripe provider
  function CheckoutPage({ orderData }: { orderData: any }) {
    return (
      <Elements stripe={stripePromise}>
        <PaymentForm
          {...orderData}
          onSuccess={(orderId) => {
            console.log('Order created:', orderId);
            // Navigate to success page
          }}
          onError={(error) => {
            console.error('Payment error:', error);
            // Show error message
          }}
        />
      </Elements>
    );
  }

  export { PaymentForm, CheckoutPage };
  ```

  ```typescript React Native with Stripe theme={null}
  import React, { useState } from 'react';
  import { Alert } from 'react-native';
  import { useStripe } from '@stripe/stripe-react-native';
  import { useAction, useMutation } from 'convex/react';
  import { api } from './convex/_generated/api';

  interface PaymentSheetProps {
    customerId: string;
    storeId: string;
    orderData: any;
    onSuccess: (orderId: string) => void;
  }

  function PaymentSheet({ customerId, storeId, orderData, onSuccess }: PaymentSheetProps) {
    const { initPaymentSheet, presentPaymentSheet } = useStripe();
    const [loading, setLoading] = useState(false);

    const createPaymentIntent = useAction(api.customers.payments.createPaymentIntent);
    const createOrder = useMutation(api.customers.cartToOrder.createOrderFromCart);

    const openPaymentSheet = async () => {
      try {
        setLoading(true);

        // Create PaymentIntent
        const paymentSetup = await createPaymentIntent({
          customerId,
          storeId,
          subtotal: orderData.subtotal,
          deliveryFee: orderData.deliveryFee,
          platformFee: orderData.platformFee,
          totalAmount: orderData.totalAmount,
          cartItems: orderData.cartItems,
          deliveryType: orderData.deliveryType,
          deliveryAddress: orderData.deliveryAddress
        });

        // Initialize payment sheet
        const { error: initError } = await initPaymentSheet({
          merchantDisplayName: 'Twigz Marketplace',
          paymentIntentClientSecret: paymentSetup.clientSecret,
          defaultBillingDetails: {
            name: 'Customer Name',
          },
          allowsDelayedPaymentMethods: false,
        });

        if (initError) {
          Alert.alert('Error', initError.message);
          return;
        }

        // Present payment sheet
        const { error: presentError } = await presentPaymentSheet();

        if (presentError) {
          Alert.alert('Payment Failed', presentError.message);
        } else {
          // Payment successful - create order
          const order = await createOrder({
            customerId,
            storeId,
            deliveryAddress: orderData.deliveryAddress,
            deliveryType: orderData.deliveryType,
            deliveryFee: orderData.deliveryFee,
            paymentMethod: "card",
            paymentIntentId: paymentSetup.paymentIntentId,
            paymentStatus: "paid"
          });

          Alert.alert('Success', 'Payment successful!');
          onSuccess(order.orderId);
        }
      } catch (error) {
        console.error('Payment error:', error);
        Alert.alert('Error', 'Payment processing failed');
      } finally {
        setLoading(false);
      }
    };

    return (
      <TouchableOpacity
        onPress={openPaymentSheet}
        disabled={loading}
        style={{
          backgroundColor: loading ? '#ccc' : '#007AFF',
          padding: 16,
          borderRadius: 8,
          alignItems: 'center'
        }}
      >
        <Text style={{ color: 'white', fontSize: 16, fontWeight: 'bold' }}>
          {loading ? 'Processing...' : `Pay $${orderData.totalAmount.toFixed(2)}`}
        </Text>
      </TouchableOpacity>
    );
  }

  export default PaymentSheet;
  ```
</CodeGroup>

## Payment Status Monitoring

Monitor payment status for order processing:

<CodeGroup>
  ```typescript Payment Status Hook theme={null}
  import { useState, useEffect } from 'react';
  import { useAction } from 'convex/react';
  import { api } from './convex/_generated/api';

  function usePaymentStatus(paymentIntentId: string | null) {
    const [status, setStatus] = useState<string | null>(null);
    const [loading, setLoading] = useState(false);
    
    const getPaymentStatus = useAction(api.customers.payments.getPaymentIntentStatus);

    useEffect(() => {
      if (!paymentIntentId) return;

      const checkStatus = async () => {
        setLoading(true);
        try {
          const result = await getPaymentStatus({ paymentIntentId });
          setStatus(result.status);
        } catch (error) {
          console.error('Failed to check payment status:', error);
        } finally {
          setLoading(false);
        }
      };

      // Check immediately
      checkStatus();

      // Poll every 2 seconds for status updates
      const interval = setInterval(checkStatus, 2000);

      // Cleanup
      return () => clearInterval(interval);
    }, [paymentIntentId]);

    return { status, loading };
  }

  // Usage in component
  function OrderStatus({ paymentIntentId }: { paymentIntentId: string }) {
    const { status, loading } = usePaymentStatus(paymentIntentId);

    const getStatusDisplay = () => {
      switch (status) {
        case 'requires_payment_method':
          return { text: 'Waiting for payment', color: 'yellow' };
        case 'requires_confirmation':
          return { text: 'Confirming payment', color: 'blue' };
        case 'processing':
          return { text: 'Processing payment', color: 'blue' };
        case 'succeeded':
          return { text: 'Payment successful', color: 'green' };
        case 'canceled':
          return { text: 'Payment canceled', color: 'gray' };
        case 'requires_action':
          return { text: 'Action required', color: 'orange' };
        default:
          return { text: 'Unknown status', color: 'gray' };
      }
    };

    const statusDisplay = getStatusDisplay();

    return (
      <div className="flex items-center space-x-2">
        <div className={`w-3 h-3 rounded-full ${
          statusDisplay.color === 'green' ? 'bg-green-500' :
          statusDisplay.color === 'blue' ? 'bg-blue-500' :
          statusDisplay.color === 'yellow' ? 'bg-yellow-500' :
          statusDisplay.color === 'orange' ? 'bg-orange-500' :
          'bg-gray-500'
        } ${loading ? 'animate-pulse' : ''}`}></div>
        <span className="text-sm font-medium">{statusDisplay.text}</span>
      </div>
    );
  }
  ```
</CodeGroup>

## Stripe Webhook Integration

The payment system automatically processes orders via webhooks:

```typescript theme={null}
// Webhook endpoint: POST /stripe/webhook
// Handles these events:

// 1. payment_intent.succeeded
//    → Creates order from cart
//    → Clears customer cart
//    → Credits store wallet
//    → Sends confirmation notifications

// 2. payment_intent.payment_failed
//    → Logs failure
//    → Sends failure notification
//    → Preserves cart for retry

// 3. payment_intent.canceled
//    → Cleans up resources
//    → Preserves cart
//    → Logs cancellation
```

## Payment Methods

<CardGroup cols={3}>
  <Card title="Credit Cards" icon="credit-card">
    **Supported**: Visa, Mastercard, American Express\
    **Processing**: Instant via Stripe\
    **Fees**: Standard Stripe rates
  </Card>

  <Card title="Debit Cards" icon="credit-card">
    **Supported**: Local and international debit cards\
    **Processing**: Instant verification\
    **Fees**: Lower processing rates
  </Card>

  <Card title="Digital Wallets" icon="wallet">
    **Supported**: Apple Pay, Google Pay\
    **Processing**: One-touch payments\
    **Fees**: Same as card rates
  </Card>
</CardGroup>

## Commission Handling

The payment system automatically handles marketplace commissions:

```mermaid theme={null}
graph TD
    A[Customer Payment:    AED 54.17] --> B[Stripe Processing]
    B --> C[Platform Commission: AED 3.20]
    B --> D[Store Earnings: AED 45.97]
    B --> E[Delivery Fee: AED 5.00]
    
    C --> F[Platform Wallet]
    D --> G[Store Wallet]
    E --> G
    
    G --> H[Available for Payout]
```

## Error Handling

<AccordionGroup>
  <Accordion title="Payment Intent creation failed">
    **Status Code**: `400`

    ```json theme={null}
    {
      "error": "Failed to create payment intent",
      "details": "Invalid amount or currency"
    }
    ```
  </Accordion>

  <Accordion title="Payment declined">
    **Status Code**: `402`

    ```json theme={null}
    {
      "error": "Payment declined",
      "code": "card_declined",
      "decline_code": "insufficient_funds"
    }
    ```
  </Accordion>

  <Accordion title="Invalid payment method">
    **Status Code**: `400`

    ```json theme={null}
    {
      "error": "Invalid payment method",
      "supported_methods": ["card", "cash", "wallet"]
    }
    ```
  </Accordion>

  <Accordion title="Commission validation failed">
    **Status Code**: `400`

    ```json theme={null}
    {
      "error": "Commission validation failed",
      "expected": 3.20,
      "provided": 2.80
    }
    ```
  </Accordion>
</AccordionGroup>

## Security Features

<CardGroup cols={2}>
  <Card title="PCI Compliance" icon="shield-check">
    All card data is handled by Stripe, ensuring PCI DSS compliance
  </Card>

  <Card title="Fraud Detection" icon="eye">
    Stripe's machine learning fraud detection protects against fraudulent transactions
  </Card>

  <Card title="3D Secure" icon="lock">
    Automatic 3D Secure authentication for enhanced security
  </Card>

  <Card title="Webhook Verification" icon="signature">
    All webhooks are verified using Stripe signatures
  </Card>
</CardGroup>

<Note>
  The payment system automatically validates platform commission amounts to ensure consistency between frontend calculations and backend processing.
</Note>

<Tip>
  Always handle payment failures gracefully and provide clear error messages to users. Use Stripe's test cards for development and testing.
</Tip>
