WFDASH-WEBAPP/docs/sops/order-operator.md
Dave aed3f5c861 feat: implement discount functionality in POS system
Added support for discounts in the POS checkout process, allowing operators to apply either a fixed amount or a percentage off the gross total. Updated the state management to include discount details and modified the receipt and cart rendering to display pre- and post-discount values. Enhanced the checkout API to handle discount inputs and ensure proper calculations. Updated relevant tests to cover new discount logic.
2026-09-03 12:19:21 +02:00

19 KiB

Order Operator — SOP

Owns: the order lifecycle, from quote → order → delivered → invoiced. Creates purchase orders, drives the order_items state machine, hands off to invoicing.

Tools: /orders, /orders/[id], /orders/new, /collection-pool, /collection-map, /stock, /stock-orders, dashboard widgets.

Handoffs:

  • From Quote Operator → customer accepted a quote.
  • To Collection Driver → PO sent, item moves to collection_pool.
  • To Invoicing → item delivered, ready to invoice.
  • To Approver → escalated pricing / margin / customer issues.

Note: the order lifecycle is item-level, not order-level. orders.status does not exist as a field. The "Complete" badge on the order header is derived from every item is delivered. This is in src/lib/server/order-completion.ts. Get familiar with it.


1. Daily / Weekly Checklist

Every working day (start of shift, 08:00 SAST)

  • Open /orders — New tab. Every pending item should have a clear next action: either waiting for pricing review, waiting for a PO, or waiting for a supplier reply.
  • Open /orders — Processing tab. Verify all processed items have a PO generated. Items stuck in processed for >1 working day = process bug. Fix or escalate.
  • Open /orders — Collection Pool tab. If you see ready_for_collection: true items with no driver assigned, the collection driver hasn't picked them up — phone them.
  • Open dashboard → "Follow-ups Due". Action anything from you (POs waiting on supplier reply, etc).
  • Check /api/v1/purchase-orders/stale (or the dashboard widget). POs with no supplier reply for >3 working days must be phoned.

Every working day (mid-day, 13:00 SAST)

  • Open /orders — Collected tab. Verify stock_verified and picking items are progressing. Anything stuck in stock_verified for >2 hours = stock discrepancy. Resolve or escalate to the Stock Controller.
  • Check /stock?filter=low — if any of your orders used a stock item that's now critical, the customer needs a heads-up.

Every working day (end of shift, 17:00 SAST)

  • Review every order that changed today. Make sure all status transitions are correct. A delivered item that shouldn't be delivered is a customer-facing disaster.
  • Triage /orders — Delivered tab. Any delivered items older than 1 working day that aren't yet ready_invoice? Push them through.

Every Monday morning

  • Review your KPIs (see §4). Pay attention to the items that drifted last week.
  • Triage stuck orders — anything in pending or processed for >5 working days is a zombie. Either ship, cancel, or escalate.

Every Friday afternoon

  • Close the week. Make sure no orders have items in limbo states over the weekend. assigned to a driver who doesn't work weekends = uncollected Monday morning.
  • Invoicing handoff: confirm the invoice clerk has received everything they need for the week's ready_invoice items. They'll print Monday morning.

2. Step-by-Step Procedures

2.1 Create an order from a quote

When: a customer accepts a quote (you got the call, or the customer replied accepting).

  1. Open the quote detail.
  2. Click Create Order. The system creates:
    • One new orders record with number ORD-YYYY-MM-NNN.
    • One order_items row per quote_items row, linked back via order_items.quote_item.
  3. The quote status does NOT change automatically. The quote stays in approved (or emailed + your acceptance note).
  4. The order starts in New (pending) tab.

Don't:

  • Don't try to create an order from the same quote twice. The system returns the existing order, not a duplicate.
  • Don't edit the quote after creating the order — the items are now in order_items, edits to quote_items don't propagate.

2.2 Create a walk-in order (no quote)

When: a customer walks in / phones with an order that wasn't quoted.

  1. Open /orders/new.
  2. Pick or create the customer (and company).
  3. Add items manually. Each item needs at minimum description
    • quantity + unit_price. Cost is optional at creation (defaults to 0 — fix later from the PO).
  4. Save. Order lands in pending.
  5. Pricing: see §2.3.

2.3 Price and process a pending item

When: an order item lands in pending and you have a quote to source from.

  1. Open the order detail.
  2. Click the line item. Set cost_price (from the quote or catalogue) and selling_price.
  3. Click Mark Processed (status flips to processed).
  4. Decide next path:
    • In stock? → click Book from Stock. The item flips to stock_verified and the stock qty decrements via the ledger. (See §2.7 for stock flow.)
    • Not in stock, need to buy? → generate a PO. See §2.4.

2.4 Generate a purchase order (PO)

When: an item needs to be ordered from a supplier.

  1. On the order detail, click Generate PO. The system groups items by supplier and creates one PO per supplier.
  2. Open the PO. Verify:
    • Items + quantities match the order.
    • Cost prices are correct (from the quote or a fresh supplier call).
    • Delivery date is realistic.
  3. Click Send PO. The email modal opens. Same shape as sending a quote (§2.4 in the Quote Operator SOP).
  4. n8n sends the email. The supplier reply monitor updates supplier_replied_at when the supplier replies.
  5. The order item flips to po_generated.

Don't:

  • Don't generate a PO without verifying the cost. Once sent, the PO is the contract — wrong cost = wrong margin.

2.5 Receive a supplier invoice and book the goods

When: a supplier invoice / delivery note arrives (PDF in email, or paper).

  1. Identify the PO. Look for the "Your Ref" / "Order No" on the document. It'll be PO-YYYY-MM-NNN (customer PO) or PO{yy}{6d-seq}STK (stock PO).
  2. Diff first — call the system to compare the invoice against the PO. Read the verdict:
    • ready_to_receive → continue to step 3.
    • Anything else (cost_mismatch, qty_mismatch, supplier_mismatch, wrong_status, po_not_found, unmatched_*) → stop and report to the supervisor. Don't force it through.
  3. Commit the receive — for stock POs this books into stock_items.current_qty and writes a stock_ledger row. For customer POs, the order items flip to collected and the supplier invoice number is stamped.
  4. The system attaches the invoice PDF to the PO.

Mode:

  • mode: "invoice" — full invoice received (price + qty).
  • mode: "grn" — only a delivery note (qty only, no price check). Use this when the supplier sends the packing slip before the invoice.

Surplus / Short:

  • If the supplier delivered MORE than ordered (surplus), the PO diff flags it. Confirm with the Stock Controller; they may book the surplus to stock (§2.8).
  • If LESS than ordered (short), the system records the received qty. Create a continuation order for the missing qty (§2.6).

2.6 Split an order into a continuation

When: some lines are stuck (waiting on a back-ordered part), the rest can ship. Or: a walk-in order has lines added later and the original is past pending.

  1. Open the order detail.
  2. Verify which lines are pending. Only pending lines can be moved (others refuse silently to avoid orphaning a PO or driver assignment).
  3. Click Split Order. Pick the pending lines to move.
  4. A new ORD-YYYY-MM-NNN order is created with the moved lines. The parent keeps its number; the moved lines get a breadcrumb in notes.
  5. Process the new continuation order normally.

2.7 Book from stock

When: a processed line item has a stock_item_id set and qty available.

  1. On the order detail, click Book from Stock.
  2. The system calls /api/v1/stock/fulfill — decrements stock_items.current_qty and writes a stock_ledger row with reason: "customer_fulfillment".
  3. The order item flips to stock_verified → picking (or directly to collected if the picking step is bypassed for direct-from-stock orders).

If the stock qty is short (someone else booked it first), the system refuses. Either:

  • Cancel the line (status → cancelled).
  • Wait for restock and re-try later.
  • Source from the supplier instead (revert to processed → generate PO).

2.8 Book a surplus

When: the supplier delivered more than ordered (e.g. 12 instead of 10), and the customer only needs 10.

  1. On the order item, click Book Surplus.
  2. Enter the surplus qty (e.g. 2).
  3. The system:
    • Splits the order line: customer qty (10) stays on the order, surplus qty (2) goes to stock.
    • Writes a stock_ledger row with reason: "purchase_surplus".
    • Sets order_items.surplus_qty and stock_booked_from on the line.
  4. The stock item's current_qty increments.

2.9 Confirm collection (driver flow)

When: the driver has picked up the goods and the collection is being confirmed back at the office (or via the mobile app).

  1. On the order detail, find the item. It should be in collection_pool or assigned.
  2. Click Confirm Collected. Enter the qty received (the quantity_collected field is the amount to ADD, not the new total).
  3. For partial deliveries: clamp to received_qty - qty_collected (≥0). Add a note explaining the partial.
  4. The item flips to collected (full) or stays in collection_pool (partial) with the qty_collected updated.

Actor field is required. The system stamps the actor on every audit entry. If you don't pass it, the audit trail loses you.

2.10 Pick, pack, deliver

When: items are collected and ready to be packed for delivery.

  1. collected → stock_verified (verify qty against the shelf count).
  2. stock_verified → picking (operator starts pulling).
  3. picking → ready_delivery (packed, on the truck).
  4. ready_delivery → delivered (signed for at the customer).

Each step needs an operator action. Don't bulk-flip — the audit trail per step is the customer's dispute evidence.

2.11 Mark an order delivered (header-level)

When: every item on the order is delivered (or short-circuits via the eligible set — see tool doc).

  1. Open the order detail.
  2. Click Mark Delivered. The system flips every eligible item to delivered and stamps delivered_at + delivered_date.
  3. Does NOT flip order.invoiced. The Invoicing Clerk does that.

If the tool reports data.itemsFailed (some items refused), inspect them. The most common reason is an item in pending/processed/cancelled that wasn't eligible.

2.12 Hand off to invoicing

When: items are delivered and the customer has been notified.

  1. On the order detail, click Mark Ready to Invoice (the button shows when items are eligible). The items flip to ready_invoice.
  2. Don't call mark_invoiced here — that's the Invoicing Clerk's job, after they generate the QB invoice or delivery note.

3. Decision Tree

3.1 The customer wants to cancel after the order is placed

Has the supplier been PO'd yet (item is in `processed` or later)?
├── No → cancel the line(s), no cost incurred. Easy.
└── Yes
    ├── Has the supplier confirmed the order (replied to PO)?
    │   ├── No → phone the supplier NOW. Most will cancel
    │   │         without charge if you catch it pre-confirm.
    │   └── Yes
    │       ├── Is the goods in transit / collected?
    │       │   ├── No → call the supplier, negotiate a cancel.
    │       │   │         Some will charge a handling fee.
    │       │   └── Yes → goods will arrive. Three options:
    │       │         a) Accept the goods, book to stock,
    │       │            re-sell later.
    │       │         b) Refuse delivery (record the refusal
    │       │            on the order notes, supervisor
    │       │            decides whether to charge the customer
    │       │            a restocking fee).
    │       │         c) Ask the supplier to return-to-vendor.
    │       │            Their policy, not yours.
    │       └── Surplus / different qty → see §3.3
    └── Customer wants to change items, not cancel → see §3.2

3.2 Customer wants to change items after the order is placed

Is the order still all-`pending`?
├── Yes → use the order editor to swap the items.
└── No (some lines already `processed` or later)
    ├── Only the `pending` lines need to change → split off the
    │   pending lines, edit on the parent, leave the rest.
    └── Some `processed` lines need to change
        ├── Has the supplier PO'd been sent?
        │   ├── No → revert the line to `pending`, edit.
        │   └── Yes → phone the supplier first, then decide:
        │             either cancel the PO + re-issue, or ship
        │             the original and create a separate order
        │             for the replacement.
        └── The change affects pricing → update the order's
            selling price, document the customer agreement in
            `order.notes`.

3.3 Supplier delivered wrong / damaged / short

What's the discrepancy?
├── Wrong qty (short)
│   ├── Receive what arrived (use the `quantity_collected`
│   │   to-add semantics).
│   ├── Mark the missing qty as back-ordered. Create a
│   │   continuation order for it (§2.6) when you can re-supply.
│   └── Phone the supplier, log the short in `order.notes`.
├── Wrong qty (over — surplus)
│   └── See §2.8. Book the surplus to stock, keep the order qty
│       at the customer's need.
├── Wrong item
│   ├── Refuse delivery. Phone the supplier for a return-to-
│   │   vendor or a credit note.
│   └── If customer will accept the wrong item at a discount →
│       call the supervisor, don't unilaterally discount.
├── Damaged
│   ├── Refuse delivery. Take photos, attach to the PO via the
│   │   supplier_invoice_ prefix on `order_item_attachements`.
│   └── If customer accepts damaged at a discount → supervisor
│       call, photos on file, credit note from supplier.
└── All correct
    └── Confirm the receive (§2.5).

3.4 Stock item is short when I try to book

Is the short a rounding / measurement error?
├── Yes → recheck the stock count. The Stock Controller may
│         have missed an entry.
└── No
    ├── Is the item critical (low stock) → flagged in
    │   `/stock?filter=critical`?
    │   ├── Yes → the order is more important than the stock
    │   │         refill. Book it. File a stock-order for
    │   │         restock.
    │   └── No → cancel the stock booking, source from the
    │             supplier via PO.
    └── Has the stock item been double-booked? → check
        `stock_ledger` for recent bookings. The Stock
        Controller can release the duplicate.

3.5 Supplier hasn't replied to a PO in 3+ days

Did the PO send successfully (check `email_message_id`)?
├── No (mail_failed) → retry the send, verify recipient address
└── Yes
    ├── Phone the supplier. Most POs are lost in spam.
    ├── If no answer, email the sales rep directly (check
    │   `/suppliers` for the contact).
    └── After 7 working days with no reply → escalate to
        supervisor. We may need to find a backup supplier.

3.6 Customer wants a delivery date that's not realistic

Can the supplier meet it (have they confirmed)?
├── Yes → set the order's `delivery_date` accordingly.
└── No
    ├── Is the customer willing to wait?
    │   ├── Yes → set a realistic date, log the agreement in
    │   │         `order.notes`. Don't over-promise.
    │   └── No → escalate to supervisor. We may need to split
    │             sources, expedite (at a cost), or partial-
    │             ship.
    └── Is there stock for at least part of the order? → split
        the order (§2.6). Ship what's in stock, back-order
        the rest.

3.7 An order has items in mixed states (some delivered, some pending)

Is the customer OK with partial delivery?
├── Yes → ship what's ready. The delivered items go to
│         invoicing. The pending items stay on the order
│         until they're ready.
└── No (customer needs everything together) → leave the order
    open. Don't flip anything to delivered until everything
    is on the truck. The customer is OK with the wait.

4. KPIs

All KPIs measured in working days.

KPI Target How it's measured
Order creation turnaround (quote accepted → order exists) < 30 min median orders.created - quotes.approved_at
pending → processed turnaround < 4 working hours median processed_at - created on order_items
PO send turnaround (processed → po_generated) < 1 working day median po_generated_at - processed_at
Supplier reply time < 2 working days median supplier_replied_at - email_message_id_sent
PO stale rate < 5% of open POs POs without reply > 3 working days / open POs
Collection turnaround (po_generated → collected) < 3 working days median collected_at - po_generated_at
collected → delivered turnaround < 1 working day median delivered_at - collected_at
Order completion rate (delivered within 5 working days of pending) > 80% orders with all items delivered within 5 wd / orders created
PO cost-mismatch rate < 3% of receives count of cost_mismatch verdicts on diff / total receives
Surplus booking rate 100% of surpluses booked same day stock_ledger entries with reason="purchase_surplus" per day vs supplier delivery notes
Split order rate < 10% of orders orders with notes containing a split breadcrumb / total orders
Cancellation rate < 5% of pending lines order_items.item_status="cancelled" / total order_items created

How KPIs feed into your review

  • Weekly (Monday standup): one-pager with the operator's KPIs. Stale POs, cost mismatches, and stuck pending items are the supervisor's first questions.
  • Monthly (1st of month): 1-page review with the operator. Surplus booking rate and PO cost-mismatch rate are weighted double — getting them wrong is a money leak.
  • Quarterly: top performers on completion rate + on-time delivery are eligible for the "trusted buyer" tier — they can raise POs up to a higher value without supervisor sign-off.

What the system does NOT measure (yet)

  • Customer satisfaction post-delivery (no feedback loop yet)
  • Damage rate per supplier (only the PO-level diff verdict is captured, not the cause)
  • Driver utilisation (that's the collection driver's KPI)

If a KPI is unfair, raise it. The supervisor can adjust targets, but the metric stays.