Skip to content

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 StateTarget StateTriggering ActorAuthorization RequiredAutomated System Actions & Side-Effects
[*] (New)PENDING_PAYMENTCustomerAny Authenticated UserGenerates unique orderNumber (SML-YYYYMMDD-XXX), creates OrderItem snapshots, calls QBiz API for dynamic QRIS URL.
PENDING_PAYMENTPAYMENT_UPLOADEDCustomerOrder OwnerSaves proof image to R2/Local storage, creates PaymentVerification record (status: PENDING), notifies Admin via WA/Push.
PENDING_PAYMENTPAYMENT_VERIFIEDSystem (Webhook)QBiz Webhook SignatureVerifies HMAC signature, updates paidAmount, creates FinancialEntry (type: INCOME), sends WhatsApp receipt to Customer.
PAYMENT_UPLOADEDPAYMENT_VERIFIEDAdminADMIN, OWNER, STAFFLogs admin verifier ID, creates FinancialEntry income record, dispatches WA confirmation message to Customer.
PAYMENT_VERIFIEDPROCESSINGKitchen StaffSTAFF, ADMINTransmits order ticket to Kitchen Display System (KDS) & ESC/POS printer, updates estimated preparation completion time.
PROCESSINGREADY_FOR_DELIVERYKitchen StaffSTAFF, ADMINTriggers Web Push alert & WhatsApp notification: "Your satay order is freshly cooked and ready for delivery!"
PROCESSINGREADY_FOR_PICKUPKitchen StaffSTAFF, ADMINSends WhatsApp pick-up alert with store address and pickup code to Customer.
READY_FOR_DELIVERYSHIPPINGDriver / StaffSTAFF, ADMINAttaches driver details, updates status, dispatches live delivery tracking notification.
SHIPPING / PICKUPDELIVEREDCustomer / DriverAny Authorized ActorMarks order completed, records deliveredAt timestamp, dispatches automated review request message after 30 minutes.
ANY_PRE_DELIVERYCANCELLEDCustomer / AdminOrder Owner or AdminReleases 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 TimeSystem 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.