Skip to main content

Overview

The Cart to Order API handles the critical conversion from shopping cart to confirmed orders. This system manages payment processing, inventory validation, cart cleanup, and order creation with comprehensive error handling and real-time updates. Location: convex/customers/cartToOrder.ts

Checkout Process

1

Validate Cart

Check item availability, stock levels, and price changes
2

Process Payment

Handle payment via Stripe, cash, or digital wallet
3

Create Order

Convert cart items to confirmed order with delivery details
4

Clear Cart

Automatically remove items from cart after successful order
5

Update Inventory

Reduce product stock levels and update analytics

Create Order from Cart

Creates an order from the items in a customer’s cart for a specific store.
customerId
Id<'customers'>
required
Customer ID placing the order
storeId
Id<'stores'>
required
Store ID receiving the order
deliveryAddress
DeliveryAddress
required
Complete delivery address information
deliveryType
'delivery' | 'pickup'
required
Delivery method selection
deliveryFee
number
required
Delivery fee amount (0 for pickup)
paymentMethod
'cash' | 'card' | 'wallet'
required
Payment method used
paymentIntentId
string
Stripe PaymentIntent ID (required for card payments)
paymentStatus
'pending' | 'paid' | 'failed'
Payment status (default: “pending”)
notes
string
Optional order notes or special instructions
estimatedDeliveryTime
number
Estimated delivery time in minutes
// Basic order creation with cash payment
const order = await convex.mutation(api.customers.cartToOrder.createOrderFromCart, {
  customerId: "c123456789",
  storeId: "j123456789",
  deliveryAddress: {
    fullAddress: "123 Main St, Apt 4B",
    city: "ct123456789",
    area: "ar123456789",
    phoneNumber: "+971501234567",
    flatVilaNumber: "4B",
    buildingNameNumber: "123",
    landmark: "Near Central Park"
  },
  deliveryType: "delivery",
  deliveryFee: 5.00,
  paymentMethod: "cash",
  notes: "Please ring the doorbell twice"
});
{
  "orderId": "o123456789",
  "orderSummary": {
    "subtotal": 45.97,
    "deliveryFee": 5.00,
    "platformCommission": 3.20,
    "storeEarnings": 47.77,
    "totalAmount": 50.97,
    "itemsCount": 3
  }
}

Validate Cart for Checkout

Validates cart items before checkout, checking availability, stock, and price changes.
customerId
Id<'customers'>
required
Customer ID
storeId
Id<'stores'>
required
Store ID
const validation = await convex.mutation(api.customers.cartToOrder.validateCartForCheckout, {
  customerId: "c123456789",
  storeId: "j123456789"
});

if (!validation.isValid) {
  console.log("Validation issues:", validation.issues);
  // Handle issues before proceeding to checkout
}
{
  "isValid": true,
  "issues": [],
  "updatedSubtotal": 47.50
}

Complete Checkout Flow

Here’s a comprehensive checkout implementation:
import { useState, useEffect } from 'react';
import { useQuery, useMutation, useAction } from 'convex/react';
import { api } from './convex/_generated/api';
import { loadStripe } from '@stripe/stripe-js';

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

interface CheckoutFlowProps {
  customerId: string;
  storeId: string;
  onSuccess: (orderId: string) => void;
  onError: (error: string) => void;
}

function CheckoutFlow({ customerId, storeId, onSuccess, onError }: CheckoutFlowProps) {
  const [step, setStep] = useState<'cart' | 'address' | 'payment' | 'processing'>('cart');
  const [deliveryType, setDeliveryType] = useState<'delivery' | 'pickup'>('delivery');
  const [deliveryAddress, setDeliveryAddress] = useState<any>(null);
  const [paymentMethod, setPaymentMethod] = useState<'cash' | 'card'>('card');
  const [validationIssues, setValidationIssues] = useState<any[]>([]);

  // API functions
  const cart = useQuery(api.customers.cart.getCart, { customerId, storeId });
  const validateCart = useMutation(api.customers.cartToOrder.validateCartForCheckout);
  const createPaymentIntent = useAction(api.customers.payments.createPaymentIntent);
  const createOrder = useMutation(api.customers.cartToOrder.createOrderFromCart);
  const calculateCommission = useQuery(api.customers.checkoutHelpers.calculatePlatformCommission, 
    cart ? { 
      subtotal: cart.subtotal, 
      storeId,
      categoryIds: cart.items.map(item => item.categoryId).filter(Boolean)
    } : "skip"
  );

  // Step 1: Validate cart
  const handleCartValidation = async () => {
    try {
      const validation = await validateCart({ customerId, storeId });
      
      if (!validation.isValid) {
        setValidationIssues(validation.issues);
        return false;
      }
      
      setValidationIssues([]);
      return true;
    } catch (error) {
      onError('Cart validation failed');
      return false;
    }
  };

  // Step 2: Process checkout
  const handleCheckout = async () => {
    if (!cart || !calculateCommission) return;

    setStep('processing');

    try {
      const deliveryFee = deliveryType === 'delivery' ? 5.00 : 0;
      const platformFee = calculateCommission.platformCommission;
      const totalAmount = cart.subtotal + deliveryFee;

      if (paymentMethod === 'card') {
        // Stripe payment flow
        const paymentSetup = await createPaymentIntent({
          customerId,
          storeId,
          subtotal: cart.subtotal,
          deliveryFee,
          platformFee,
          totalAmount,
          cartItems: cart.items.map(item => ({
            productId: item.productId,
            productName: item.productName,
            quantity: item.quantity,
            price: item.productPrice
          })),
          deliveryType,
          deliveryAddress
        });

        // Process payment with Stripe
        const stripe = await stripePromise;
        if (!stripe) throw new Error('Stripe not loaded');

        const { error, paymentIntent } = await stripe.confirmCardPayment(
          paymentSetup.clientSecret,
          {
            payment_method: {
              card: {
                // Card element would be here
              }
            }
          }
        );

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

        // Create order with successful payment
        const order = await createOrder({
          customerId,
          storeId,
          deliveryAddress,
          deliveryType,
          deliveryFee,
          paymentMethod: 'card',
          paymentIntentId: paymentIntent.id,
          paymentStatus: 'paid'
        });

        onSuccess(order.orderId);

      } else {
        // Cash payment flow
        const order = await createOrder({
          customerId,
          storeId,
          deliveryAddress,
          deliveryType,
          deliveryFee,
          paymentMethod: 'cash',
          paymentStatus: 'pending'
        });

        onSuccess(order.orderId);
      }

    } catch (error) {
      console.error('Checkout failed:', error);
      onError(error.message || 'Checkout failed');
      setStep('payment');
    }
  };

  // Render current step
  const renderStep = () => {
    switch (step) {
      case 'cart':
        return (
          <CartReview 
            cart={cart}
            validationIssues={validationIssues}
            onValidate={handleCartValidation}
            onNext={() => setStep('address')}
          />
        );
      
      case 'address':
        return (
          <AddressForm
            deliveryType={deliveryType}
            onDeliveryTypeChange={setDeliveryType}
            onAddressSubmit={(address) => {
              setDeliveryAddress(address);
              setStep('payment');
            }}
          />
        );
      
      case 'payment':
        return (
          <PaymentMethodSelection
            paymentMethod={paymentMethod}
            onPaymentMethodChange={setPaymentMethod}
            totalAmount={cart ? cart.subtotal + (deliveryType === 'delivery' ? 5 : 0) : 0}
            onConfirmOrder={handleCheckout}
          />
        );
      
      case 'processing':
        return (
          <div className="text-center p-8">
            <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
            <p className="text-lg font-medium">Processing your order...</p>
            <p className="text-gray-600">Please don't close this page</p>
          </div>
        );
      
      default:
        return null;
    }
  };

  return (
    <div className="max-w-2xl mx-auto">
      {/* Progress Indicator */}
      <div className="mb-8">
        <div className="flex items-center">
          {['cart', 'address', 'payment'].map((stepName, index) => (
            <React.Fragment key={stepName}>
              <div className={`flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium ${
                step === stepName ? 'bg-blue-600 text-white' :
                ['cart', 'address', 'payment'].indexOf(step) > index ? 'bg-green-600 text-white' :
                'bg-gray-300 text-gray-600'
              }`}>
                {index + 1}
              </div>
              {index < 2 && (
                <div className={`flex-1 h-1 mx-2 ${
                  ['cart', 'address', 'payment'].indexOf(step) > index ? 'bg-green-600' : 'bg-gray-300'
                }`}></div>
              )}
            </React.Fragment>
          ))}
        </div>
        <div className="flex justify-between mt-2 text-sm text-gray-600">
          <span>Review Cart</span>
          <span>Delivery</span>
          <span>Payment</span>
        </div>
      </div>

      {/* Current Step Content */}
      {renderStep()}
    </div>
  );
}

export default CheckoutFlow;

Delivery Address Structure

Complete delivery address format:
interface DeliveryAddress {
  fullAddress: string;           // Complete address string
  city: Id<"cities">;           // City ID from locations API
  area: Id<"areas">;            // Area ID from locations API
  phoneNumber: string;          // Contact phone number
  flatVilaNumber?: string;      // Apartment/villa number
  buildingNameNumber?: string;  // Building name or number
  landmark?: string;            // Nearby landmark
  latitude?: string;            // GPS coordinates
  longitude?: string;           // GPS coordinates
}

Payment Integration Notes

Stripe Payments

Method: "card"
Required: paymentIntentId
Status: "paid" for successful payments

Cash Payments

Method: "cash"
Required: None
Status: "pending" until delivery

Wallet Payments

Method: "wallet"
Required: Sufficient wallet balance
Status: "paid" immediately

Automatic Processing

When an order is created, the system automatically:
Automatic: Cart is cleared after successful order creation
// After order creation:
// 1. All cart items are removed
// 2. Cart subtotal reset to 0
// 3. Cart timestamp updated
Automatic: Product inventory is updated
// For each cart item:
// 1. Product stock reduced by quantity ordered
// 2. Stock alerts triggered if below threshold
// 3. Product availability updated if out of stock
Automatic: Store wallet is credited with earnings
// Financial processing:
// 1. Platform commission deducted
// 2. Store earnings added to wallet
// 3. Transaction history recorded
// 4. Payout eligibility updated
Automatic: Relevant parties are notified
// Notifications sent to:
// 1. Customer: Order confirmation
// 2. Store: New order notification
// 3. Admin: High-value order alerts (if applicable)

Error Handling

Status Code: 400
{
  "error": "Cannot create order from empty cart",
  "cartItemCount": 0
}
Status Code: 400
{
  "error": "Store is not accepting orders",
  "storeStatus": "inactive",
  "storeId": "j123456789"
}
Status Code: 400
{
  "error": "Insufficient stock for one or more items",
  "details": [
    {
      "productId": "p123456789",
      "requested": 5,
      "available": 3
    }
  ]
}
Status Code: 402
{
  "error": "Payment processing failed",
  "paymentIntentId": "pi_123456789",
  "paymentError": "Your card was declined"
}
Status Code: 400
{
  "error": "Invalid delivery address",
  "details": {
    "city": "City ID is required",
    "area": "Area ID is required",
    "phoneNumber": "Valid phone number is required"
  }
}

Best Practices

Always Validate

Always validate the cart before creating an order to handle stock changes and price updates

Handle Failures

Implement comprehensive error handling for payment failures and validation issues

User Feedback

Provide clear feedback during each step of the checkout process

Security

Never store sensitive payment information - let Stripe handle all card data

Order Creation Flow

The cart-to-order conversion is atomic - either the entire process succeeds or fails completely, ensuring data consistency.
Use the validation function before showing the payment screen to catch issues early and provide a better user experience.