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.
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. Everypendingitem 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 allprocesseditems have a PO generated. Items stuck inprocessedfor >1 working day = process bug. Fix or escalate. - Open
/orders— Collection Pool tab. If you seeready_for_collection: trueitems 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. Verifystock_verifiedandpickingitems are progressing. Anything stuck instock_verifiedfor >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
delivereditem that shouldn't bedeliveredis a customer-facing disaster. - Triage
/orders— Delivered tab. Anydelivereditems older than 1 working day that aren't yetready_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
pendingorprocessedfor >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.
assignedto 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_invoiceitems. 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).
- Open the quote detail.
- Click Create Order. The system creates:
- One new
ordersrecord with numberORD-YYYY-MM-NNN. - One
order_itemsrow perquote_itemsrow, linked back viaorder_items.quote_item.
- One new
- The quote status does NOT change automatically. The quote
stays in
approved(oremailed+ your acceptance note). - 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 toquote_itemsdon't propagate.
2.2 Create a walk-in order (no quote)
When: a customer walks in / phones with an order that wasn't quoted.
- Open
/orders/new. - Pick or create the customer (and company).
- Add items manually. Each item needs at minimum
descriptionquantity+unit_price. Cost is optional at creation (defaults to 0 — fix later from the PO).
- Save. Order lands in
pending. - 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.
- Open the order detail.
- Click the line item. Set
cost_price(from the quote or catalogue) andselling_price. - Click Mark Processed (status flips to
processed). - Decide next path:
- In stock? → click Book from Stock. The item flips
to
stock_verifiedand 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.
- In stock? → click Book from Stock. The item flips
to
2.4 Generate a purchase order (PO)
When: an item needs to be ordered from a supplier.
- On the order detail, click Generate PO. The system groups items by supplier and creates one PO per supplier.
- 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.
- Click Send PO. The email modal opens. Same shape as sending a quote (§2.4 in the Quote Operator SOP).
- n8n sends the email. The supplier reply monitor updates
supplier_replied_atwhen the supplier replies. - 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).
- Identify the PO. Look for the "Your Ref" / "Order No" on
the document. It'll be
PO-YYYY-MM-NNN(customer PO) orPO{yy}{6d-seq}STK(stock PO). - 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.
- Commit the receive — for stock POs this books into
stock_items.current_qtyand writes astock_ledgerrow. For customer POs, the order items flip tocollectedand the supplier invoice number is stamped. - 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.
- Open the order detail.
- Verify which lines are
pending. Onlypendinglines can be moved (others refuse silently to avoid orphaning a PO or driver assignment). - Click Split Order. Pick the
pendinglines to move. - A new
ORD-YYYY-MM-NNNorder is created with the moved lines. The parent keeps its number; the moved lines get a breadcrumb innotes. - 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.
- On the order detail, click Book from Stock.
- The system calls
/api/v1/stock/fulfill— decrementsstock_items.current_qtyand writes astock_ledgerrow withreason: "customer_fulfillment". - The order item flips to
stock_verified→picking(or directly tocollectedif 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.
- On the order item, click Book Surplus.
- Enter the surplus qty (e.g. 2).
- The system:
- Splits the order line: customer qty (10) stays on the order, surplus qty (2) goes to stock.
- Writes a
stock_ledgerrow withreason: "purchase_surplus". - Sets
order_items.surplus_qtyandstock_booked_fromon the line.
- The stock item's
current_qtyincrements.
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).
- On the order detail, find the item. It should be in
collection_poolorassigned. - Click Confirm Collected. Enter the qty received (the
quantity_collectedfield is the amount to ADD, not the new total). - For partial deliveries: clamp to
received_qty - qty_collected(≥0). Add anoteexplaining the partial. - The item flips to
collected(full) or stays incollection_pool(partial) with theqty_collectedupdated.
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.
collected→stock_verified(verify qty against the shelf count).stock_verified→picking(operator starts pulling).picking→ready_delivery(packed, on the truck).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).
- Open the order detail.
- Click Mark Delivered. The system flips every eligible
item to
deliveredand stampsdelivered_at+delivered_date. - 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.
- On the order detail, click Mark Ready to Invoice (the
button shows when items are eligible). The items flip to
ready_invoice. - Don't call
mark_invoicedhere — 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.