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).Customer ID placing the order
Store ID receiving the order
Array of items with productId and quantity
Complete delivery address information
Delivery method selection
Delivery fee amount
Payment method used
Optional order notes
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).Order ID to update
New order status:
pending, confirmed, preparing, ready, out_for_delivery, delivered, cancelledOptional 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.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.Customer ID
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.Store ID
Filter by order status
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.
