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

479 lines
19 KiB
Markdown

# 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.