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

# Checkout Helpers

> Commission calculation and checkout validation utilities

## Overview

The Checkout Helpers API provides essential utilities for calculating platform commissions, validating checkout data, and ensuring accurate pricing throughout the checkout process. These functions are critical for maintaining marketplace financial integrity.

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

## Calculate Platform Commission

Calculates platform commission for an order with support for category-specific and store-specific rates.

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

<ParamField path="storeId" type="Id<'stores'>">
  Store ID for store-specific commission rates
</ParamField>

<ParamField path="categoryIds" type="Array<Id<'categories'>>">
  Category IDs for category-specific commission rates
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const commission = await convex.query(api.customers.checkoutHelpers.calculatePlatformCommission, {
    subtotal: 45.97,
    storeId: "j123456789",
    categoryIds: ["k123456789"]
  });
  ```

  ```javascript JavaScript theme={null}
  const commission = await convex.query("customers.checkoutHelpers.calculatePlatformCommission", {
    subtotal: 45.97,
    storeId: "j123456789",
    categoryIds: ["k123456789"]
  });
  ```

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

  response = requests.post(
      f"{CONVEX_URL}/api/query",
      json={
          "path": "customers.checkoutHelpers.calculatePlatformCommission",
          "args": {
              "subtotal": 45.97,
              "storeId": "j123456789",
              "categoryIds": ["k123456789"]
          }
      },
      headers={"Authorization": f"Bearer {token}"}
  )

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

<ResponseExample>
  ```json Response theme={null}
  {
    "platformCommission": 3.20,
    "commissionRate": 0.05,
    "fixedFee": 1.00,
    "calculationDetails": {
      "baseCommission": 2.30,
      "fixedFeeApplied": 1.00,
      "minimumApplied": false,
      "maximumApplied": false,
      "finalCommission": 3.20
    }
  }
  ```
</ResponseExample>

## Commission Calculation Logic

The commission system uses a sophisticated calculation method:

<Steps>
  <Step title="Base Commission">
    Calculate percentage-based commission: `subtotal × commissionRate`
  </Step>

  <Step title="Fixed Fee">
    Add per-order fixed fee to base commission
  </Step>

  <Step title="Minimum/Maximum">
    Apply minimum and maximum commission limits if configured
  </Step>

  <Step title="Category Adjustments">
    Apply category-specific rate adjustments if applicable
  </Step>

  <Step title="Store Adjustments">
    Apply store-specific rate adjustments for premium stores
  </Step>
</Steps>

## Commission Breakdown

Understanding the commission structure:

```mermaid theme={null}
graph TD
    A[Order Subtotal: $45.97] --> B[Base Commission: 5%]
    B --> C[Base Amount: $2.30]
    C --> D[Fixed Fee: $1.00]
    D --> E[Total Commission: $3.30]
    E --> F[Store Earnings: $42.67]
    
    G[Category Rate] --> B
    H[Store Rate] --> B
    I[Minimum/Maximum] --> E
```

## Checkout Validation Hook

Complete checkout validation with commission calculation:

<CodeGroup>
  ```typescript React Hook theme={null}
  import { useQuery } from 'convex/react';
  import { api } from './convex/_generated/api';

  interface CheckoutData {
    customerId: string;
    storeId: string;
    deliveryType: 'delivery' | 'pickup';
    deliveryFee?: number;
  }

  function useCheckoutCalculation({ customerId, storeId, deliveryType, deliveryFee = 5.99 }: CheckoutData) {
    // Get cart data
    const cart = useQuery(api.customers.cart.getCart, { customerId, storeId });
    
    // Calculate commission
    const commission = useQuery(
      api.customers.checkoutHelpers.calculatePlatformCommission,
      cart ? {
        subtotal: cart.subtotal,
        storeId,
        categoryIds: cart.items.map(item => item.categoryId).filter(Boolean)
      } : "skip"
    );

    // Calculate totals
    const totals = React.useMemo(() => {
      if (!cart || !commission) return null;

      const actualDeliveryFee = deliveryType === 'pickup' ? 0 : deliveryFee;
      const totalAmount = cart.subtotal + actualDeliveryFee;
      const storeEarnings = cart.subtotal - commission.platformCommission;

      return {
        subtotal: cart.subtotal,
        deliveryFee: actualDeliveryFee,
        platformCommission: commission.platformCommission,
        storeEarnings,
        totalAmount,
        itemsCount: cart.itemsCount,
        commissionRate: commission.commissionRate,
        commissionDetails: commission.calculationDetails
      };
    }, [cart, commission, deliveryType, deliveryFee]);

    return {
      cart,
      commission,
      totals,
      isLoading: cart === undefined || commission === undefined,
      hasError: cart === null
    };
  }

  // Usage in checkout component
  function CheckoutSummary({ customerId, storeId, deliveryType }: CheckoutData) {
    const { totals, isLoading, hasError } = useCheckoutCalculation({
      customerId,
      storeId,
      deliveryType
    });

    if (isLoading) return <div>Calculating totals...</div>;
    if (hasError) return <div>Unable to load cart</div>;
    if (!totals) return <div>No items in cart</div>;

    return (
      <div className="bg-white p-6 rounded-lg shadow">
        <h2 className="text-lg font-semibold mb-4">Order Summary</h2>
        
        <div className="space-y-3">
          <div className="flex justify-between">
            <span>Subtotal ({totals.itemsCount} items)</span>
            <span>${totals.subtotal.toFixed(2)}</span>
          </div>
          
          <div className="flex justify-between">
            <span>Delivery Fee</span>
            <span>${totals.deliveryFee.toFixed(2)}</span>
          </div>
          
          <div className="flex justify-between text-sm text-gray-600">
            <span>Platform Fee ({(totals.commissionRate * 100).toFixed(1)}%)</span>
            <span>${totals.platformCommission.toFixed(2)}</span>
          </div>
          
          <div className="border-t pt-3 flex justify-between font-semibold text-lg">
            <span>Total</span>
            <span>${totals.totalAmount.toFixed(2)}</span>
          </div>
          
          <div className="text-sm text-gray-600">
            <p>Store receives: ${totals.storeEarnings.toFixed(2)}</p>
          </div>
        </div>

        {/* Commission Breakdown (for debugging/transparency) */}
        {process.env.NODE_ENV === 'development' && totals.commissionDetails && (
          <div className="mt-4 p-3 bg-gray-50 rounded text-xs">
            <p><strong>Commission Breakdown:</strong></p>
            <p>Base: ${totals.commissionDetails.baseCommission.toFixed(2)}</p>
            <p>Fixed Fee: ${totals.commissionDetails.fixedFeeApplied.toFixed(2)}</p>
            <p>Min Applied: {totals.commissionDetails.minimumApplied ? 'Yes' : 'No'}</p>
            <p>Max Applied: {totals.commissionDetails.maximumApplied ? 'Yes' : 'No'}</p>
            <p>Final: ${totals.commissionDetails.finalCommission.toFixed(2)}</p>
          </div>
        )}
      </div>
    );
  }

  export { useCheckoutCalculation, CheckoutSummary };
  ```

  ```typescript Vue Composition API theme={null}
  import { computed } from 'vue';
  import { useConvexQuery } from '@convex-vue/core';

  export function useCheckoutCalculation(checkoutData) {
    const cart = useConvexQuery('customers.cart.getCart', {
      customerId: checkoutData.customerId,
      storeId: checkoutData.storeId
    });

    const commission = useConvexQuery('customers.checkoutHelpers.calculatePlatformCommission', 
      computed(() => cart.value ? {
        subtotal: cart.value.subtotal,
        storeId: checkoutData.storeId,
        categoryIds: cart.value.items.map(item => item.categoryId).filter(Boolean)
      } : null)
    );

    const totals = computed(() => {
      if (!cart.value || !commission.value) return null;

      const deliveryFee = checkoutData.deliveryType === 'pickup' ? 0 : (checkoutData.deliveryFee || 5.99);
      const totalAmount = cart.value.subtotal + deliveryFee;
      const storeEarnings = cart.value.subtotal - commission.value.platformCommission;

      return {
        subtotal: cart.value.subtotal,
        deliveryFee,
        platformCommission: commission.value.platformCommission,
        storeEarnings,
        totalAmount,
        itemsCount: cart.value.itemsCount
      };
    });

    return {
      cart,
      commission,
      totals,
      isLoading: computed(() => cart.value === undefined || commission.value === undefined),
      hasError: computed(() => cart.value === null)
    };
  }
  ```
</CodeGroup>

## Commission Rate Structure

<CardGroup cols={2}>
  <Card title="Default Rate" icon="percentage">
    **Standard**: 5% of subtotal\
    **Fixed Fee**: $1.00 per order       **Minimum**: $0.50 per order
  </Card>

  <Card title="Category Rates" icon="tags">
    **Food**: 4% commission\
    **Electronics**: 6% commission\
    **Fashion**: 7% commission
  </Card>

  <Card title="Store Rates" icon="store">
    **New Stores**: Standard rates\
    **Premium Stores**: Reduced rates\
    **High Volume**: Negotiated rates
  </Card>

  <Card title="Special Rates" icon="star">
    **Promotional**: Temporary reduced rates\
    **Seasonal**: Holiday adjustments\
    **Volume Discounts**: Based on monthly sales
  </Card>
</CardGroup>

## Commission Calculation Examples

<AccordionGroup>
  <Accordion title="Standard Food Order">
    **Scenario**: \$50 food order, standard 4% rate

    ```typescript theme={null}
    const commission = await convex.query(api.customers.checkoutHelpers.calculatePlatformCommission, {
      subtotal: 50.00,
      categoryIds: ["food-category-id"]
    });

    // Returns:
    // {
    //   platformCommission: 3.00,  // $50 × 4% = $2.00 + $1.00 fixed fee
    //   commissionRate: 0.04,
    //   fixedFee: 1.00,
    //   calculationDetails: {
    //     baseCommission: 2.00,
    //     fixedFeeApplied: 1.00,
    //     minimumApplied: false,
    //     maximumApplied: false,
    //     finalCommission: 3.00
    //   }
    // }
    ```
  </Accordion>

  <Accordion title="Small Order with Minimum">
    **Scenario**: \$5 order, minimum commission applies

    ```typescript theme={null}
    const commission = await convex.query(api.customers.checkoutHelpers.calculatePlatformCommission, {
      subtotal: 5.00
    });

    // Returns:
    // {
    //   platformCommission: 1.50,  // Minimum commission applied
    //   commissionRate: 0.05,
    //   fixedFee: 1.00,
    //   calculationDetails: {
    //     baseCommission: 0.25,    // $5 × 5% = $0.25
    //     fixedFeeApplied: 1.00,
    //     minimumApplied: true,    // $1.25 < $1.50 minimum
    //     maximumApplied: false,
    //     finalCommission: 1.50
    //   }
    // }
    ```
  </Accordion>

  <Accordion title="High-Value Order">
    **Scenario**: \$1000 electronics order, maximum commission applies

    ```typescript theme={null}
    const commission = await convex.query(api.customers.checkoutHelpers.calculatePlatformCommission, {
      subtotal: 1000.00,
      categoryIds: ["electronics-category-id"]
    });

    // Returns:
    // {
    //   platformCommission: 50.00,  // Maximum cap applied
    //   commissionRate: 0.06,
    //   fixedFee: 1.00,
    //   calculationDetails: {
    //     baseCommission: 60.00,   // $1000 × 6% = $60
    //     fixedFeeApplied: 1.00,
    //     minimumApplied: false,
    //     maximumApplied: true,    // $61 > $50 maximum
    //     finalCommission: 50.00
    //   }
    // }
    ```
  </Accordion>
</AccordionGroup>

## Real-time Commission Calculator

Interactive commission calculator component:

<CodeGroup>
  ```typescript React Calculator theme={null}
  import { useState, useEffect } from 'react';
  import { useQuery } from 'convex/react';
  import { api } from './convex/_generated/api';

  function CommissionCalculator() {
    const [subtotal, setSubtotal] = useState<number>(0);
    const [selectedStoreId, setSelectedStoreId] = useState<string>('');
    const [selectedCategories, setSelectedCategories] = useState<string[]>([]);

    const stores = useQuery(api.customers.explore.getStoresWithPagination, {
      paginationOpts: { numItems: 50, cursor: null }
    });
    
    const categories = useQuery(api.shared.categories.getParents);
    
    const commission = useQuery(
      api.customers.checkoutHelpers.calculatePlatformCommission,
      subtotal > 0 ? {
        subtotal,
        storeId: selectedStoreId || undefined,
        categoryIds: selectedCategories.length > 0 ? selectedCategories : undefined
      } : "skip"
    );

    const handleSubtotalChange = (value: string) => {
      const numValue = parseFloat(value) || 0;
      setSubtotal(numValue);
    };

    return (
      <div className="max-w-2xl mx-auto bg-white p-6 rounded-lg shadow">
        <h2 className="text-xl font-bold mb-6">Commission Calculator</h2>
        
        {/* Subtotal Input */}
        <div className="mb-4">
          <label className="block text-sm font-medium text-gray-700 mb-2">
            Order Subtotal ($)
          </label>
          <input
            type="number"
            step="0.01"
            min="0"
            value={subtotal || ''}
            onChange={(e) => handleSubtotalChange(e.target.value)}
            placeholder="Enter order subtotal"
            className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
          />
        </div>

        {/* Store Selection */}
        <div className="mb-4">
          <label className="block text-sm font-medium text-gray-700 mb-2">
            Store (Optional)
          </label>
          <select
            value={selectedStoreId}
            onChange={(e) => setSelectedStoreId(e.target.value)}
            className="w-full p-3 border border-gray-300 rounded-lg"
          >
            <option value="">Select a store for store-specific rates</option>
            {stores?.page.map((store) => (
              <option key={store._id} value={store._id}>
                {store.name}
              </option>
            ))}
          </select>
        </div>

        {/* Category Selection */}
        <div className="mb-6">
          <label className="block text-sm font-medium text-gray-700 mb-2">
            Categories (Optional)
          </label>
          <div className="grid grid-cols-2 gap-2">
            {categories?.map((category) => (
              <label key={category._id} className="flex items-center space-x-2">
                <input
                  type="checkbox"
                  checked={selectedCategories.includes(category._id)}
                  onChange={(e) => {
                    if (e.target.checked) {
                      setSelectedCategories(prev => [...prev, category._id]);
                    } else {
                      setSelectedCategories(prev => prev.filter(id => id !== category._id));
                    }
                  }}
                  className="rounded"
                />
                <span className="text-sm">{category.name}</span>
              </label>
            ))}
          </div>
        </div>

        {/* Results */}
        {commission && subtotal > 0 && (
          <div className="bg-blue-50 p-4 rounded-lg">
            <h3 className="font-semibold text-blue-900 mb-3">Commission Breakdown</h3>
            
            <div className="space-y-2 text-sm">
              <div className="flex justify-between">
                <span>Order Subtotal:</span>
                <span className="font-medium">${subtotal.toFixed(2)}</span>
              </div>
              
              <div className="flex justify-between">
                <span>Commission Rate:</span>
                <span className="font-medium">{(commission.commissionRate * 100).toFixed(1)}%</span>
              </div>
              
              <div className="flex justify-between">
                <span>Base Commission:</span>
                <span>${commission.calculationDetails.baseCommission.toFixed(2)}</span>
              </div>
              
              <div className="flex justify-between">
                <span>Fixed Fee:</span>
                <span>${commission.calculationDetails.fixedFeeApplied.toFixed(2)}</span>
              </div>
              
              {commission.calculationDetails.minimumApplied && (
                <div className="flex justify-between text-orange-600">
                  <span>Minimum Applied:</span>
                  <span>Yes</span>
                </div>
              )}
              
              {commission.calculationDetails.maximumApplied && (
                <div className="flex justify-between text-orange-600">
                  <span>Maximum Applied:</span>
                  <span>Yes</span>
                </div>
              )}
              
              <div className="border-t pt-2 flex justify-between font-semibold text-blue-900">
                <span>Total Platform Commission:</span>
                <span>${commission.platformCommission.toFixed(2)}</span>
              </div>
              
              <div className="flex justify-between font-semibold text-green-700">
                <span>Store Earnings:</span>
                <span>${(subtotal - commission.platformCommission).toFixed(2)}</span>
              </div>
            </div>
          </div>
        )}
      </div>
    );
  }

  export default CommissionCalculator;
  ```
</CodeGroup>

## Integration with Payment Flow

Use checkout helpers in your payment processing:

<CodeGroup>
  ```typescript Complete Checkout Flow theme={null}
  async function processCheckout(checkoutData: CheckoutData) {
    try {
      // Step 1: Calculate commission
      const commission = await convex.query(api.customers.checkoutHelpers.calculatePlatformCommission, {
        subtotal: checkoutData.subtotal,
        storeId: checkoutData.storeId,
        categoryIds: checkoutData.categoryIds
      });

      // Step 2: Create payment intent with calculated commission
      const paymentSetup = await convex.action(api.customers.payments.createPaymentIntent, {
        customerId: checkoutData.customerId,
        storeId: checkoutData.storeId,
        subtotal: checkoutData.subtotal,
        deliveryFee: checkoutData.deliveryFee,
        platformFee: commission.platformCommission, // Use calculated commission
        totalAmount: checkoutData.subtotal + checkoutData.deliveryFee,
        cartItems: checkoutData.cartItems,
        deliveryType: checkoutData.deliveryType,
        deliveryAddress: checkoutData.deliveryAddress
      });

      // Step 3: Process payment
      const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
      const { error, paymentIntent } = await stripe!.confirmCardPayment(paymentSetup.clientSecret);

      if (error) {
        throw new Error(error.message);
      }

      // Step 4: Create order
      const order = await convex.mutation(api.customers.cartToOrder.createOrderFromCart, {
        customerId: checkoutData.customerId,
        storeId: checkoutData.storeId,
        deliveryAddress: checkoutData.deliveryAddress,
        deliveryType: checkoutData.deliveryType,
        deliveryFee: checkoutData.deliveryFee,
        paymentMethod: 'card',
        paymentIntentId: paymentIntent.id,
        paymentStatus: 'paid'
      });

      return order;

    } catch (error) {
      console.error('Checkout failed:', error);
      throw error;
    }
  }
  ```
</CodeGroup>

## Commission Transparency

For marketplace transparency, you can show commission information to store owners:

```typescript theme={null}
function StoreCommissionInfo({ storeId }: { storeId: string }) {
  const [testSubtotal, setTestSubtotal] = useState(100);
  
  const commission = useQuery(api.customers.checkoutHelpers.calculatePlatformCommission, {
    subtotal: testSubtotal,
    storeId
  });

  return (
    <div className="bg-white p-6 rounded-lg shadow">
      <h3 className="text-lg font-semibold mb-4">Your Commission Rates</h3>
      
      <div className="mb-4">
        <label className="block text-sm font-medium text-gray-700 mb-2">
          Test Order Amount ($)
        </label>
        <input
          type="number"
          value={testSubtotal}
          onChange={(e) => setTestSubtotal(parseFloat(e.target.value) || 0)}
          className="w-full p-2 border rounded"
        />
      </div>

      {commission && (
        <div className="space-y-3">
          <div className="flex justify-between">
            <span>Commission Rate:</span>
            <span className="font-medium">{(commission.commissionRate * 100).toFixed(1)}%</span>
          </div>
          
          <div className="flex justify-between">
            <span>Fixed Fee per Order:</span>
            <span className="font-medium">${commission.fixedFee.toFixed(2)}</span>
          </div>
          
          <div className="border-t pt-3 space-y-2">
            <div className="flex justify-between">
              <span>Order Subtotal:</span>
              <span>${testSubtotal.toFixed(2)}</span>
            </div>
            
            <div className="flex justify-between text-red-600">
              <span>Platform Commission:</span>
              <span>-${commission.platformCommission.toFixed(2)}</span>
            </div>
            
            <div className="flex justify-between font-semibold text-green-600">
              <span>You Receive:</span>
              <span>${(testSubtotal - commission.platformCommission).toFixed(2)}</span>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
```

## Error Handling

<AccordionGroup>
  <Accordion title="Invalid subtotal">
    **Status Code**: `400`

    ```json theme={null}
    {
      "error": "Invalid subtotal amount",
      "code": "INVALID_SUBTOTAL",
      "details": {
        "provided": -10,
        "minimum": 0
      }
    }
    ```
  </Accordion>

  <Accordion title="Store not found">
    **Status Code**: `404`

    ```json theme={null}
    {
      "error": "Store not found",
      "code": "STORE_NOT_FOUND",
      "storeId": "j123456789"
    }
    ```
  </Accordion>

  <Accordion title="Commission calculation failed">
    **Status Code**: `500`

    ```json theme={null}
    {
      "error": "Commission calculation failed",
      "code": "CALCULATION_ERROR",
      "details": "Platform configuration not found"
    }
    ```
  </Accordion>
</AccordionGroup>

<Note>
  Commission calculations are performed in real-time and reflect the current platform configuration. Rates may vary based on store performance and category.
</Note>

<Tip>
  Always use the calculated commission from this API rather than hardcoding rates, as they can change based on store agreements and platform policies.
</Tip>
