Appearance
Order State Machine, Geo-Shipping & Business Rules Engine
1. Order Status Lifecycle & Transition Matrix
The Order entity enforces a strict, deterministic state machine (OrderStatus). State transitions are gated by user role permissions, system triggers, and automated payment webhooks.
mermaid
stateDiagram-v2
[*] --> PENDING_PAYMENT : Customer Creates Order
PENDING_PAYMENT --> PAYMENT_UPLOADED : Customer Uploads Receipt
PENDING_PAYMENT --> PAYMENT_VERIFIED : QBiz QRIS Webhook Callback
PENDING_PAYMENT --> CANCELLED : Timeout / User Cancels
PAYMENT_UPLOADED --> PAYMENT_VERIFYING : Admin Begins Verification
PAYMENT_UPLOADED --> CANCELLED : Admin Rejects Receipt
PAYMENT_VERIFYING --> PAYMENT_VERIFIED : Admin Approves Receipt
PAYMENT_VERIFYING --> PENDING_PAYMENT : Admin Requests Re-upload
PAYMENT_VERIFIED --> PROCESSING : Kitchen Accepts Order (KDS)
PROCESSING --> READY_FOR_DELIVERY : Food Preparation Complete (Delivery Mode)
PROCESSING --> READY_FOR_PICKUP : Food Preparation Complete (Pickup Mode)
READY_FOR_DELIVERY --> SHIPPING : Courier Picked Up Order
SHIPPING --> DELIVERED : Delivery Confirmed
READY_FOR_PICKUP --> DELIVERED : Customer Picked Up Order
DELIVERED --> [*]
CANCELLED --> [*]Complete State Transition Matrix Table
| Initial State | Target State | Triggering Actor | Authorization Required | Automated System Actions & Side-Effects |
|---|---|---|---|---|
[*] (New) | PENDING_PAYMENT | Customer | Any Authenticated User | Generates unique orderNumber (SML-YYYYMMDD-XXX), creates OrderItem snapshots, calls QBiz API for dynamic QRIS URL. |
PENDING_PAYMENT | PAYMENT_UPLOADED | Customer | Order Owner | Saves proof image to R2/Local storage, creates PaymentVerification record (status: PENDING), notifies Admin via WA/Push. |
PENDING_PAYMENT | PAYMENT_VERIFIED | System (Webhook) | QBiz Webhook Signature | Verifies HMAC signature, updates paidAmount, creates FinancialEntry (type: INCOME), sends WhatsApp receipt to Customer. |
PAYMENT_UPLOADED | PAYMENT_VERIFIED | Admin | ADMIN, OWNER, STAFF | Logs admin verifier ID, creates FinancialEntry income record, dispatches WA confirmation message to Customer. |
PAYMENT_VERIFIED | PROCESSING | Kitchen Staff | STAFF, ADMIN | Transmits order ticket to Kitchen Display System (KDS) & ESC/POS printer, updates estimated preparation completion time. |
PROCESSING | READY_FOR_DELIVERY | Kitchen Staff | STAFF, ADMIN | Triggers Web Push alert & WhatsApp notification: "Your satay order is freshly cooked and ready for delivery!" |
PROCESSING | READY_FOR_PICKUP | Kitchen Staff | STAFF, ADMIN | Sends WhatsApp pick-up alert with store address and pickup code to Customer. |
READY_FOR_DELIVERY | SHIPPING | Driver / Staff | STAFF, ADMIN | Attaches driver details, updates status, dispatches live delivery tracking notification. |
SHIPPING / PICKUP | DELIVERED | Customer / Driver | Any Authorized Actor | Marks order completed, records deliveredAt timestamp, dispatches automated review request message after 30 minutes. |
ANY_PRE_DELIVERY | CANCELLED | Customer / Admin | Order Owner or Admin | Releases pre-order holds, logs cancellation reason in AdminLog, issues refund entry if previously verified. |
2. Geo-Shipping Fee Calculation Engine
Shipping cost calculations are executed dynamically in src/backend/utils/distance.ts using the Haversine Great-Circle Distance Formula:
$$ ext{dLat} = ( ext{lat}_2 - ext{lat}_1) imes rac{\pi}{180}, \quad ext{dLon} = ( ext{lon}_2 - ext{lon}_1) imes rac{\pi}{180}$$
$$a = \sin^2\left( rac{ ext{dLat}}{2} ight) + \cos( ext{lat}_1 \cdot rac{\pi}{180}) \cos( ext{lat}_2 \cdot rac{\pi}{180}) \sin^2\left( rac{ ext{dLon}}{2} ight)$$
$$c = 2 imes ext{atan2}\left(\sqrt{a}, \sqrt{1-a} ight), \quad D = R imes c imes ext{margin}$$
Where $R = 6371 ext{ km}$ (Earth's radius) and $ ext{margin} = 1.1$ (10% road distance factor multiplier).
Code Implementation (src/backend/utils/distance.ts)
typescript
export const calculateHaversineDistance = (
lat1: number, lon1: number,
lat2: number, lon2: number,
margin: number = 1.1
): number => {
if (!lat1 || !lon1 || !lat2 || !lon2) return 0;
const R = 6371; // Earth radius in km
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLon = (lon2 - lon1) * (Math.PI / 180);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * (Math.PI / 180)) *
Math.cos(lat2 * (Math.PI / 180)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return (R * c) * margin;
};
export const calculateShippingFee = (
distance: number,
feePerKm: number = 2000
): number => {
const total = distance * feePerKm;
// Minimum shipping fee Rp 5.000, rounded up to nearest Rp 1.000
return Math.max(5000, Math.ceil(total / 1000) * 1000);
};3. Catering Pre-Order Rules Engine
To protect kitchen logistics, catering and bulk order placements enforce volume-based lead time thresholds configured in Setting:
| Order Volume (Boxes / Portion) | Minimum Lead Time | System Rule Enforced |
|---|---|---|
| Small (< 15 portions) | H-1 (24 Hours Prior) | Earliest selectable delivery date is today + 1 day. |
| Medium (15 - 20 portions) | H-3 (72 Hours Prior) | Earliest selectable delivery date is today + 3 days. |
| Large (> 20 portions) | H-7 (7 Days Prior) | Earliest selectable delivery date is today + 7 days. |