Add the full buyer-side supplier invoice reconciliation workflow: - Include the new `stock_purchases.match_and_receive_from_invoice` MCP tool for deterministic PO-invoice matching - Create core matching service with strict, rule-based line matching (sku → supplier_sku → description overlap) - Support dry-run default mode to validate matches before committing - Add automatic attachment handling with standard `supplier_invoice_*` naming convention - Add full test suite for the matching logic - Update documentation and agent skill profile to document the new tool and workflow Pre-existing test failures (3 total) are unrelated to this change.
132 lines
15 KiB
Markdown
132 lines
15 KiB
Markdown
# MCP Feature Requests & Gaps
|
||
|
||
Captures requests the existing WFDASH MCP tools can't fulfil, surfaced from real
|
||
questions asked by agent operators / the agent itself. Each entry should be
|
||
small enough to ship in a single PR.
|
||
|
||
When one ships, move it to **Implemented** with a one-line commit/PR reference.
|
||
|
||
## Entry template
|
||
|
||
```
|
||
### [SHORT TITLE]
|
||
- **Surfaced:** YYYY-MM-DD — <quoted or paraphrased user question>
|
||
- **Workaround:** <what we did in the meantime to answer the question>
|
||
- **Severity:** now | nice-to-have
|
||
- **Suggested tool:** <name + signature, e.g. `orders.stats({from, to})`>
|
||
- **Schema needs:** <PB field additions / changes, or "none">
|
||
- **Open questions:** <anything to clarify with ops before implementing>
|
||
```
|
||
|
||
## Open requests
|
||
|
||
### Orders: filter by customer
|
||
- **Surfaced:** 2026-08-06 — "last 3 orders for Seth"
|
||
- **Workaround:** filter `order_items` by `order.customer = "<id>"`, dedupe to distinct orders
|
||
- **Severity:** now — every "orders for customer X" question hits this
|
||
- **Suggested tool:** add `customer` (id or name) + `from`/`to` filters to `orders.list`
|
||
- **Schema needs:** none
|
||
|
||
### Orders: date-range filter on `orders.list`
|
||
- **Surfaced:** 2026-08-06 — "how many orders were placed yesterday"
|
||
- **Workaround:** fall back to `order_items.list` with `created` filter, dedupe to distinct orders
|
||
- **Severity:** now — every "yesterday / last week / this month" question hits this
|
||
- **Suggested tool:** add `filter` (PB syntax) + `createdAfter` / `createdBefore` to `orders.list`
|
||
- **Schema needs:** none
|
||
|
||
### Orders: aggregate money value (count + selling total + cost total)
|
||
- **Surfaced:** 2026-08-06 — "...give me the money value aswell"
|
||
- **Workaround:** sum `selling_price × quantity` across items client-side
|
||
- **Severity:** now — every count question also wants a value
|
||
- **Suggested tool:** `orders.stats({from, to, groupBy?: 'day'|'customer'|'supplier'})` returning `{count, totalSell, totalCost}`
|
||
- **Schema needs:** none
|
||
|
||
### Order items: clarify "received" — no unambiguous field
|
||
- **Surfaced:** 2026-08-06 — "how many orders were received yesterday"
|
||
- **Workaround:** filtered by `qty_collected > 0 && updated` on target date — proxy, not exact
|
||
- **Severity:** now — user had to disambiguate ("received" can mean received-from-supplier, collected-from-warehouse, or delivered-to-customer)
|
||
- **Open questions:** ops must decide which definition to standardise on; current candidates are `drv_collected_at` (12 items/d yesterday) and `delivered_at` (0 items/d yesterday)
|
||
- **Suggested tool:** once defined, add a `received_at` filter param to `order_items.list` and a `orders.received_between` helper
|
||
- **Schema needs:** *probably* none if we pick an existing field; might need a new `received_at` if the chosen definition isn't already captured
|
||
|
||
### Order items: detect items missing `selling_price`
|
||
- **Surfaced:** 2026-08-06 — 4 of 42 items placed yesterday had `selling_price = 0` (still unpriced at end of day)
|
||
- **Workaround:** filter `selling_price = 0 && created > X` and report
|
||
- **Severity:** nice-to-have — these are unpriced orders that shouldn't ship without review
|
||
- **Suggested tool:** `orders.unpriced_items({from, to, minAgeHours?: number})` returning `{orderId, itemId, description, createdAt, hoursUnpriced}`
|
||
- **Schema needs:** none
|
||
|
||
### Dashboard: single-call daily summary
|
||
- **Surfaced:** 2026-08-06 — combining counts + values across multiple tools to answer one question
|
||
- **Workaround:** 3–4 separate tool calls + client-side aggregation
|
||
- **Severity:** nice-to-have
|
||
- **Suggested tool:** `dashboard.daily_summary({date, includeReceived: bool})` returning a single payload with placed + received counts and totals
|
||
- **Schema needs:** none
|
||
|
||
### Order items: whitelist missing `sku` field
|
||
- **Surfaced:** 2026-08-06 — "the items on PO-2026-08-006 add this sku for each item and save it" — every patch with `sku` rejected
|
||
- **Workaround:** none — `sku` is a real PB field on `order_items`, but the route.ts whitelist (`ORDER_ITEM_PATCH_FIELDS`) omitted it. Locally fixed by adding `"sku"` to the array at `route.ts:145`; still pending deploy on `wfdash.apphub.co.za`
|
||
- **Severity:** now — every supplier PO needs SKUs and the agent can't set them
|
||
- **Suggested tool:** no new tool — just keep the whitelist in sync with the real PB schema. Better long-term: derive whitelist from the live PB schema on each call (or at boot) instead of a hardcoded array, so schema drift never silently blocks writes
|
||
- **Schema needs:** none — PB field exists, the gap is MCP-only
|
||
- **Open questions:** should the patch tool also reject unknown keys silently (current behaviour is a hard 4xx — fine), or warn-and-accept? User seems happy with hard rejects for now.
|
||
|
||
### Orders: no create-from-scratch — only from a quote
|
||
- **Surfaced:** 2026-08-06 — "can you create a order or load items as a order using mcp?"
|
||
- **Workaround:** the only path is `quotes.*` → `orders.create_from_quote`. To create an order from scratch via MCP: (1) seed a quote via `quotes_create_from_email` or `quotes_create_from_template`, (2) add items with `quote_items_create` (or `bulk_update_quote_items`), (3) call `orders.create_from_quote`. There is NO `orders.create` and NO `order_items.create`.
|
||
- **Severity:** nice-to-have — the quote-pipe works, but it's 3+ calls and forces the user to materialise a quote just to load an order
|
||
- **Suggested tool:** `orders.create({customer, company, items: [{description, quantity, unit?, sku?, cost_price?, selling_price?}]})` returning the new order + items. (Pushes the same idea further: `order_items.create({order_id, ...})` if items need to be appended to an existing order.)
|
||
- **Schema needs:** none — every field already exists on `orders` / `order_items`
|
||
- **Open questions:** if we ship this, does `quotes.*` still matter as a destination, or do we collapse quote and order into "draft vs committed"? Currently the app forces the two-step dance; an opinionated answer here would also clean up the buyer flow.
|
||
|
||
### Quote/order items: whitelist missing `category` field
|
||
- **Surfaced:** 2026-08-06 — "can you see the category and ai insights? can you change the category"
|
||
- **Workaround:** reads work fine (`category` is returned on every item + `expand=category` exposes the name/match_keywords). Writes are blocked — same shape as the `sku` whitelist gap: `QUOTE_ITEM_PATCH_FIELDS` and `ORDER_ITEM_PATCH_FIELDS` in `route.ts` don't list `category`, so any patch with it is rejected with `forbidden_field`. Until that whitelist gap is closed + deployed, the only way to recategorize is via PB admin UI. Audit found 2 items needing change: KYOCERA TIPS (Fasteners → Cutting Tools) and Bulldog Disc (Unknown → Fasteners).
|
||
- **Severity:** now — recategorization is a real operator workflow and Hermes can't do it
|
||
- **Suggested tool:** no new tool — fold into the same `route.ts` whitelist expansion that fixes `sku` (and consider regenerating the whitelists from the live PB schema so this stops happening). Optional companion tool: `categories.classify({description})` running the master-list keyword logic in `route.ts` so the AI agent doesn't have to memorise the 10 categories.
|
||
- **Schema needs:** none
|
||
- **Open questions:** none — same pattern as the `sku` entry above.
|
||
|
||
### Stock purchases: no PDF → stock order ingestion
|
||
- **Surfaced:** 2026-08-06 — "can you load a stock order from a pdf that i give you or items"
|
||
- **Workaround:** `stock_purchases_create({supplier, items:[...]})` works for structured input but the operator has to type each line. No tool accepts a PDF or an email payload. Existing attachment uploads (`purchase_orders_add_attachment`, `quotes_list_attachments`) attach files *to existing records*; they don't parse them.
|
||
- **Severity:** nice-to-have
|
||
- **Suggested tool:** `stock_purchases.create_from_pdf({supplier, base64, content_type})` that parses supplier PO PDFs (using something like `pdf-parse` or `unpdf`) and emits a draft `stock_purchase` for review. Alternatively, `stock_purchases.create_from_email({from, subject, body, ...})` mirroring `quotes_create_from_email`.
|
||
- **Schema needs:** none — the `stock_purchase` + `stock_purchase_item` collections already exist
|
||
- **Open questions:** which PDF format(s) does the user want supported? Many SA suppliers send bespoke PO PDFs — a generic parser would need supplier-specific extraction rules. Probably needs an LLM-driven extraction step rather than layout heuristics. Worth scoping before implementation.
|
||
|
||
### Stock purchases: paperclip badge on list table
|
||
- **Surfaced:** 2026-08-06 — surfaced during the attachments-card PR (UI ships the detail-page affordance but the list table still shows no document indicator)
|
||
- **Severity:** nice-to-have
|
||
- **Suggested tool:** add a `count_additional_attachments` field to `stock_purchases.list` (or a separate `/api/v1/stock-purchases/attachment-counts` endpoint mirroring `/api/v1/sprint/attachment-counts`), render the sprint-style `<Paperclip>` chip in `StockOrdersTable` per row
|
||
- **Schema needs:** none — derived from `additional_attachments.length`
|
||
|
||
### Drivers: no MCP visibility or assignment
|
||
- **Surfaced:** 2026-08-06 — "can you see the driver stops and assign stops to drivers?"
|
||
- **Workaround:** "driver" data lives only as three fields on `order_items` (`drv_id`, `drv_assigned_at`, `drv_collected_at`). There is no separate `drivers` collection, no MCP tool to list drivers, no tool to see today's stops for one driver, no tool to bulk-assign stops to a driver. Operators can call `orders.list` and manually click through. Effectively the driver flow is invisible to the agent.
|
||
- **Severity:** now — driver dispatch is a daily workflow and currently bypasses the agent entirely
|
||
- **Suggested tool:** `drivers.list()` returning `{id, name, currentLoad, maxStops}`. `drivers.assign_stops({drv_id, order_item_ids:[...]})` to bulk-assign. `drivers.stops({drv_id, date})` returning today's route. If `drivers` isn't a real PB collection, the simplest first cut is virtual (read drivers off `users` or `suppliers` where they probably already exist) — but that needs ops to confirm the canonical source.
|
||
- **Schema needs:** possibly a new `drivers` collection if there isn't one already; otherwise a way to identify the driver relation (`drv_id` currently points to a freeform string, not a PB relation)
|
||
- **Open questions:** where is the canonical drivers list today? `users`? `suppliers`? a CSV? "Assign" semantics — operator workflow or agent-controlled?
|
||
|
||
### Suppliers: read/update works, geocoding does not
|
||
- **Surfaced:** 2026-08-06 — "can you access suppliers and geocode them update the lat and long if needed ? and the address ?"
|
||
- **Read:** ✅ `suppliers.list` / `suppliers.get` already return full records including `coordinates` (legacy "lat, lng" string) and `supplieraddress`
|
||
- **Update:** ✅ `suppliers.update({id, patch:{supplieraddress, coordinates, ...}})` patches those fields today
|
||
- **Geocode:** ❌ no MCP tool — there's no geocoding service exposed, so the agent can't *generate* the lat/lng from an address. Anyone wanting current coordinates has to call Google Maps / Nominatim externally and then patch the result via `suppliers.update`. `coordinates` is also still the legacy text "lat, lng" string on `suppliers` (it's pre-`geoPoint`) — even when the agent *has* lat/lng it has to know to format it as "lat,lng".
|
||
- **Severity:** now — the user clearly wants the round-trip (read missing coords → geocode → write back) automated, not hand-driven
|
||
- **Suggested tool:** `suppliers.geocode({id?, address?, limit?: 10})` calling a hosted geocoder (OpenStreetMap Nominatim is free and sufficient; Google would need a key). Returns `{lat, lng, formatted_address, confidence}` and optionally persists via `suppliers.update` if `id` is passed. Migrate `coordinates` to `geoPoint` while we're here so the round trip becomes a single field write.
|
||
- **Schema needs:** migrate `suppliers.coordinates` from text to PB `geoPoint` (delete the field, recreate it as lat/lng pair). Backfill existing rows from the parsed "lat,lng" strings first.
|
||
|
||
## Schema questions for ops
|
||
|
||
These came up but need a human answer before code changes:
|
||
|
||
- **"Received" semantics** — see `Order items: clarify "received"` above. Also, the `order_items_update` docstring references fields (`collection_date`, `collected_date`, `verified_date`, `delivered_date`) that don't exist in the current PB schema (only `drv_collected_at`, `delivered_at`, `assigned_at`, `drv_assigned_at` exist). Either the docstring is stale or those fields were planned and never created.
|
||
- **Currency notes** — prices are stored as numbers but the `notes` field on items embeds "R" (ZAR) strings. Confirm the system is single-currency ZAR before adding any multi-currency stats.
|
||
|
||
## Implemented
|
||
|
||
- 2026-08-06 — `stock_purchases.add_attachment` shipped (commits `f89bbb3`, `dd57d29`). Adds a sibling to `purchase_orders.additional_attachments` plus the matching REST upload/delete routes and a UI card on `/stock-orders/[id]`. PB schema: new `stock_purchases.additional_attachments` file field (multi, maxSelect 10, any MIME).
|
||
- 2026-08-07 — Split-invoice stock-PO workflow shipped. 5 new MCP tools (`stock_purchase_items.update`, `stock_purchase_items.bulk_update`, `stock_purchase_items.assign_driver`, `stock_purchase_items.mark_collected`, `stock_items.update`) and the underlying services (`stock-purchase-items-service.ts`, extended `stock-items-service.ts`). Closes the per-line write gap surfaced by the operator's split-delivery scenario (PO with multiple supplier invoices arriving on different days). `mark_collected` auto-flips the parent PO header to `received` when every line is fully collected. PB schema: new `stock_purchase_items.sku` / `unit` / `supplier_sku` text fields (migration `scripts/migrations/2026-08-07-stock-purchase-items-catalogue-fields.ts`, user applies via PB admin).
|
||
- 2026-08-09 — Buyer-side supplier-invoice reconciliation shipped. New MCP tool `stock_purchases.match_and_receive_from_invoice` (single commit, no schema change). The agent parses a scanned supplier invoice with its native vision, passes the structured fields (`po_reference`, `invoice.{document_no,date,supplier_name?,lines[]}`, optional `attachment` bytes) plus a `confirm` flag. Deterministic line-by-line matching: `stock_item.sku` exact → `stock_purchase_item.supplier_sku` exact → description token overlap (≥80%). Verdict enum (`ready_to_receive | po_not_found | wrong_status | supplier_mismatch | unmatched_po_lines | unmatched_invoice_lines | qty_mismatch | cost_mismatch`) drives the HTTP status (200/404/409/422). Dry-run by default; `confirm=true` only commits when `verdict === "ready_to_receive"` — on success it attaches the invoice image using the `supplier_invoice_<supplier-slug>_<invoice-no>_<YYYY-MM-DD>.<ext>` naming convention (PB appends its random token at write time, matching the operator's existing `supplier_invoice_afriq_ina64721_2026_08_06_…` format on PO26073109STK), then calls `stock_purchases.receive` in the same round-trip. Closes the "AI checks my invoices for the buyer" workflow surfaced 2026-08-09. Implementation: new `src/lib/server/stock-purchase-invoice-matching-service.ts` (+ matching `.test.ts`), wired into `src/app/api/mcp/route.ts` (one new tool entry + one dispatch block + 4 dispatch tests). 1,654/1,657 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in `pocketbase-admin.test.ts`, 5s timeout in `quotes/items/[id]/attachments/upload/route.test.ts` — both reproducible on `main` without this change).
|