The driver app records the full quantity as received when it collects a
stock pickup, and Collection Confirm only listed stock lines that were
not fully received — so collected stock orders skipped it and sat at
"sent", outside the accountant's booking queue, until someone clicked
Mark received (29 stuck on 2026-10-19).
- STOCK_ON_PAGE_FILTER / stockLineOnPage: a collected stock line stays on
Collection Confirm until its stock order is received; cancelled orders
never show. Shared by the list and the AI check (so those lines get
photos read and ticks too).
- Confirm stock: markStockOrdersReceived flips each stock order to
"received" once every line is collected/booked (→ booking queue);
orders with lines still out stay open, reported as stillOpen. The
route returns { updatedIds, received, stillOpen }; the toast says so.
- Stock photos: `npm run pb:migrate:stock-photos` makes
stock_purchases.additional_attachments multi-file;
addStockPurchaseAttachment, Move photo and Delete photo add/remove with
`+`/`-` instead of replacing the single slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
165 lines
9.1 KiB
Markdown
165 lines
9.1 KiB
Markdown
# Collection Confirm AI checks runbook
|
|
|
|
Design: `docs/superpowers/specs/2026-10-17-collection-confirm-ai-checks-design.md`.
|
|
|
|
## What it does
|
|
|
|
On Collection Confirm, drivers upload photos of the supplier's tax invoice
|
|
or delivery note. The AI (the provider set in Settings, AI) reads each
|
|
photo, pairs the document lines with our order lines, and shows four ticks
|
|
per line (SKU, quantity, description, price), plus a POS column and an
|
|
"AI findings" box when something at document level looks wrong.
|
|
|
|
- It runs automatically (n8n clock, every 5 minutes) on files not yet read,
|
|
and on demand with the **Check with AI** button on a card.
|
|
- **Check with AI** needs only the global AI switch. The scheduled tick
|
|
needs both the global AI switch and "Collection checks enabled".
|
|
- A file is read once (by content hash, stored in `collection_doc_reads`).
|
|
A failed photo read is retried only by the **Check with AI** button, not
|
|
by the tick.
|
|
- Lines with no AI check look exactly as before, with no ticks.
|
|
- **Add to POS** is a manual feature (per line, or "Confirm and add to
|
|
POS" on the card). It creates a `stock_items` row with zero stock. A line
|
|
needs a SKU first (the button is disabled without one); the toast reports
|
|
lines added, failed and still needing a SKU.
|
|
|
|
What the AI writes: only `ai_check` on the order/stock line, the document
|
|
reads, an **empty SKU**, and a **discounted cost** — each only when it is
|
|
sure. It never overwrites a SKU a person set, never confirms a collection,
|
|
and never changes status.
|
|
|
|
Discounts (added 2026-10-19): the AI reads a line's own "Disc %" column
|
|
(e.g. Swagelok) and a discount printed only in the invoice totals
|
|
(e.g. HYCO "Discount 25.00%"), and the Price tick compares our cost with
|
|
the price **after** discount. When a tax-invoice line it is "sure" about
|
|
has a discount and our cost isn't the discounted price (to the cent), the
|
|
AI sets the cost to it — through the same path as the cost pencil, so a
|
|
`cost` note is added and the audit log records it — and shows "Cost
|
|
changed from R… to R…: <invoice> shows R… less …%" in purple under the
|
|
line. Without a discount, from a delivery note, or on a "likely" match it
|
|
never touches the cost. Reads saved before this change have no discount
|
|
fields: delete that document's `collection_doc_reads` row and run
|
|
**Check with AI** to re-read it.
|
|
On order lines the SKU fill is audited as an ordinary `sku` change; stock
|
|
purchase lines (`stock_purchase_items`) are not covered by the audit hook,
|
|
so a SKU fill there is not in the audit log.
|
|
|
|
## Going live (manual steps, in this order)
|
|
|
|
1. **Migrate PocketBase.** Run against the live PB:
|
|
`npm run pb:migrate:collection-ai`
|
|
(creates `collection_doc_reads`, adds `ai_check` to `order_items` and
|
|
`stock_purchase_items`, and the `collection_ai_*` fields on
|
|
`ai_settings`).
|
|
2. **Redeploy the audit hook** `pocketbase/pb_hooks/audit_log_utils.js` to
|
|
the PB server's `pb_hooks/`. It now ignores `ai_check`; until then the
|
|
only cost is noisy history (a large JSON diff on every re-run).
|
|
3. **Deploy** the app. Pushing to main does not auto-deploy; it is a
|
|
manual deploy that takes 5 to 10 minutes.
|
|
4. **Activate the n8n workflow** (below). Create it inactive, activate it
|
|
after steps 1 to 3.
|
|
5. **Turn it on** in Settings, AI: the global "AI enabled" switch must be
|
|
on, then switch on "Collection checks enabled" in the Collection checks
|
|
section and click Save.
|
|
|
|
The tick does nothing until both switches are on, so steps 4 and 5 are
|
|
safe in either order.
|
|
|
|
## n8n workflow: "WFDASH Collection check tick"
|
|
|
|
n8n only acts as a clock; the app decides whether anything is due.
|
|
|
|
| Node | Setting |
|
|
|---|---|
|
|
| Name | `WFDASH Collection check tick` |
|
|
| Trigger | Schedule Trigger, every 5 minutes |
|
|
| Action | HTTP Request |
|
|
| Method / URL | `POST` `https://wfdash.apphub.co.za/api/v1/collection-confirm/ai/tick` |
|
|
| Authentication | The `WFDASH Agent API Key` credential (`Authorization: Bearer <AGENT_API_KEY>`). Do not create a new secret. |
|
|
| Body | none |
|
|
| Options | Timeout **120000 ms** (120 s); Never Error (neverError) **on** |
|
|
| Status | **inactive** until the app is deployed and migrated |
|
|
|
|
After deploy, Execute workflow once. Expected responses:
|
|
`{"ran":false,"reason":"disabled"}` while off, `{"ran":false,"reason":"running"}`
|
|
while an earlier run is still going, `{"ran":true,"summary":{...}}` when it
|
|
ran. A 401 means the credential is wrong.
|
|
|
|
Each run is bounded: at most 10 files, and it stops starting new files
|
|
after 90 seconds, so it normally answers inside the 120 s timeout;
|
|
leftovers carry to the next run. Runs never overlap: a run holds a lease
|
|
for up to 10 minutes, and if a run dies without writing its summary the
|
|
next tick after that takes over.
|
|
|
|
The last run (files read, cards checked) is shown in Settings, AI,
|
|
Collection checks, and stored in
|
|
`ai_settings.collection_ai_last_run_summary`.
|
|
|
|
Photo on the wrong PO (added 2026-10-19): when the AI reads a PO number
|
|
on a photo that isn't the card's PO, and that number is a PO we have, the
|
|
card says "Photo may be for <PO>" and, when open, "Invoice <no> is for
|
|
<PO>, not this PO" with a **Move photo to <PO>** button. The move is a
|
|
person's call — nothing moves on its own. It copies the photo onto the
|
|
other PO (a collected line of a customer PO, or the stock purchase's one
|
|
photo slot — refused if that slot is taken), removes it from this line,
|
|
and deletes the AI's read of it so it is read again where it now belongs
|
|
(`POST /api/v1/collection-confirm/move-photo`,
|
|
`src/lib/server/collection-photo-move-service.ts`).
|
|
|
|
Delete photo (added 2026-10-19): next to "Add supplier invoice", **Delete
|
|
photo** removes the photo open in the previewer — for a wrong upload or a
|
|
duplicate — after an inline "Delete …? Yes, delete" confirm. It takes the
|
|
file off every order line (or the stock purchase) carrying it and deletes
|
|
the AI's read of it. The PO PDF can't be deleted there. This can't be
|
|
undone (`POST /api/v1/collection-confirm/delete-photo`).
|
|
|
|
Stock orders (changed 2026-10-19): a driver collecting a stock pickup
|
|
records the full quantity as received, so collected stock lines used to
|
|
skip Collection Confirm and sit "sent" until someone clicked Mark received
|
|
(29 stock orders were stuck). Now a collected stock line stays on
|
|
Collection Confirm (with its photo and AI check) until its stock order is
|
|
marked received; **Confirm stock** marks it received once every line is
|
|
collected — that sends it to the accountant's booking queue. Orders with
|
|
lines still out with a driver stay open (the toast says how many).
|
|
Cancelled stock orders never show. Driver photos for stock pickups are
|
|
kept on the stock purchase (`additional_attachments`, multi-file after
|
|
`npm run pb:migrate:stock-photos`).
|
|
|
|
## Reading the ticks
|
|
|
|
| Colour | Meaning |
|
|
|---|---|
|
|
| Green | The document agrees with our line: our SKU equals the document code after normalising (uppercase, letters and digits only; this is also how a SKU the AI filled goes green), quantity equals what was collected and what was ordered, the AI is sure it is the same item, unit price after any discount within 1 cent |
|
|
| Amber | Close but worth a look: our SKU differs from the document code; collected quantity matches the document but the ordered quantity differs; the AI thinks it is the same item but is only "likely" sure |
|
|
| Red | Mismatch or missing: no SKU on our line, quantity differs from the document, price differs or we have no cost, or the line is not on the document or the AI says it is a different product (all four red) |
|
|
| Grey | Can't check, per tick: **SKU** when the document line shows no code; **Qty** when it shows no quantity; **Price** when the document line shows no price (a delivery note that prints prices, like Plumblink's, is compared like an invoice — but the AI only ever corrects a cost from a tax invoice); **all four** when the AI couldn't pair the document (see the AI findings box for the reason; **Check with AI** retries) |
|
|
|
|
For stock purchase lines the SKU tick and robot use the line's own SKU
|
|
(the one a rep or the AI set on the purchase line), not the catalogue SKU.
|
|
|
|
The AI findings box also lists the AI's **reading notes** — what it noticed on the photo (a delivery note rather than an invoice, the year missing from the date, a rotated or partly covered page).
|
|
|
|
Hover a tick for the values compared (ours vs the document). A robot icon
|
|
marks a SKU the AI filled. Click the document chip on a card to see the
|
|
extracted lines next to the photo.
|
|
|
|
## Errors
|
|
|
|
- **"Collection checks are not set up yet: run `npm run pb:migrate:collection-ai`
|
|
against PocketBase first"** is the guard. The service refuses to run
|
|
until `ai_check` exists on the live PB, because PB silently drops
|
|
unknown fields and results would vanish. Run step 1 above.
|
|
- **"AI is disabled in settings"** from **Check with AI**: switch on the
|
|
global AI switch in Settings, AI.
|
|
|
|
## Changing the prompt
|
|
|
|
Settings, AI, Collection checks prompt. "Reset to default" loads the
|
|
built-in prompt; empty means the built-in default. Keep it to the
|
|
`submit_document_read` tool's fields.
|
|
|
|
## Stopping it
|
|
|
|
Switch off "Collection checks enabled" in Settings, AI and Save, or
|
|
deactivate the n8n workflow. Existing ticks stay until the card is
|
|
re-checked or the line is cleared.
|