WFDASH-WEBAPP/docs/collection-ai-checks.md
Dave 3eea87ec03 feat(collection-confirm): auto-push purple cards on to invoicing, with a 2-day log
After each AI tick, every Collection Confirm card the page shows purple
(cardAiState verified) is confirmed without a person. Customer lines go
to ready for invoicing with the invoice number and date the AI read; stock
orders go to stock booking once every line is collected. Cards with a
problem flag, a photo for another PO, or no readable invoice number/date
stay for review.

Each push is recorded in ai_settings.collection_auto_push_log and shown at
the top of the page for today and yesterday (PO, supplier, items, value).
Off by default behind a Settings > AI switch; needs the manual live step
npm run pb:migrate:collection-auto-push before it will run.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:36:23 +02:00

192 lines
10 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`).
## Auto-push (added 2026-10-28)
With **Auto-push verified cards** on (Settings, AI, Collection checks, off by
default), the tick confirms a card on its own when the page shows it purple
(`cardAiState` "verified": every line checked, no red or amber tick). It runs
at the end of every tick over every card on the page, so a card a person
checked by hand is covered too. It calls the same functions as the manual
buttons (`confirmOrderItemsWithSplits`, `confirmStockRows` +
`markStockOrdersReceived`), as actor "AI collection check".
- Customer lines go to ready for invoicing with the invoice number and date
the AI read. A card covered by two invoices is pushed one invoice at a time.
- Stock lines go to the accountant's stock booking, but only when every line
of the stock order is already collected (no follow-on order is created by
the AI).
- A card is left for a person when: any line has a problem flag, a photo
looks like it belongs to another PO, or a customer line has no invoice
number or no `YYYY-MM-DD` date. Grey ticks do not block it (same rule as
the purple card).
- Every push is appended to `ai_settings.collection_auto_push_log` (kept
three days). The page shows today and yesterday at the top: PO, supplier,
items, value. Endpoint: `GET /api/v1/collection-confirm/auto-push-log`.
- Without the PB fields it never pushes (it could not record the push).
Going live: `npm run pb:migrate:collection-auto-push`, deploy, then switch it on
in Settings, AI and Save.
## 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.