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.Order subtotal before fees and commission
Store ID for store-specific commission rates
Category IDs for category-specific commission rates
Copy
const commission = await convex.query(api.customers.checkoutHelpers.calculatePlatformCommission, {
subtotal: 45.97,
storeId: "j123456789",
categoryIds: ["k123456789"]
});
Copy
{
"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 × commissionRate2
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:Copy
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.00perorder∗∗Minimum∗∗:0.50 per order
Fixed Fee: 1.00perorder∗∗Minimum∗∗:0.50 per order
Category Rates
Food: 4% commission
Electronics: 6% commission
Fashion: 7% commission
Electronics: 6% commission
Fashion: 7% commission
Store Rates
New Stores: Standard rates
Premium Stores: Reduced rates
High Volume: Negotiated rates
Premium Stores: Reduced rates
High Volume: Negotiated rates
Special Rates
Promotional: Temporary reduced rates
Seasonal: Holiday adjustments
Volume Discounts: Based on monthly sales
Seasonal: Holiday adjustments
Volume Discounts: Based on monthly sales
Commission Calculation Examples
Standard Food Order
Standard Food Order
Scenario: $50 food order, standard 4% rate
Copy
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
// }
// }
Small Order with Minimum
Small Order with Minimum
Scenario: $5 order, minimum commission applies
Copy
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
// }
// }
High-Value Order
High-Value Order
Scenario: $1000 electronics order, maximum commission applies
Copy
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:Copy
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:Copy
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:Copy
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
Invalid subtotal
Invalid subtotal
Status Code:
400Copy
{
"error": "Invalid subtotal amount",
"code": "INVALID_SUBTOTAL",
"details": {
"provided": -10,
"minimum": 0
}
}
Store not found
Store not found
Status Code:
404Copy
{
"error": "Store not found",
"code": "STORE_NOT_FOUND",
"storeId": "j123456789"
}
Commission calculation failed
Commission calculation failed
Status Code:
500Copy
{
"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.
