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.
15 KiB
15 KiB
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_itemsbyorder.customer = "<id>", dedupe to distinct orders - Severity: now — every "orders for customer X" question hits this
- Suggested tool: add
customer(id or name) +from/tofilters toorders.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.listwithcreatedfilter, dedupe to distinct orders - Severity: now — every "yesterday / last week / this month" question hits this
- Suggested tool: add
filter(PB syntax) +createdAfter/createdBeforetoorders.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 × quantityacross 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 && updatedon 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) anddelivered_at(0 items/d yesterday) - Suggested tool: once defined, add a
received_atfilter param toorder_items.listand aorders.received_betweenhelper - Schema needs: probably none if we pick an existing field; might need a new
received_atif 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 > Xand 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
skurejected - Workaround: none —
skuis a real PB field onorder_items, but the route.ts whitelist (ORDER_ITEM_PATCH_FIELDS) omitted it. Locally fixed by adding"sku"to the array atroute.ts:145; still pending deploy onwfdash.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 viaquotes_create_from_emailorquotes_create_from_template, (2) add items withquote_items_create(orbulk_update_quote_items), (3) callorders.create_from_quote. There is NOorders.createand NOorder_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 (
categoryis returned on every item +expand=categoryexposes the name/match_keywords). Writes are blocked — same shape as theskuwhitelist gap:QUOTE_ITEM_PATCH_FIELDSandORDER_ITEM_PATCH_FIELDSinroute.tsdon't listcategory, so any patch with it is rejected withforbidden_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.tswhitelist expansion that fixessku(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 inroute.tsso the AI agent doesn't have to memorise the 10 categories. - Schema needs: none
- Open questions: none — same pattern as the
skuentry 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 likepdf-parseorunpdf) and emits a draftstock_purchasefor review. Alternatively,stock_purchases.create_from_email({from, subject, body, ...})mirroringquotes_create_from_email. - Schema needs: none — the
stock_purchase+stock_purchase_itemcollections 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_attachmentsfield tostock_purchases.list(or a separate/api/v1/stock-purchases/attachment-countsendpoint mirroring/api/v1/sprint/attachment-counts), render the sprint-style<Paperclip>chip inStockOrdersTableper 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 separatedriverscollection, 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 callorders.listand 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. Ifdriversisn't a real PB collection, the simplest first cut is virtual (read drivers offusersorsupplierswhere they probably already exist) — but that needs ops to confirm the canonical source. - Schema needs: possibly a new
driverscollection if there isn't one already; otherwise a way to identify the driver relation (drv_idcurrently 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.getalready return full records includingcoordinates(legacy "lat, lng" string) andsupplieraddress - 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.coordinatesis also still the legacy text "lat, lng" string onsuppliers(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 viasuppliers.updateifidis passed. MigratecoordinatestogeoPointwhile we're here so the round trip becomes a single field write. - Schema needs: migrate
suppliers.coordinatesfrom text to PBgeoPoint(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, theorder_items_updatedocstring references fields (collection_date,collected_date,verified_date,delivered_date) that don't exist in the current PB schema (onlydrv_collected_at,delivered_at,assigned_at,drv_assigned_atexist). Either the docstring is stale or those fields were planned and never created. - Currency notes — prices are stored as numbers but the
notesfield 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_attachmentshipped (commitsf89bbb3,dd57d29). Adds a sibling topurchase_orders.additional_attachmentsplus the matching REST upload/delete routes and a UI card on/stock-orders/[id]. PB schema: newstock_purchases.additional_attachmentsfile 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, extendedstock-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_collectedauto-flips the parent PO header toreceivedwhen every line is fully collected. PB schema: newstock_purchase_items.sku/unit/supplier_skutext fields (migrationscripts/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[]}, optionalattachmentbytes) plus aconfirmflag. Deterministic line-by-line matching:stock_item.skuexact →stock_purchase_item.supplier_skuexact → 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=trueonly commits whenverdict === "ready_to_receive"— on success it attaches the invoice image using thesupplier_invoice_<supplier-slug>_<invoice-no>_<YYYY-MM-DD>.<ext>naming convention (PB appends its random token at write time, matching the operator's existingsupplier_invoice_afriq_ina64721_2026_08_06_…format on PO26073109STK), then callsstock_purchases.receivein the same round-trip. Closes the "AI checks my invoices for the buyer" workflow surfaced 2026-08-09. Implementation: newsrc/lib/server/stock-purchase-invoice-matching-service.ts(+ matching.test.ts), wired intosrc/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 inpocketbase-admin.test.ts, 5s timeout inquotes/items/[id]/attachments/upload/route.test.ts— both reproducible onmainwithout this change).