WFDASH-WEBAPP/src/app/api/mcp/FEATURE-REQUESTS.md
SJCoetz 60d320411d feat(mcp): add stock purchase invoice reconciliation tool
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.
2026-08-09 16:59:44 +02:00

15 KiB
Raw Blame History

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).