Skip to main content

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.
subtotal
number
required
Order subtotal before fees and commission
storeId
Id<'stores'>
Store ID for store-specific commission rates
categoryIds
Array<Id<'categories'>>
Category IDs for category-specific commission rates
const commission = await convex.query(api.customers.checkoutHelpers.calculatePlatformCommission, {
  subtotal: 45.97,
  storeId: "j123456789",
  categoryIds: ["k123456789"]
});
{
  "platformCommission": 3.20,
  "commissionRate": 0.05,
  "fixedFee": 1.00,
  "calculationDetails": {
    "baseCommission": 2.30,
    "fixedFeeApplied": 1.00,
    "minimumApplied": false,
    "maximumApplied": false,
    "finalCommission": 3.20
  }
}

Commission Calculation Logic

The commission system uses a sophisticated calculation method:
1

Base Commission

Calculate percentage-based commission: subtotal × commissionRate
2

Fixed Fee

Add per-order fixed fee to base commission
3

Minimum/Maximum

Apply minimum and maximum commission limits if configured
4

Category Adjustments

Apply category-specific rate adjustments if applicable
5

Store Adjustments

Apply store-specific rate adjustments for premium stores

Commission Breakdown

Understanding the commission structure:

Checkout Validation Hook

Complete checkout validation with commission calculation:
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 };

Commission Rate Structure

Default Rate

Standard: 5% of subtotal
Fixed Fee: 1.00perorderMinimum:1.00 per order **Minimum**: 0.50 per order

Category Rates

Food: 4% commission
Electronics: 6% commission
Fashion: 7% commission

Store Rates

New Stores: Standard rates
Premium Stores: Reduced rates
High Volume: Negotiated rates

Special Rates

Promotional: Temporary reduced rates
Seasonal: Holiday adjustments
Volume Discounts: Based on monthly sales

Commission Calculation Examples

Scenario: $50 food order, standard 4% rate
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
//   }
// }
Scenario: $5 order, minimum commission applies
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
//   }
// }
Scenario: $1000 electronics order, maximum commission applies
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
//   }
// }

Real-time Commission Calculator

Interactive commission calculator component:
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;

Integration with Payment Flow

Use checkout helpers in your payment processing:
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;
  }
}

Commission Transparency

For marketplace transparency, you can show commission information to store owners:
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

Status Code: 400
{
  "error": "Invalid subtotal amount",
  "code": "INVALID_SUBTOTAL",
  "details": {
    "provided": -10,
    "minimum": 0
  }
}
Status Code: 404
{
  "error": "Store not found",
  "code": "STORE_NOT_FOUND",
  "storeId": "j123456789"
}
Status Code: 500
{
  "error": "Commission calculation failed",
  "code": "CALCULATION_ERROR",
  "details": "Platform configuration not found"
}
Commission calculations are performed in real-time and reflect the current platform configuration. Rates may vary based on store performance and category.
Always use the calculated commission from this API rather than hardcoding rates, as they can change based on store agreements and platform policies.