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).Id<'customers'>
required
Customer ID placing the order
Id<'stores'>
required
Store ID receiving the order
Array<object>
required
Array of items with productId and quantity
object
required
Complete delivery address information
'delivery' | 'pickup'
required
Delivery method selection
number
required
Delivery fee amount
'cash' | 'card' | 'wallet'
required
Payment method used
string
Optional order notes
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"
});
const order = await convex.mutation("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",
phoneNumber: "+971501234567"
},
deliveryType: "delivery",
deliveryFee: 5.00,
paymentMethod: "cash"
});
import requests
response = requests.post(
f"{CONVEX_URL}/api/mutation",
json={
"path": "customers.orders.createOrder",
"args": {
"customerId": "c123456789",
"storeId": "j123456789",
"orderItems": [
{"productId": "p123456789", "quantity": 2},
{"productId": "p987654321", "quantity": 1}
],
"deliveryAddress": {
"fullAddress": "123 Main St, Apartment 4B",
"city": "ct123456789",
"area": "ar123456789",
"phoneNumber": "+971501234567"
},
"deliveryType": "delivery",
"deliveryFee": 5.00,
"paymentMethod": "card"
}
},
headers={"Authorization": f"Bearer {token}"}
)
order = response.json()
{
"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).Id<'orders'>
required
Order ID to update
string
required
New order status:
pending, confirmed, preparing, ready, out_for_delivery, delivered, cancelledstring
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"
});
// Customer cancelling order
await convex.mutation(api.customers.orders.updateOrderStatus, {
orderId: "o123456789",
status: "cancelled",
notes: "Customer requested cancellation"
});
await convex.mutation("customers.orders.updateOrderStatus", {
orderId: "o123456789",
status: "preparing",
notes: "Started cooking your delicious meal!"
});
null
Get Order Details
Retrieve detailed information about a specific order.Id<'orders'>
required
Order ID to retrieve
const order = await convex.query(api.customers.orders.getOrder, {
orderId: "o123456789"
});
const order = await convex.query("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.Id<'customers'>
required
Customer ID
number
Maximum number of orders to return
const customerOrders = await convex.query(api.customers.orders.getCustomerOrders, {
customerId: "c123456789",
limit: 20
});
const customerOrders = await convex.query("customers.orders.getCustomerOrders", {
customerId: "c123456789",
limit: 10
});
[
{
"_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.Id<'stores'>
required
Store ID
string
Filter by order status
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
});
const storeOrders = await convex.query("customers.orders.getStoreOrders", {
storeId: "j123456789",
status: "confirmed"
});
[
{
"_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;
<template>
<div class="order-management">
<h2>Store Orders</h2>
<!-- Order Filters -->
<div class="filters mb-4">
<select v-model="statusFilter" @change="loadOrders" class="p-2 border rounded">
<option value="">All Orders</option>
<option value="pending">Pending</option>
<option value="confirmed">Confirmed</option>
<option value="preparing">Preparing</option>
<option value="ready">Ready</option>
<option value="out_for_delivery">Out for Delivery</option>
<option value="delivered">Delivered</option>
</select>
</div>
<!-- Orders List -->
<div class="orders-grid">
<div v-for="order in orders" :key="order._id" class="order-card">
<div class="order-header">
<h3>Order #{{ order._id.slice(-8) }}</h3>
<span :class="getStatusClass(order.status)">
{{ order.status.replace('_', ' ').toUpperCase() }}
</span>
</div>
<div class="order-details">
<p><strong>Customer:</strong> {{ order.customerName }}</p>
<p><strong>Items:</strong> {{ order.itemsCount }}</p>
<p><strong>Total:</strong> ${{ order.totalAmount.toFixed(2) }}</p>
<p><strong>Type:</strong> {{ order.deliveryType }}</p>
<p><strong>Time:</strong> {{ formatDate(order.orderDate) }}</p>
</div>
<!-- Action Buttons -->
<div class="order-actions" v-if="canUpdateStatus(order.status)">
<button
v-for="action in getAvailableActions(order.status)"
:key="action.status"
@click="updateOrderStatus(order._id, action.status, action.notes)"
:class="action.buttonClass"
>
{{ action.label }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
storeId: {
type: String,
required: true
}
},
data() {
return {
orders: [],
statusFilter: '',
loading: false
};
},
async mounted() {
await this.loadOrders();
},
methods: {
async loadOrders() {
this.loading = true;
try {
this.orders = await convex.query("customers.orders.getStoreOrders", {
storeId: this.storeId,
status: this.statusFilter || undefined,
limit: 50
});
} catch (error) {
console.error('Failed to load orders:', error);
} finally {
this.loading = false;
}
},
async updateOrderStatus(orderId, status, notes) {
try {
await convex.mutation("customers.orders.updateOrderStatus", {
orderId,
status,
notes
});
// Reload orders to reflect changes
await this.loadOrders();
} catch (error) {
console.error('Failed to update order status:', error);
}
},
getStatusClass(status) {
const classes = {
'pending': 'status-pending',
'confirmed': 'status-confirmed',
'preparing': 'status-preparing',
'ready': 'status-ready',
'out_for_delivery': 'status-delivery',
'delivered': 'status-delivered',
'cancelled': 'status-cancelled'
};
return classes[status] || 'status-default';
},
canUpdateStatus(status) {
return !['delivered', 'cancelled'].includes(status);
},
getAvailableActions(currentStatus) {
const actions = {
'pending': [
{ status: 'confirmed', label: 'Confirm', notes: 'Order confirmed', buttonClass: 'btn-confirm' },
{ status: 'cancelled', label: 'Cancel', notes: 'Order cancelled', buttonClass: 'btn-cancel' }
],
'confirmed': [
{ status: 'preparing', label: 'Start Preparing', notes: 'Started preparing order', buttonClass: 'btn-prepare' }
],
'preparing': [
{ status: 'ready', label: 'Mark Ready', notes: 'Order is ready', buttonClass: 'btn-ready' }
],
'ready': [
...(order.deliveryType === 'delivery' ? [
{ status: 'out_for_delivery', label: 'Send for Delivery', notes: 'Order dispatched', buttonClass: 'btn-delivery' }
] : []),
{
status: 'delivered',
label: order.deliveryType === 'delivery' ? 'Mark Delivered' : 'Mark Picked Up',
notes: 'Order completed',
buttonClass: 'btn-complete'
}
],
'out_for_delivery': [
{ status: 'delivered', label: 'Mark Delivered', notes: 'Order delivered successfully', buttonClass: 'btn-complete' }
]
};
return actions[currentStatus] || [];
},
formatDate(timestamp) {
return new Date(timestamp).toLocaleString();
}
}
};
</script>
Order Status Flow
Pending
Initial Status: Order placed, awaiting store confirmation
Actions: Store can confirm or cancel
Actions: Store can confirm or cancel
Confirmed
Store Action: Store has accepted the order
Next: Store will start preparing
Next: Store will start preparing
Preparing
Active Status: Order is being prepared
ETA: Estimated completion time provided
ETA: Estimated completion time provided
Ready
Completion: Order ready for pickup/delivery
Actions: Dispatch for delivery or notify for pickup
Actions: Dispatch for delivery or notify for pickup
Out for Delivery
Transit: Order is on the way to customer
Tracking: Real-time delivery updates
Tracking: Real-time delivery updates
Delivered
Final Status: Order completed successfully
Actions: Review and rating available
Actions: Review and rating available
Error Handling
Order not found
Order not found
Status Code:
404{
"error": "Order not found",
"orderId": "o123456789"
}
Invalid status transition
Invalid status transition
Status Code:
400{
"error": "Invalid status transition",
"currentStatus": "delivered",
"requestedStatus": "preparing",
"message": "Cannot change status of completed order"
}
Insufficient permissions
Insufficient permissions
Status Code:
403{
"error": "Insufficient permissions to update order",
"orderId": "o123456789",
"userRole": "customer"
}
Order creation failed
Order creation failed
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.
