Skip to main content

Overview

The Orders API provides comprehensive order management functionality including order creation, status updates, and retrieval for both customers and store owners. It supports real-time order tracking, automated notifications, and complete order lifecycle management. Location: convex/customers/orders.ts

Order Lifecycle

1

Pending

Order created, awaiting store confirmation
2

Confirmed

Store has confirmed the order and will start preparing
3

Preparing

Store is actively preparing the order
4

Ready

Order is ready for pickup or delivery
5

Out for Delivery

Order is on the way to customer (delivery only)
6

Delivered

Order has been completed successfully

Create Order

Creates a new order directly with specified items (alternative to cart-based ordering).
customerId
Id<'customers'>
required
Customer ID placing the order
storeId
Id<'stores'>
required
Store ID receiving the order
orderItems
Array<object>
required
Array of items with productId and quantity
deliveryAddress
object
required
Complete delivery address information
deliveryType
'delivery' | 'pickup'
required
Delivery method selection
deliveryFee
number
required
Delivery fee amount
paymentMethod
'cash' | 'card' | 'wallet'
required
Payment method used
notes
string
Optional order notes
estimatedDeliveryTime
number
Estimated delivery time in minutes
const order = await convex.mutation(api.customers.orders.createOrder, {
  customerId: "c123456789",
  storeId: "j123456789",
  orderItems: [
    { productId: "p123456789", quantity: 2 },
    { productId: "p987654321", quantity: 1 }
  ],
  deliveryAddress: {
    fullAddress: "123 Main St, Apartment 4B",
    city: "ct123456789",
    area: "ar123456789",
    flatVilaNumber: "4B",
    buildingNameNumber: "123",
    landmark: "Near Central Park",
    phoneNumber: "+971501234567"
  },
  deliveryType: "delivery",
  deliveryFee: 5.00,
  paymentMethod: "card",
  notes: "Please ring doorbell twice"
});
{
  "orderId": "o123456789",
  "orderSummary": {
    "subtotal": 31.98,
    "deliveryFee": 5.00,
    "platformCommission": 3.20,
    "storeEarnings": 33.78,
    "totalAmount": 36.98,
    "itemsCount": 3
  }
}

Update Order Status

Update order status with optional notes (available to both store owners and customers).
orderId
Id<'orders'>
required
Order ID to update
status
string
required
New order status: pending, confirmed, preparing, ready, out_for_delivery, delivered, cancelled
notes
string
Optional status update notes
// Store owner updating order status
await convex.mutation(api.customers.orders.updateOrderStatus, {
  orderId: "o123456789",
  status: "confirmed",
  notes: "Order confirmed. We'll start preparing it shortly."
});

await convex.mutation(api.customers.orders.updateOrderStatus, {
  orderId: "o123456789",
  status: "preparing",
  notes: "Chef started preparing your order"
});

await convex.mutation(api.customers.orders.updateOrderStatus, {
  orderId: "o123456789",
  status: "ready",
  notes: "Your order is ready for pickup!"
});

await convex.mutation(api.customers.orders.updateOrderStatus, {
  orderId: "o123456789",
  status: "out_for_delivery",
  notes: "Order is on the way. ETA: 25 minutes"
});

await convex.mutation(api.customers.orders.updateOrderStatus, {
  orderId: "o123456789",
  status: "delivered",
  notes: "Order delivered successfully"
});
null

Get Order Details

Retrieve detailed information about a specific order.
orderId
Id<'orders'>
required
Order ID to retrieve
const order = await convex.query(api.customers.orders.getOrder, {
  orderId: "o123456789"
});
{
  "_id": "o123456789",
  "customerId": "c123456789",
  "storeId": "j123456789",
  "orderItems": [
    {
      "productId": "p123456789",
      "productName": "Pizza Margherita",
      "quantity": 2,
      "price": 15.99,
      "subtotal": 31.98,
      "productImage": "https://storage.convex.dev/pizza.jpg"
    },
    {
      "productId": "p987654321",
      "productName": "Garlic Bread",
      "quantity": 1,
      "price": 6.99,
      "subtotal": 6.99,
      "productImage": "https://storage.convex.dev/bread.jpg"
    }
  ],
  "deliveryAddress": {
    "fullAddress": "123 Main St, Apartment 4B",
    "city": "ct123456789",
    "cityName": "Dubai",
    "area": "ar123456789",
    "areaName": "Marina",
    "phoneNumber": "+971501234567",
    "flatVilaNumber": "4B",
    "buildingNameNumber": "123",
    "landmark": "Near Central Park"
  },
  "deliveryType": "delivery",
  "deliveryFee": 5.00,
  "paymentMethod": "card",
  "paymentStatus": "paid",
  "paymentIntentId": "pi_123456789",
  "status": "preparing",
  "notes": "Please ring doorbell twice",
  "statusHistory": [
    {
      "status": "pending",
      "timestamp": 1640995200000,
      "notes": "Order placed"
    },
    {
      "status": "confirmed",
      "timestamp": 1640995500000,
      "notes": "Order confirmed by store"
    },
    {
      "status": "preparing",
      "timestamp": 1640995800000,
      "notes": "Chef started preparing your order"
    }
  ],
  "orderSummary": {
    "subtotal": 38.97,
    "deliveryFee": 5.00,
    "platformCommission": 3.20,
    "storeEarnings": 40.77,
    "totalAmount": 43.97,
    "itemsCount": 3
  },
  "store": {
    "name": "Mario's Pizza Palace",
    "logoUrl": "https://storage.convex.dev/logo.jpg",
    "phoneNumber": "+971501111111"
  },
  "customer": {
    "name": "John Doe",
    "phoneNumber": "+971501234567"
  },
  "estimatedDeliveryTime": 30,
  "_creationTime": 1640995200000,
  "lastUpdated": 1640995800000
}

Get Customer Orders

Retrieve all orders for a specific customer.
customerId
Id<'customers'>
required
Customer ID
limit
number
Maximum number of orders to return
const customerOrders = await convex.query(api.customers.orders.getCustomerOrders, {
  customerId: "c123456789",
  limit: 20
});
[
  {
    "_id": "o123456789",
    "storeId": "j123456789",
    "storeName": "Mario's Pizza Palace",
    "storeLogoUrl": "https://storage.convex.dev/logo.jpg",
    "status": "delivered",
    "totalAmount": 43.97,
    "itemsCount": 3,
    "deliveryType": "delivery",
    "orderDate": 1640995200000,
    "deliveredAt": 1640997000000
  },
  {
    "_id": "o987654321",
    "storeId": "j987654321",
    "storeName": "Tech Paradise",
    "storeLogoUrl": "https://storage.convex.dev/tech-logo.jpg",
    "status": "preparing",
    "totalAmount": 299.99,
    "itemsCount": 1,
    "deliveryType": "pickup",
    "orderDate": 1640994000000
  }
]

Get Store Orders

Retrieve orders for a specific store with optional status filtering.
storeId
Id<'stores'>
required
Store ID
status
string
Filter by order status
limit
number
Maximum number of orders to return
// Get all orders for store
const allOrders = await convex.query(api.customers.orders.getStoreOrders, {
  storeId: "j123456789",
  limit: 50
});

// Get only pending orders
const pendingOrders = await convex.query(api.customers.orders.getStoreOrders, {
  storeId: "j123456789",
  status: "pending",
  limit: 20
});
[
  {
    "_id": "o123456789",
    "customerId": "c123456789",
    "customerName": "John Doe",
    "customerPhone": "+971501234567",
    "status": "confirmed",
    "totalAmount": 43.97,
    "itemsCount": 3,
    "deliveryType": "delivery",
    "deliveryAddress": {
      "fullAddress": "123 Main St, Apt 4B",
      "areaName": "Marina",
      "cityName": "Dubai"
    },
    "paymentMethod": "card",
    "paymentStatus": "paid",
    "orderDate": 1640995200000,
    "estimatedDeliveryTime": 30
  }
]

Order Management Components

Complete order management interface:
import { useQuery, useMutation } from 'convex/react';
import { api } from './convex/_generated/api';

interface OrderTrackerProps {
  orderId: string;
  userType: 'customer' | 'store';
}

function OrderTracker({ orderId, userType }: OrderTrackerProps) {
  const order = useQuery(api.customers.orders.getOrder, { orderId });
  const updateStatus = useMutation(api.customers.orders.updateOrderStatus);

  if (!order) {
    return <div>Loading order...</div>;
  }

  const statusSteps = [
    { key: 'pending', label: 'Order Placed', icon: '📝' },
    { key: 'confirmed', label: 'Confirmed', icon: '✅' },
    { key: 'preparing', label: 'Preparing', icon: '👨‍🍳' },
    { key: 'ready', label: 'Ready', icon: '🎉' },
    ...(order.deliveryType === 'delivery' ? [
      { key: 'out_for_delivery', label: 'Out for Delivery', icon: '🚚' }
    ] : []),
    { key: 'delivered', label: order.deliveryType === 'delivery' ? 'Delivered' : 'Picked Up', icon: '✨' }
  ];

  const currentStepIndex = statusSteps.findIndex(step => step.key === order.status);

  const handleStatusUpdate = async (newStatus: string, notes?: string) => {
    try {
      await updateStatus({
        orderId,
        status: newStatus,
        notes
      });
    } catch (error) {
      console.error('Failed to update status:', error);
    }
  };

  return (
    <div className="bg-white rounded-lg shadow p-6">
      {/* Order Header */}
      <div className="flex justify-between items-start mb-6">
        <div>
          <h2 className="text-xl font-bold text-gray-900">
            Order #{orderId.slice(-8)}
          </h2>
          <p className="text-gray-600">
            {new Date(order._creationTime).toLocaleDateString()} • 
            {order.itemsCount} items${order.orderSummary.totalAmount.toFixed(2)}
          </p>
        </div>
        <span className={`px-3 py-1 rounded-full text-sm font-medium ${
          order.status === 'delivered' ? 'bg-green-100 text-green-800' :
          order.status === 'cancelled' ? 'bg-red-100 text-red-800' :
          order.status === 'preparing' ? 'bg-yellow-100 text-yellow-800' :
          'bg-blue-100 text-blue-800'
        }`}>
          {order.status.replace('_', ' ').toUpperCase()}
        </span>
      </div>

      {/* Progress Timeline */}
      <div className="mb-6">
        <div className="flex items-center">
          {statusSteps.map((step, index) => (
            <React.Fragment key={step.key}>
              <div className="flex flex-col items-center">
                <div className={`w-10 h-10 rounded-full flex items-center justify-center text-lg ${
                  index <= currentStepIndex ? 'bg-green-500 text-white' :
                  'bg-gray-200 text-gray-500'
                }`}>
                  {step.icon}
                </div>
                <span className="text-xs mt-1 text-center max-w-20">
                  {step.label}
                </span>
              </div>
              {index < statusSteps.length - 1 && (
                <div className={`flex-1 h-1 mx-2 ${
                  index < currentStepIndex ? 'bg-green-500' : 'bg-gray-200'
                }`}></div>
              )}
            </React.Fragment>
          ))}
        </div>
      </div>

      {/* Order Items */}
      <div className="mb-6">
        <h3 className="font-semibold text-gray-900 mb-3">Order Items</h3>
        <div className="space-y-3">
          {order.orderItems.map((item) => (
            <div key={item.productId} className="flex items-center space-x-4 p-3 bg-gray-50 rounded">
              {item.productImage && (
                <img 
                  src={item.productImage} 
                  alt={item.productName}
                  className="w-12 h-12 rounded object-cover"
                />
              )}
              <div className="flex-1">
                <p className="font-medium">{item.productName}</p>
                <p className="text-sm text-gray-600">
                  Quantity: {item.quantity} × ${item.price.toFixed(2)}
                </p>
              </div>
              <p className="font-medium">${item.subtotal.toFixed(2)}</p>
            </div>
          ))}
        </div>
      </div>

      {/* Store Owner Actions */}
      {userType === 'store' && order.status !== 'delivered' && order.status !== 'cancelled' && (
        <div className="border-t pt-4">
          <h3 className="font-semibold text-gray-900 mb-3">Update Order Status</h3>
          <div className="flex flex-wrap gap-2">
            {order.status === 'pending' && (
              <>
                <button
                  onClick={() => handleStatusUpdate('confirmed', 'Order confirmed and will be prepared')}
                  className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
                >
                  Confirm Order
                </button>
                <button
                  onClick={() => handleStatusUpdate('cancelled', 'Unable to fulfill order')}
                  className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
                >
                  Cancel Order
                </button>
              </>
            )}
            
            {order.status === 'confirmed' && (
              <button
                onClick={() => handleStatusUpdate('preparing', 'Started preparing order')}
                className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
              >
                Start Preparing
              </button>
            )}
            
            {order.status === 'preparing' && (
              <button
                onClick={() => handleStatusUpdate('ready', 'Order is ready!')}
                className="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700"
              >
                Mark as Ready
              </button>
            )}
            
            {order.status === 'ready' && order.deliveryType === 'delivery' && (
              <button
                onClick={() => handleStatusUpdate('out_for_delivery', 'Order dispatched for delivery')}
                className="px-4 py-2 bg-orange-600 text-white rounded hover:bg-orange-700"
              >
                Out for Delivery
              </button>
            )}
            
            {(order.status === 'ready' && order.deliveryType === 'pickup') || 
             order.status === 'out_for_delivery' && (
              <button
                onClick={() => handleStatusUpdate('delivered', 'Order completed successfully')}
                className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
              >
                Mark as {order.deliveryType === 'delivery' ? 'Delivered' : 'Picked Up'}
              </button>
            )}
          </div>
        </div>
      )}

      {/* Status History */}
      {order.statusHistory && order.statusHistory.length > 0 && (
        <div className="border-t pt-4 mt-4">
          <h3 className="font-semibold text-gray-900 mb-3">Order Timeline</h3>
          <div className="space-y-2">
            {order.statusHistory.map((history, index) => (
              <div key={index} className="flex justify-between items-center p-2 bg-gray-50 rounded">
                <div>
                  <span className="font-medium capitalize">
                    {history.status.replace('_', ' ')}
                  </span>
                  {history.notes && (
                    <p className="text-sm text-gray-600">{history.notes}</p>
                  )}
                </div>
                <span className="text-sm text-gray-500">
                  {new Date(history.timestamp).toLocaleString()}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

export default OrderTracker;

Order Status Flow

Pending

Initial Status: Order placed, awaiting store confirmation
Actions: Store can confirm or cancel

Confirmed

Store Action: Store has accepted the order
Next: Store will start preparing

Preparing

Active Status: Order is being prepared
ETA: Estimated completion time provided

Ready

Completion: Order ready for pickup/delivery
Actions: Dispatch for delivery or notify for pickup

Out for Delivery

Transit: Order is on the way to customer
Tracking: Real-time delivery updates

Delivered

Final Status: Order completed successfully
Actions: Review and rating available

Error Handling

Status Code: 404
{
  "error": "Order not found",
  "orderId": "o123456789"
}
Status Code: 400
{
  "error": "Invalid status transition",
  "currentStatus": "delivered",
  "requestedStatus": "preparing",
  "message": "Cannot change status of completed order"
}
Status Code: 403
{
  "error": "Insufficient permissions to update order",
  "orderId": "o123456789",
  "userRole": "customer"
}
Status Code: 400
{
  "error": "Order creation failed",
  "details": {
    "inventory": "Insufficient stock for Pizza Margherita",
    "store": "Store is currently not accepting orders"
  }
}
Order status updates automatically trigger notifications to relevant parties and update real-time dashboards for both customers and store owners.
Use the order status history to provide customers with detailed tracking information and resolve any delivery disputes.