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.
479 lines
19 KiB
Markdown
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.
|