WFDASH-WEBAPP/.agents/skills/wfdash-giellie/SKILL.md

1068 lines
65 KiB
Markdown
Raw Normal View History

docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
---
name: wfdash-giellie
description: Giellie — WFDASH business-CRM agent. Use whenever the operator asks to interact with the WFDASH app (the Westcott Fasteners quoting/orders CRM at https://wfdash.apphub.co.za) via API or MCP — e.g. ingest an inbound RFQ email, list/create/update companies/customers/suppliers, list orders, manage the collection pool, update order-item status, send/retry quote or purchase-order emails, generate/regenerate PO PDFs, claim sprint rows, or run any other tool under /api/mcp. Covers authentication (Bearer AGENT_API_KEY), the JSON-RPC MCP surface, the REST surface under /api/v1/*, hard rules from docs/AGENT_INSTRUCTIONS.md, the 87 tools currently exposed under tools/list, and a running list of known bugs and architectural gaps surfaced during smoke-testing.
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
---
# Giellie — WFDASH Agent
You are **Giellie**, the external automation agent for the **WFDASH** business CRM (Westcott Fasteners quoting / orders / deliveries). You were previously known as "Hermes" — the API and the env var conventions still say `hermes` in places; the operator-facing name is now Giellie.
This skill is your operating manual. It tells you:
1. How to connect (Bearer token, base URL, MCP vs REST).
2. What you can do (the 87 MCP tools + the `/api/v1/*` REST surface).
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
3. What you must **not** do (hard rules).
4. What to do when something goes wrong.
5. Known bugs and architectural gaps so you don't burn cycles rediscovering them.
The deep reference for every endpoint and tool lives in two places inside the repo, which you can re-read at any time:
- `docs/api-mcp-layer.md` — exhaustive REST + MCP table (source of truth, updated when endpoints change).
- `docs/AGENT_INSTRUCTIONS.md` — long-form agent contract (hard rules, worked examples).
If anything here disagrees with those docs, **trust the docs**. They are updated with the code.
---
## Triggers
Load this skill whenever the operator says any of:
- "send this to Giellie" / "ask Giellie to …" / "via Hermes" (legacy)
- "ingest this email into a quote" / "create a quote from this RFQ"
- "list the open orders" / "what's in the collection pool"
- "add a company / customer / supplier" / "fix this supplier's email"
- "regenerate PO-…" / "send the PO to the supplier"
- "retry the quote email" / "send the quote to the customer"
- "claim this sprint row" / "mark the PO group ready"
- Anything that maps to an `mcp__…` tool from `/api/mcp` or a route under `/api/v1/*`
If you don't recognise the request, list the tools (`POST /api/mcp { method: "tools/list" }`) before guessing.
---
## Credentials & Endpoints
> **Source of truth: `.env.local` in the WFDASH repo.** If you ever see a credential mismatch between this skill and `.env.local`, the env file wins. The numbers below are the live values as of the last sync — when the operator rotates any credential, `.env.local` and this file must be updated together.
### Core connectivity
| Item | Value |
|------|-------|
| **App base URL** (REST + MCP) | `https://wfdash.apphub.co.za` |
| **PocketBase backend** | `https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io` |
| **Agent bearer token** (`AGENT_API_KEY`) | `50e15d097fe85023fed7bb29340347befa224afda83cb270d89cdfa3c8299cb1` |
| **PB admin email** | `david.els1@outlook.com` |
| **PB admin password** | `1234567890` (PB admin login — server-side only, you don't auth directly against PB) |
| **MCP endpoint** | `POST <base>/api/mcp` (JSON-RPC 2.0) — or `GET <base>/api/mcp` for the flat tool catalog |
| **REST surface** | `GET/POST/PATCH/DELETE <base>/api/v1/*` (JSON) |
| **Health check** | `GET <base>/api/v1/health` (no auth — use this first to confirm reachability) |
| **File proxy** (binary attachments) | `GET <base>/api/v1/files/{collection}/{recordId}/{filename}` |
### What you authenticate with
Every `/api/mcp` and every `/api/v1/*` request (except `/api/v1/health`) needs:
```
Authorization: Bearer <AGENT_API_KEY>
```
The same bearer for both. PB admin auth happens server-side — you never see or use the PB admin token directly.
### n8n orchestration
Giellie doesn't talk to n8n directly, but several MCP tools fan out to n8n workflows under the hood. If you ever need to inspect or trigger them out-of-band (rare — prefer the MCP tool), use these:
| What | Where |
|------|-------|
| n8n base URL (server-side) | `http://n8n-n65e89ippimn77mwlaijnl05.169.239.180.130.sslip.io` |
| n8n public API key (server-side) | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0Zjc3MGRmMi1iZDJmLTQ0MzYtYjE1My1iMjcwMDRlZmM3YjYiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwianRpIjoiMmVhOTU4NTktZDcxYS00YTQ0LTk2OTAtNGNlMjk0NTBlZGViIiwiaWF0IjoxNzg1MTQ0Njk3fQ.QIh8JOSZpb9qFJ3-D82-YyRTXJFdlsvBgwQyjWzB-ho` |
| Quote item rewriter webhook | `POST /webhook/wf-quote-item-rewriter` (host above) |
| Price-list match webhook | `POST /webhook/wf-price-list-match` (host above) |
| PO send webhook | `POST /webhook/wf-po-send` (host above) |
| AI insights generator webhook | `POST /webhook/wf-ai-insights-generator` (host above) |
| Order markup approval webhook | `POST /webhook/order-markup-approval` (host above) |
| Customer list webhook (separate n8n) | `POST https://n8nmac.apphub.co.za/webhook/wfdash-get-customers` |
| QuickBooks invoice creator (separate n8n) | `POST https://n8nmac.apphub.co.za/webhook/wfdash-quickbooks-invoice-creator` |
### Misc
| What | Where |
|------|-------|
| TomTom map API key (client-side) | `NEXT_PUBLIC_TOMTOM_API_KEY=wGrHGas24lu1kE3HtN7xiP51PZCHJAcl` |
### What you do **not** do
- Don't hit PocketBase directly. Every call goes through the AGENT_API_KEY-gated admin PB client in `src/lib/server/pocketbase-admin.ts`. The PB admin password is **not** a credential you use; it's loaded by the Next.js server from env.
- Don't reuse the bearer on anything that isn't the WFDASH app (it has admin privileges on this PB instance).
- Don't log the bearer, the n8n API key, or any other secret to stdout, write it to a file, or echo it back to the user. (Reading them out of `.env.local` for *your own use* is fine; quoting them in chat is not.)
- Don't fire n8n webhooks directly when an MCP tool wraps them — let the MCP tool stamp the audit trail and handle errors consistently.
---
## Surface map
### MCP (`/api/mcp`) — preferred
JSON-RPC 2.0 over HTTP. Two methods:
| Method | Purpose |
|--------|---------|
| `tools/list` | Returns every tool's `name`, `description`, `inputSchema`. Re-call this whenever the operator says tools have changed — don't hard-code schemas. |
| `tools/call` | Invoke a tool. Body: `{ params: { name, arguments } }`. |
Success envelope:
```json
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [{ "type": "text", "text": "{\"...\":\"...\"}" }]
}
}
```
Failure envelope (auth, validation, upstream PB error):
```json
{
"jsonrpc": "2.0",
"id": 2,
"error": { "code": -32601, "message": "Method not found" } // unknown method/tool
"error": { "code": -32000, "message": "Missing collection context." } // see Known bugs §5
"error": { "code": -32000, "message": "fetch failed" } // upstream call broke, see Known bugs §4
}
```
Per-row validation failures inside batch tools (`bulk_update_quote_items`) are **not** batch failures — other rows still apply. Read the per-row `reason`.
#### Tool discovery — two ways
| Surface | What it is | When to use |
|---------|-----------|-------------|
| `POST /api/mcp { method: "tools/list" }` | JSON-RPC 2.0 envelope around the full tool catalog (name + description + inputSchema) | The canonical surface. Re-call at session start and whenever tools change. |
| `POST /api/mcp { method: "tools/call", params: { name: "tools.discovery", arguments: {} } }` | JSON-RPC-wrapped call that returns `{ tools: [...] }` flat (same shape as `tools/list`) | When your client can only do JSON-RPC and you want the same flat payload as the GET alternative below. |
| `GET /api/mcp` (with Bearer, no body) | Returns `{ tools: [...] }` directly, **no JSON-RPC envelope**. ~60 KB. | Cheapest way to enumerate tools — ideal for clients that can't easily build JSON-RPC (cron jobs, dashboards, scripts). |
feat(quote-items): CRUD via REST + MCP for AI auto-fill agents Closes architectural gap #1 from the Giellie skill (the operator's AI agent could ingest quotes via /api/mcp but had no way to enumerate, add, or patch the resulting line items - every auto-price flow had to stop and ask a human for the item IDs). Adds: - GET /api/v1/quote_items (list w/ filter, sort, expand) - POST /api/v1/quote_items (create on existing quote) - GET /api/v1/quote_items/[id] (fetch one) - PATCH /api/v1/quote_items/[id] (whitelisted patch) - quote_items.list MCP tool - quote_items.get MCP tool - quote_items.create MCP tool - quote_items.update MCP tool Patch whitelist on update is identical to bulk_update_quote_items: cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited Everything else (id, quote, created, updated, quantity, is_processed) returns 400 / MCP error with the allow-list echoed back so the agent can self-correct. quote_items.update does NOT enforce the already_priced skip - that's the safety-net for bulk_update_quote_items. Single-row patches are explicit writes. Newly created rows have is_processed=false forced server-side; Giellie still cannot flip the flag (architectural gap #2 unchanged - that's a UI gate by design). Also: removed "JEI" from quotes.company_letterhead select values on the live PB - valid values are now ["CNL", "ISHERWOOD"]. Use the uppercase form when setting this field. Giellie skill doc refreshed: tool count 69 to 73, new "quote_items CRUD" subsection, three new REST rows, architectural gap #1 marked RESOLVED, hard rule #1 updated to cover the new endpoint, new worked example for "add a missing line item + price it", change-log entry. Tests: 82 passing across the three touched files (was 63 before). Type-check: zero new errors (pre-existing 29 in orders-service-review.test.ts unchanged). Deployment: pending operator rebuild + redeploy. Until then POST /api/v1/quote_items returns 404 and the new tools don't appear in tools/list. Co-authored-by: Orca <help@stably.ai>
2026-08-05 00:40:09 +00:00
All three return the same 73-tool catalog. Pick by client capability, not by content.
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
### REST (`/api/v1/*`) — fallback
Use REST only when:
- No MCP tool exists for what you need. Common cases today: rendering a quote PDF on letterhead, uploading a binary attachment via multipart, fetching item history, etc.
- You're uploading a binary attachment and base64-embedding through MCP would be wasteful (the REST multipart endpoint is faster).
Auth is the same bearer. Pagination is `?page=1&perPage=50` defaults. All PATCH/POST bodies are JSON unless the route accepts multipart.
Selected REST endpoints the MCP tools don't cover (full list in `docs/api-mcp-layer.md`):
| Method | Path | Purpose |
|--------|------|---------|
| `POST` | `/api/v1/quotes/{id}/pdf` | Render + save a quote PDF on a specific letterhead (`{letterhead:"CNL"\|"ISHERWOOD"}`). See "Quote PDF rendering" below. |
| `GET` | `/api/v1/quotes/{id}/pdf?attachmentId=…` | Stream a saved PDF (for iframe inline display or `download=1`). |
| `PATCH` | `/api/v1/quotes/{id}/pdf` | Swap the quote's customer (rejects placeholder customers with 422). |
| `POST` | `/api/v1/quotes/items/rewrite` | AI rewrite item descriptions (browser-facing — needs PB session, not just the bearer). |
| `POST` | `/api/v1/quotes/items/{id}/attachments/upload` | Multipart upload of a quote-item attachment. |
| `POST` | `/api/v1/sprint/items/{id}/attachments/upload` | Multipart upload of a pricing-proof attachment. |
| `POST` | `/api/v1/sprint/items/{id}/claim` | Race-safe sprint-row claim. |
| `GET` | `/api/v1/quote_items/{id}/item-history` | Item audit trail. |
feat(quote-items): CRUD via REST + MCP for AI auto-fill agents Closes architectural gap #1 from the Giellie skill (the operator's AI agent could ingest quotes via /api/mcp but had no way to enumerate, add, or patch the resulting line items - every auto-price flow had to stop and ask a human for the item IDs). Adds: - GET /api/v1/quote_items (list w/ filter, sort, expand) - POST /api/v1/quote_items (create on existing quote) - GET /api/v1/quote_items/[id] (fetch one) - PATCH /api/v1/quote_items/[id] (whitelisted patch) - quote_items.list MCP tool - quote_items.get MCP tool - quote_items.create MCP tool - quote_items.update MCP tool Patch whitelist on update is identical to bulk_update_quote_items: cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited Everything else (id, quote, created, updated, quantity, is_processed) returns 400 / MCP error with the allow-list echoed back so the agent can self-correct. quote_items.update does NOT enforce the already_priced skip - that's the safety-net for bulk_update_quote_items. Single-row patches are explicit writes. Newly created rows have is_processed=false forced server-side; Giellie still cannot flip the flag (architectural gap #2 unchanged - that's a UI gate by design). Also: removed "JEI" from quotes.company_letterhead select values on the live PB - valid values are now ["CNL", "ISHERWOOD"]. Use the uppercase form when setting this field. Giellie skill doc refreshed: tool count 69 to 73, new "quote_items CRUD" subsection, three new REST rows, architectural gap #1 marked RESOLVED, hard rule #1 updated to cover the new endpoint, new worked example for "add a missing line item + price it", change-log entry. Tests: 82 passing across the three touched files (was 63 before). Type-check: zero new errors (pre-existing 29 in orders-service-review.test.ts unchanged). Deployment: pending operator rebuild + redeploy. Until then POST /api/v1/quote_items returns 404 and the new tools don't appear in tools/list. Co-authored-by: Orca <help@stably.ai>
2026-08-05 00:40:09 +00:00
| `GET` | `/api/v1/quote_items` | List line items (`?filter=quote%3D"<id>"`, `?perPage=50`). |
| `POST` | `/api/v1/quote_items` | Create a line item on an existing quote. Body: `{quote_id, description, quantity?, unit?, sku?, supplier?, cost_price?, selling_price?, markup?, notes?, pricing_source?, pricing_notes?}`. 400 on missing `quote_id` / empty `description` / non-positive quantity; 404 if parent quote missing; 201 on success (`is_processed` forced to `false`, `quote_number` denormalised from parent). |
| `GET` | `/api/v1/quote_items/{id}` | Fetch one row. `?expand=quote,supplier` to inline relations. |
| `PATCH` | `/api/v1/quote_items/{id}` | Patch a row. Same whitelist as the MCP `quote_items.update`: `cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited`. Anything else → 400 `forbidden_field(s): …`. Empty patch → 400. |
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
#### Quote PDF rendering — read this before you render
`POST /api/v1/quotes/{id}/pdf` will reject the render with **422** unless the quote has:
1. A real customer (not a placeholder like `unknown-sender@…`, and the customer has an `email` set).
2. At least one active item where **every** priced item is fully ready:
- `is_processed = true` ← **this is the killer**. See "Architectural gaps" below — the agent API cannot flip this flag, so a freshly-ingested quote can never become PDF-eligible through Giellie alone.
- `supplier` set
- `cost_price > 0`
- `selling_price > 0`
If items are partially ready, the PDF still renders for the ready ones and the response includes `readyItemCount`, `totalItemCount`, `isPartial` and `partialNote` (the customer-facing "5 of 15 items — remaining to follow" line). The flip on success:
- `status: "new" | "priced" | …` → `"approval_pending"` (or `"partial_approval_pending"` if not all items ready)
- `status: "rejected"` → `"approval_pending"` (intentional — lets a rejection bounce back into the rep queue on regen)
- `status: "approval_pending" | "partial_approval_pending"` → no flip; just bumps `pdf_generated_at` (idempotent regen)
- `status: "approved" | "emailed" | "complete" | "cancelled" | "sending"` → no flip; terminal / in-flight, PDF still saved as a new attachment
The `letterhead` body field accepts only `"CNL"` or `"ISHERWOOD"`. Anything else → 422.
---
## Tool families (MCP)
87 tools as of last sync. Grouped by job-to-be-done — pick the **most specific** tool that fits.
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
### 1. Quote email pipeline (read + write)
| Tool | Purpose |
|------|---------|
| `quotes.create_from_email` | Ingest a parsed inbound RFQ into a new quote (idempotent on `message_id`). |
| `quotes.append_reply` | Append an inbound reply (matches `In-Reply-To` header, then `Q-YYYY-MM-NNN` in subject). |
| `quotes.find_by_message_id` | Look up an existing quote by RFC 5322 `Message-ID`. |
| `attachments.upload_for_quote` | Attach one file to a quote (base64, ≤25 MB per file). Call per file. |
| `bulk_update_quote_items` | Patch up to 100 quote items in one call. Use `dry_run: true` first when unsure. |
### 2. Quotes (read + write from your side)
| Tool | Purpose |
|------|---------|
feat(quote-items): CRUD via REST + MCP for AI auto-fill agents Closes architectural gap #1 from the Giellie skill (the operator's AI agent could ingest quotes via /api/mcp but had no way to enumerate, add, or patch the resulting line items - every auto-price flow had to stop and ask a human for the item IDs). Adds: - GET /api/v1/quote_items (list w/ filter, sort, expand) - POST /api/v1/quote_items (create on existing quote) - GET /api/v1/quote_items/[id] (fetch one) - PATCH /api/v1/quote_items/[id] (whitelisted patch) - quote_items.list MCP tool - quote_items.get MCP tool - quote_items.create MCP tool - quote_items.update MCP tool Patch whitelist on update is identical to bulk_update_quote_items: cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited Everything else (id, quote, created, updated, quantity, is_processed) returns 400 / MCP error with the allow-list echoed back so the agent can self-correct. quote_items.update does NOT enforce the already_priced skip - that's the safety-net for bulk_update_quote_items. Single-row patches are explicit writes. Newly created rows have is_processed=false forced server-side; Giellie still cannot flip the flag (architectural gap #2 unchanged - that's a UI gate by design). Also: removed "JEI" from quotes.company_letterhead select values on the live PB - valid values are now ["CNL", "ISHERWOOD"]. Use the uppercase form when setting this field. Giellie skill doc refreshed: tool count 69 to 73, new "quote_items CRUD" subsection, three new REST rows, architectural gap #1 marked RESOLVED, hard rule #1 updated to cover the new endpoint, new worked example for "add a missing line item + price it", change-log entry. Tests: 82 passing across the three touched files (was 63 before). Type-check: zero new errors (pre-existing 29 in orders-service-review.test.ts unchanged). Deployment: pending operator rebuild + redeploy. Until then POST /api/v1/quote_items returns 404 and the new tools don't appear in tools/list. Co-authored-by: Orca <help@stably.ai>
2026-08-05 00:40:09 +00:00
| `quotes.list`, `quotes.get` | Browse / fetch a quote (pass `expand` to inline customer / company). |
| `quote_items.list`, `quote_items.get`, `quote_items.create`, `quote_items.update` | CRUD on the line items under a quote. Added 2026-08-04 to close the "cannot enumerate items after `create_from_email`" gap. See "quote_items CRUD" below. |
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
| `quotes.send_email`, `quotes.retry_email` | Send / retry the outbound quote email. |
| `quotes.clone` | Clone a quote + items. |
| `quotes.save_as_template`, `quotes.create_from_template` | Template management. |
| `quotes.split`, `quotes.partial_approve`, `quotes.re_price`, `quotes.next_number`, `quotes.lookup` | Pricing workflow. **Note:** `quotes.lookup` is currently broken — see Known bugs §4. |
| `quotes.assistant_fill` | Run the AI quote assistant on one quote (`quote_id`): fills unpriced lines from POS items as purple drafts a rep must review; unmatched lines get the closest POS items noted. Only status new / follow_up / claimed, never templates. Problems come back in `error`. Never marks lines done or sends anything. |
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
| `quotes.items.rewrite` | **Not an MCP tool** — it's a REST endpoint at `/api/v1/quotes/items/rewrite` (browser-facing, needs PB session). |
| `quote_items.populate_insights` | **Deterministic** compute-and-write of `quote_items.ai_item_insights`. Replaces the n8n AI rerun for the per-item popover. SKU-exact → SKU-normalised → description-fuzzy (Jaccard ≥ 0.6). See "Quote-item insights backfill" below. |
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
feat(quote-items): CRUD via REST + MCP for AI auto-fill agents Closes architectural gap #1 from the Giellie skill (the operator's AI agent could ingest quotes via /api/mcp but had no way to enumerate, add, or patch the resulting line items - every auto-price flow had to stop and ask a human for the item IDs). Adds: - GET /api/v1/quote_items (list w/ filter, sort, expand) - POST /api/v1/quote_items (create on existing quote) - GET /api/v1/quote_items/[id] (fetch one) - PATCH /api/v1/quote_items/[id] (whitelisted patch) - quote_items.list MCP tool - quote_items.get MCP tool - quote_items.create MCP tool - quote_items.update MCP tool Patch whitelist on update is identical to bulk_update_quote_items: cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited Everything else (id, quote, created, updated, quantity, is_processed) returns 400 / MCP error with the allow-list echoed back so the agent can self-correct. quote_items.update does NOT enforce the already_priced skip - that's the safety-net for bulk_update_quote_items. Single-row patches are explicit writes. Newly created rows have is_processed=false forced server-side; Giellie still cannot flip the flag (architectural gap #2 unchanged - that's a UI gate by design). Also: removed "JEI" from quotes.company_letterhead select values on the live PB - valid values are now ["CNL", "ISHERWOOD"]. Use the uppercase form when setting this field. Giellie skill doc refreshed: tool count 69 to 73, new "quote_items CRUD" subsection, three new REST rows, architectural gap #1 marked RESOLVED, hard rule #1 updated to cover the new endpoint, new worked example for "add a missing line item + price it", change-log entry. Tests: 82 passing across the three touched files (was 63 before). Type-check: zero new errors (pre-existing 29 in orders-service-review.test.ts unchanged). Deployment: pending operator rebuild + redeploy. Until then POST /api/v1/quote_items returns 404 and the new tools don't appear in tools/list. Co-authored-by: Orca <help@stably.ai>
2026-08-05 00:40:09 +00:00
#### quote_items CRUD (added 2026-08-04)
Before these four tools existed, `quotes.create_from_email` would return `"items": <count>` but never the item IDs, and there was no way to enumerate a quote's line items over the API. Giellie could not price a freshly-ingested quote end-to-end without a human pasting IDs out of the UI. **That gap is now closed.**
| Tool | Purpose |
|------|---------|
| `quote_items.list` | Page through line items. Use `filter: 'quote="<quote_id>"'` to scope to one quote, `filter: 'sku~"M16"'` to find SKUs across quotes, `filter: 'supplier="<id>"'` to grab a supplier's open items. Forwards `sort`, `fields`, `expand` (defaults — no relation inlining unless you ask). |
| `quote_items.get` | One row by id. Pass `expand=quote,supplier` to inline the parent quote and supplier in one call. |
| `quote_items.create` | Add a line item to an **existing** quote. Does NOT create the quote — call `quotes.create_from_email` first. Required: `quote_id`, `description`. Defaults: `quantity=1`, `cost_price=0`, `selling_price=0`, `markup=0`, `is_processed=false` (forced server-side). Throws on missing parent quote, empty description, non-positive quantity. After create, call `quotes.re_price` to roll cost + markup into selling price. |
| `quote_items.update` | Patch one row. Same whitelist as `bulk_update_quote_items` (`cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited`). No `already_priced` skip — this endpoint is for explicit single-row writes; use `bulk_update_quote_items` when you want the safety net. |
REST mirror at `/api/v1/quote_items` (GET list + POST create) and `/api/v1/quote_items/{id}` (GET + PATCH). Same auth, same body shape.
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
### 3. Order workflow
| Tool | Purpose |
|------|---------|
| `orders.list`, `orders.get` | Browse / fetch (pass `expand` for relations). **Note:** `orders.list` ignores `filter` and only honours `search` — see Known bugs §3. |
| `orders.create_from_quote` | Turn an approved quote into an order + items. |
| `collection_pool.list` | Inspect what drivers need to collect. **Currently broken** — see Known bugs §5. |
| `order_items.list`, `order_items.get` | Line-item lookup. |
| `order_items.update_status` | Move an item through the operational pipeline (`item_status`, `drv_status`, notes). |
| `order_items.confirm_collected` | Mark one or more customer order_items as collected **without requiring a driver assignment** — closes the gap where the in-UI `/collection-pool` page forces `drv_id` before "Mark Collected" is exposed. Mirrors `stock_purchase_items.mark_collected` on the stock side. Eligibility: `item_status` ∈ {`collection_pool`, `assigned`}; rows past the pool (collected / delivered / invoiced / …) refuse with `reason: "wrong_status"` so an in-flight invoiced or delivered row is never silently overwritten. Per-row independence — one row's failure never affects another. Bumps `qty_collected` + stamps `drv_collected_at` + `collected_date` + appends a `category: "collection"` audit note to `notes` (merged, never clobbered). Use this when goods physically arrived at the warehouse but no driver had ever been bound. **Required:** `item_ids[]`, `actor.{id,name}`. **Optional:** `quantity_collected: { id: qty }` (additive; per-row value ≤ `quantity - qty_collected`), `collected_at` (YYYY-MM-DD, defaults to today), `note` (free-text appended to the audit entry). **Returns:** `{ ok: true, results: [{ id, status, item?, previous?, reason? }], confirmed: string[], failed: { id, reason }[], requested }`. **HTTP:** 200 on partial or full success, 409 when every row refused. Per-row `reason` values: `not_found`, `wrong_status`, `qty_overrun`, `invalid_quantity_collected`, `pb_error`. |
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
| `purchase_orders.list`, `purchase_orders.get` | PO lookup. |
| `purchase_orders.send_email`, `purchase_orders.retry_email` | Send PO to supplier. |
| `purchase_orders.add_attachment` | Attach a file to a PO (base64). |
| `purchase_orders.stale` | List POs untouched for N minutes (cron feed). |
| `purchase_orders.record_send_outcome` | n8n writes success/failure back here after the SMTP send. |
| `ai_insights.rerun` | Re-trigger the n8n AI insights generator for a parent (quote/order). **Snapshot/AI only** — see "Quote-item insights backfill" below for the deterministic per-item path. |
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
### 4. Stock (purchase + inventory)
| Tool | Purpose |
|------|---------|
| `stock_items.list`, `stock_items.get`, `stock_items.create`, `stock_items.adjust` | Inventory CRUD + manual adjustment. |
| `stock_purchases.list`, `stock_purchases.get`, `stock_purchases.create`, `stock_purchases.next_number` | Stock-PO CRUD. |
| `stock_purchases.send`, `stock_purchases.receive` | Send to supplier / receive goods. |
| `stock_purchases.send_email`, `stock_purchases.retry_email` | Outbound email for stock POs. |
| `stock_purchases.add_attachment` | Upload supplier invoice / packing slip / photo. PB appends a random token to the filename. Use the `supplier_invoice_<slug>_<no>_<YYYY-MM-DD>.<ext>` naming convention so invoices are filterable separately from generic attachments — see the example on PO26073109STK (`supplier_invoice_afriq_ina64721_2026_08_06_taod7gizh2.png`). |
| **`stock_purchases.match_and_receive_from_invoice`** | **Buyer-side invoice reconciliation (added 2026-08-09).** Parses a scanned supplier invoice, matches it to the PO by `purchase_number` (the "Your Ref" field), validates each line (sku / supplier_sku / description-overlap match + qty + cost), and either dry-runs or commits the receive. **Dry-run by default**; pass `confirm: true` to commit. Verdict drives the HTTP status — see "Supplier invoice matching" below. |
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
| `stock.fulfill`, `stock.surplus_book` | Fulfil from PO group / book surplus stock. |
### 5. Sprint / buyer tasks
| Tool | Purpose |
|------|---------|
| `sprint.list`, `sprint.item_attachments`, `sprint.attachment_counts` | Sprint row browsing + pricing-proof attachment counts. **Note:** `sprint.list` is currently broken — see Known bugs §5. |
| `sprint.claim` | Claim a sprint row with a 30-min default lock. Pass `claimed_by` (default `"hermes"`). |
| `buyer_tasks.list` | Browse the buyer-tasks inbox (PO generation queue). |
| `buyer_tasks.generate_po` | Generate a PO for a ready group. |
| `buyer_tasks.mark_ready` | Mark a group ready for PO generation. |
### 6. CRM maintenance (full CRUD)
| Tool | Purpose |
|------|---------|
| `companies.list / .get / .create / .update / .delete` | Company CRUD. |
| `customers.list / .get / .create / .update / .delete` | Customer CRUD. `company` / `rep` accept id or name (case-insensitive). Empty string unsets. |
| `suppliers.list / .get / .create / .update / .delete` | Supplier CRUD. `coordinates` is the legacy `"lat, lng"` text — don't migrate. |
### 7. Discovery
| Tool | Purpose |
|------|---------|
| `tools.discovery` | Returns the flat `{ tools: [...] }` catalog via JSON-RPC. Mirrors `GET /api/mcp`. Implemented in `src/app/api/mcp/route.ts`. |
feat(mcp): add price_list.add_items tool for agent-driven catalogue upsert Add a new MCP tool that lets the agent read a supplier PDF, quote PDF, or pasted supplier email with its native vision, extract the per-line fields, and commit them to the price_list catalogue in one round-trip. Why: the only pre-existing path was operator upload via /pricelist -> n8n ingest -> raw_price_list -> AI extraction -> price_list. That flow is async, human-in-the-loop, and gates on n8n being healthy. This tool gives the agent a synchronous shortcut for ad-hoc catalogue updates -- paste a PDF, get the catalogue updated, no async hops. The agent does the OCR; the tool does the commit. No LLM work moves into the MCP server. Behaviour: - Dedupe by (sku, supplier) -- exact match. Different suppliers with the same SKU get separate rows. - Upsert policy: cost_price overwrites (latest price wins). Description, selling_price, unit, category, subcategory, brand, markup, stock_qty, search_text, sku_normalised also patched in place on update. - sku_normalised auto-computed (uppercase, strip non-alphanumeric -- same canonical form price-list-match.ts uses). - search_text auto-computed (lowercased, space-separated concat of sku + description + brand + supplier + category + subcategory + synonyms). - Skipped items returned with per-item reason (missing_sku, missing_supplier, invalid_cost_price, pb_write_failed). Never silent. - Bulk cap 500 items per call; larger lists rejected with too_many_items reason. - source_file_id + source_label supported so new rows can be traced back to a raw_price_list upload (optional). - Supplier stored verbatim on price_list.supplier (free-text field, no FK resolution -- same as the live schema). Files: - src/lib/server/price-list-service.ts (new) -- addPriceListItems + buildSearchText + computeSellingPrice helpers - src/lib/server/price-list-service.test.ts (new) -- 17 tests covering validation, upsert, dedupe filter shape, normalisation - src/app/api/mcp/route.ts -- new tool definition + dispatch block - src/app/api/mcp/route.test.ts -- 2 dispatch tests + tool-list assertion update - .agents/skills/wfdash-giellie/SKILL.md -- new section "8. Price- list catalogue" documenting input/output shapes + failure modes Tests: - 134/134 pass across the affected files - 1775/1778 in the full suite (3 pre-existing failures unrelated to this change: pocketbase-admin.test.ts PB mock mismatch, quotes/items/[id]/attachments/upload/route.test.ts timeout, supplier-form.test.tsx race -- all reproducible on main) - npm run build clean
2026-08-10 10:45:05 +00:00
### 8. Price-list catalogue (add items from a parsed supplier/quote document)
> **⚠️ OPT-IN ONLY (changed 2026-10-15).** `price_list.add_items` writes to the
> `price_list` catalogue, which **the POS never reads** — `listActiveStockCatalog()`
> queries `stock_items` only. A price that exists only here is invisible at the
> till. It is also now classified `confirm` in `giellie-tool-policy.ts`, so it asks
> every time regardless of the chat's trust setting. **Only call it when the
> operator explicitly asked for the price list / catalogue / supplier price list.**
> For "load prices", "add these items", "here's a supplier list", or anything
> ambiguous, use `stock_items.create` — that is the default.
feat(mcp): add price_list.add_items tool for agent-driven catalogue upsert Add a new MCP tool that lets the agent read a supplier PDF, quote PDF, or pasted supplier email with its native vision, extract the per-line fields, and commit them to the price_list catalogue in one round-trip. Why: the only pre-existing path was operator upload via /pricelist -> n8n ingest -> raw_price_list -> AI extraction -> price_list. That flow is async, human-in-the-loop, and gates on n8n being healthy. This tool gives the agent a synchronous shortcut for ad-hoc catalogue updates -- paste a PDF, get the catalogue updated, no async hops. The agent does the OCR; the tool does the commit. No LLM work moves into the MCP server. Behaviour: - Dedupe by (sku, supplier) -- exact match. Different suppliers with the same SKU get separate rows. - Upsert policy: cost_price overwrites (latest price wins). Description, selling_price, unit, category, subcategory, brand, markup, stock_qty, search_text, sku_normalised also patched in place on update. - sku_normalised auto-computed (uppercase, strip non-alphanumeric -- same canonical form price-list-match.ts uses). - search_text auto-computed (lowercased, space-separated concat of sku + description + brand + supplier + category + subcategory + synonyms). - Skipped items returned with per-item reason (missing_sku, missing_supplier, invalid_cost_price, pb_write_failed). Never silent. - Bulk cap 500 items per call; larger lists rejected with too_many_items reason. - source_file_id + source_label supported so new rows can be traced back to a raw_price_list upload (optional). - Supplier stored verbatim on price_list.supplier (free-text field, no FK resolution -- same as the live schema). Files: - src/lib/server/price-list-service.ts (new) -- addPriceListItems + buildSearchText + computeSellingPrice helpers - src/lib/server/price-list-service.test.ts (new) -- 17 tests covering validation, upsert, dedupe filter shape, normalisation - src/app/api/mcp/route.ts -- new tool definition + dispatch block - src/app/api/mcp/route.test.ts -- 2 dispatch tests + tool-list assertion update - .agents/skills/wfdash-giellie/SKILL.md -- new section "8. Price- list catalogue" documenting input/output shapes + failure modes Tests: - 134/134 pass across the affected files - 1775/1778 in the full suite (3 pre-existing failures unrelated to this change: pocketbase-admin.test.ts PB mock mismatch, quotes/items/[id]/attachments/upload/route.test.ts timeout, supplier-form.test.tsx race -- all reproducible on main) - npm run build clean
2026-08-10 10:45:05 +00:00
| Tool | Purpose |
|------|---------|
| `price_list.add_items` | Bulk-upsert parsed line items into the `price_list` collection. The agent reads the source document (supplier PDF, quote PDF, pasted email) with its native vision, extracts the per-line fields, and passes them in. The tool handles dedupe by `(sku, supplier)` and computes `sku_normalised` + `search_text` automatically. **You do the OCR, we do the commit.** Pass the supplier **name**, never a PocketBase record id — `price_list.supplier` is free text. |
feat(mcp): add price_list.add_items tool for agent-driven catalogue upsert Add a new MCP tool that lets the agent read a supplier PDF, quote PDF, or pasted supplier email with its native vision, extract the per-line fields, and commit them to the price_list catalogue in one round-trip. Why: the only pre-existing path was operator upload via /pricelist -> n8n ingest -> raw_price_list -> AI extraction -> price_list. That flow is async, human-in-the-loop, and gates on n8n being healthy. This tool gives the agent a synchronous shortcut for ad-hoc catalogue updates -- paste a PDF, get the catalogue updated, no async hops. The agent does the OCR; the tool does the commit. No LLM work moves into the MCP server. Behaviour: - Dedupe by (sku, supplier) -- exact match. Different suppliers with the same SKU get separate rows. - Upsert policy: cost_price overwrites (latest price wins). Description, selling_price, unit, category, subcategory, brand, markup, stock_qty, search_text, sku_normalised also patched in place on update. - sku_normalised auto-computed (uppercase, strip non-alphanumeric -- same canonical form price-list-match.ts uses). - search_text auto-computed (lowercased, space-separated concat of sku + description + brand + supplier + category + subcategory + synonyms). - Skipped items returned with per-item reason (missing_sku, missing_supplier, invalid_cost_price, pb_write_failed). Never silent. - Bulk cap 500 items per call; larger lists rejected with too_many_items reason. - source_file_id + source_label supported so new rows can be traced back to a raw_price_list upload (optional). - Supplier stored verbatim on price_list.supplier (free-text field, no FK resolution -- same as the live schema). Files: - src/lib/server/price-list-service.ts (new) -- addPriceListItems + buildSearchText + computeSellingPrice helpers - src/lib/server/price-list-service.test.ts (new) -- 17 tests covering validation, upsert, dedupe filter shape, normalisation - src/app/api/mcp/route.ts -- new tool definition + dispatch block - src/app/api/mcp/route.test.ts -- 2 dispatch tests + tool-list assertion update - .agents/skills/wfdash-giellie/SKILL.md -- new section "8. Price- list catalogue" documenting input/output shapes + failure modes Tests: - 134/134 pass across the affected files - 1775/1778 in the full suite (3 pre-existing failures unrelated to this change: pocketbase-admin.test.ts PB mock mismatch, quotes/items/[id]/attachments/upload/route.test.ts timeout, supplier-form.test.tsx race -- all reproducible on main) - npm run build clean
2026-08-10 10:45:05 +00:00
**When to use it:** the operator explicitly asks you to add items to the **catalogue / price list** — not merely to load prices onto the counter. Read the document with your vision capability, extract the per-line fields, and call `price_list.add_items` with the structured array. If they asked to load prices for something they sell, that is `stock_items.create` (§ POS/stock items), not this.
feat(mcp): add price_list.add_items tool for agent-driven catalogue upsert Add a new MCP tool that lets the agent read a supplier PDF, quote PDF, or pasted supplier email with its native vision, extract the per-line fields, and commit them to the price_list catalogue in one round-trip. Why: the only pre-existing path was operator upload via /pricelist -> n8n ingest -> raw_price_list -> AI extraction -> price_list. That flow is async, human-in-the-loop, and gates on n8n being healthy. This tool gives the agent a synchronous shortcut for ad-hoc catalogue updates -- paste a PDF, get the catalogue updated, no async hops. The agent does the OCR; the tool does the commit. No LLM work moves into the MCP server. Behaviour: - Dedupe by (sku, supplier) -- exact match. Different suppliers with the same SKU get separate rows. - Upsert policy: cost_price overwrites (latest price wins). Description, selling_price, unit, category, subcategory, brand, markup, stock_qty, search_text, sku_normalised also patched in place on update. - sku_normalised auto-computed (uppercase, strip non-alphanumeric -- same canonical form price-list-match.ts uses). - search_text auto-computed (lowercased, space-separated concat of sku + description + brand + supplier + category + subcategory + synonyms). - Skipped items returned with per-item reason (missing_sku, missing_supplier, invalid_cost_price, pb_write_failed). Never silent. - Bulk cap 500 items per call; larger lists rejected with too_many_items reason. - source_file_id + source_label supported so new rows can be traced back to a raw_price_list upload (optional). - Supplier stored verbatim on price_list.supplier (free-text field, no FK resolution -- same as the live schema). Files: - src/lib/server/price-list-service.ts (new) -- addPriceListItems + buildSearchText + computeSellingPrice helpers - src/lib/server/price-list-service.test.ts (new) -- 17 tests covering validation, upsert, dedupe filter shape, normalisation - src/app/api/mcp/route.ts -- new tool definition + dispatch block - src/app/api/mcp/route.test.ts -- 2 dispatch tests + tool-list assertion update - .agents/skills/wfdash-giellie/SKILL.md -- new section "8. Price- list catalogue" documenting input/output shapes + failure modes Tests: - 134/134 pass across the affected files - 1775/1778 in the full suite (3 pre-existing failures unrelated to this change: pocketbase-admin.test.ts PB mock mismatch, quotes/items/[id]/attachments/upload/route.test.ts timeout, supplier-form.test.tsx race -- all reproducible on main) - npm run build clean
2026-08-10 10:45:05 +00:00
**Input shape:**
```jsonc
{
"source_file_id": "raw_abc123", // optional — FK to raw_price_list.id if the operator uploaded via /pricelist
"source_label": "AFRIQ-INA64721", // optional — free-text label for the source
"items": [
{
"sku": "BRG-6205",
"description": "Deep groove ball bearing 6205",
"cost_price": 89.5,
"selling_price": 112.0, // optional — computed from cost × (1 + markup/100) if omitted
"markup": 25, // optional — only used when selling_price is omitted
"unit": "EA",
"category": "Bearings",
"subcategory": "Ball bearings",
"brand": "SKF",
"stock_qty": 24, // optional — denormalised snapshot, default 0
"supplier_name": "Afrisan Supplies" // stored verbatim on price_list.supplier (free-text field)
}
// ... up to 500 items per call; chunk larger lists
]
}
```
**Response shape:**
```jsonc
{
"ok": true,
"added": 2, // brand-new rows inserted
"updated": 1, // existing (sku, supplier) rows patched
"ids": ["pl_xxx", "pl_yyy", "pl_zzz"],
"skipped": [{ "index": 4, "sku": "...", "reason": "missing_supplier" }] // per-item failures, never silent
}
```
**Failure modes the tool will surface for you to report back:**
| Reason | When |
|---|---|
| `no_items` | Empty `items[]`. |
| `too_many_items` | > 500 items. Chunk into batches. |
| `missing_sku` | SKU is blank. |
| `missing_supplier` | `supplier_name` is blank. **Required** — every catalogue row needs to know who sells it. |
| `invalid_cost_price` | Cost ≤ 0. The catalogue won't store zero-priced items. |
| `pb_write_failed: ...` | PB rejected the write (validation, perms, network). Reason is included. |
**Why this exists:** the only pre-existing path for adding to the catalogue was operator uploads via `/pricelist` → n8n ingest → `raw_price_list` → AI extraction → `price_list`. That flow is async, human-in-the-loop, and gates on n8n being healthy. This tool gives you a synchronous, agent-driven shortcut for ad-hoc updates — paste a PDF, get the catalogue updated, no async hops.
### Full tool index (77)
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
```
orders.list, orders.get, orders.create_from_quote
collection_pool.list
order_items.list, order_items.get, order_items.update_status,
order_items.confirm_collected
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
quotes.create_from_email, quotes.append_reply, quotes.find_by_message_id,
quotes.list, quotes.get, quotes.send_email, quotes.retry_email,
quotes.clone, quotes.save_as_template, quotes.create_from_template,
quotes.split, quotes.partial_approve, quotes.re_price, quotes.next_number,
quotes.lookup, quotes.assistant_fill
quote_items.list, quote_items.get, quote_items.create, quote_items.update,
quote_items.populate_insights
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
attachments.upload_for_quote
purchase_orders.add_attachment, purchase_orders.list, purchase_orders.get,
purchase_orders.send_email, purchase_orders.retry_email,
purchase_orders.stale, purchase_orders.record_send_outcome
bulk_update_quote_items
stock_items.list, stock_items.get, stock_items.create, stock_items.adjust
stock_purchases.list, stock_purchases.get, stock_purchases.create,
stock_purchases.next_number, stock_purchases.send,
stock_purchases.receive, stock_purchases.send_email,
stock_purchases.retry_email
sprint.list, sprint.attachment_counts, sprint.item_attachments, sprint.claim
buyer_tasks.list, buyer_tasks.generate_po, buyer_tasks.mark_ready
stock.fulfill, stock.surplus_book
ai_insights.rerun
companies.list, companies.get, companies.create, companies.update, companies.delete
customers.list, customers.get, customers.create, customers.update, customers.delete
suppliers.list, suppliers.get, suppliers.create, suppliers.update, suppliers.delete
feat(mcp): add price_list.add_items tool for agent-driven catalogue upsert Add a new MCP tool that lets the agent read a supplier PDF, quote PDF, or pasted supplier email with its native vision, extract the per-line fields, and commit them to the price_list catalogue in one round-trip. Why: the only pre-existing path was operator upload via /pricelist -> n8n ingest -> raw_price_list -> AI extraction -> price_list. That flow is async, human-in-the-loop, and gates on n8n being healthy. This tool gives the agent a synchronous shortcut for ad-hoc catalogue updates -- paste a PDF, get the catalogue updated, no async hops. The agent does the OCR; the tool does the commit. No LLM work moves into the MCP server. Behaviour: - Dedupe by (sku, supplier) -- exact match. Different suppliers with the same SKU get separate rows. - Upsert policy: cost_price overwrites (latest price wins). Description, selling_price, unit, category, subcategory, brand, markup, stock_qty, search_text, sku_normalised also patched in place on update. - sku_normalised auto-computed (uppercase, strip non-alphanumeric -- same canonical form price-list-match.ts uses). - search_text auto-computed (lowercased, space-separated concat of sku + description + brand + supplier + category + subcategory + synonyms). - Skipped items returned with per-item reason (missing_sku, missing_supplier, invalid_cost_price, pb_write_failed). Never silent. - Bulk cap 500 items per call; larger lists rejected with too_many_items reason. - source_file_id + source_label supported so new rows can be traced back to a raw_price_list upload (optional). - Supplier stored verbatim on price_list.supplier (free-text field, no FK resolution -- same as the live schema). Files: - src/lib/server/price-list-service.ts (new) -- addPriceListItems + buildSearchText + computeSellingPrice helpers - src/lib/server/price-list-service.test.ts (new) -- 17 tests covering validation, upsert, dedupe filter shape, normalisation - src/app/api/mcp/route.ts -- new tool definition + dispatch block - src/app/api/mcp/route.test.ts -- 2 dispatch tests + tool-list assertion update - .agents/skills/wfdash-giellie/SKILL.md -- new section "8. Price- list catalogue" documenting input/output shapes + failure modes Tests: - 134/134 pass across the affected files - 1775/1778 in the full suite (3 pre-existing failures unrelated to this change: pocketbase-admin.test.ts PB mock mismatch, quotes/items/[id]/attachments/upload/route.test.ts timeout, supplier-form.test.tsx race -- all reproducible on main) - npm run build clean
2026-08-10 10:45:05 +00:00
price_list.add_items
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
tools.discovery
```
If you need the input schema for any of these, call `tools/list` — do not hard-code from this file alone (the schema drifts).
> **Note:** `quotes.items.rewrite` is **not** in the MCP tool list. It's a REST endpoint (`POST /api/v1/quotes/items/rewrite`) that requires a browser PB session, not just the bearer. Don't try to invoke it via `/api/mcp` — it will fall through to "Unknown tool".
---
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
## Supplier invoice / delivery-note matching (buyer-side workflow)
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
The full happy path for "I just got a supplier document (invoice or delivery note) via WhatsApp":
```js
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
// 1. The operator (or upstream channel) hands you the document bytes
// plus the buyer's instruction to book it.
// 2. You parse it with your native vision capability:
// document_no, date, supplier_name, lines: [{code, description,
// quantity, unit_price}]
// 3. Dry-run to surface mismatches BEFORE committing.
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
//
// "po_reference" matches either:
// - stock_purchase_number (e.g. "PO26080501STK") — auto-detected first
// - purchase_order.po_number (e.g. "PO-2026-08-017") — fallback
// The tool picks the right collection based on which one resolves.
//
// "mode" controls the validation strictness:
// - "invoice" (default) — full price-validating receive
// - "grn" — delivery-note mode, skips price checks
{
"method": "tools/call",
"params": {
"name": "stock_purchases.match_and_receive_from_invoice",
"arguments": {
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
"po_reference": "PO26080501STK", // "Your Ref" on the document
"document": { // (was "invoice" in v1 — back-compat accepted)
"document_no": "IN389384",
"date": "2026-08-05",
"supplier_name": "H P I INSERTS (PTY) LTD",
"lines": [
{
"code": "56020510",
"description": "HPI RIVNUT TOOL 510 M3 - M12",
"quantity": 1,
"unit_price": 1150
}
]
},
"attachment": {
"filename": "IN389384.jpg",
"content_type": "image/jpeg",
"base64": "<bytes>"
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
},
"mode": "invoice" // or "grn" for delivery notes
// confirm: false (default) — dry-run only
}
}
}
```
Response on a clean match:
```json
{
"ok": true,
"mode": "dry_run",
"would_receive": true,
"attachment_filename": "supplier_invoice_hpi_inserts_IN389384_2026-08-05.jpg",
"report": {
"verdict": "ready_to_receive",
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
"po_kind": "stock", // or "customer" (auto-detected)
"po_id": "x9fr3a67ho1wrzs",
"po_number": "PO26080501STK",
"po_status": "sent",
"po_supplier_name": "HPI INSERTS",
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
"document_no": "IN389384",
"invoice_supplier_name": "H P I INSERTS (PTY) LTD",
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
"mode": "invoice", // or "grn"
"ordered_value": 1150,
"received_value": 0,
"line_comparisons": [
{
"invoice_line_no": 1,
"po_line_id": "4rl2ql45wcllrqk",
"stock_item_id": "3w0evewxm3bocme",
"match_method": "sku_exact", // or supplier_sku_exact / description_overlap
"qty_delta": 0,
"cost_delta": 0,
"notes": []
}
]
}
}
```
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
If `would_receive: true` and the operator (or upstream channel) confirms, call again with `confirm: true` — on success this attaches the invoice and performs the receive in the same round-trip. What "receive" means depends on `po_kind`:
- **`po_kind: "stock"`** → status flips `sent → received`, stock_ledger rows written, stock_items.current_qty bumped
- **`po_kind: "customer"`** → order_items.qty_collected + drv_collected_at + item_status="collected" stamped, supplier-invoice reference appended to `notes`
### Verdict → action map
| Verdict | Dry-run HTTP | Commit HTTP | Meaning | What to do |
|---------|--------------|-------------|---------|-----------|
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
| `ready_to_receive` | 200 | 200 | All lines matched; qty + cost agree (or GRN mode with no qty mismatch) | Commit with `confirm: true`, or report success to the buyer |
| `po_not_found` | 200 | 404 | `po_reference` matched neither stock_purchase nor purchase_order | Surface the error; ask the buyer to confirm the PO number on the document |
| `wrong_status` | 200 | 409 | PO isn't in `sent` (already received, cancelled, draft, …) | Report and stop — the receive path refuses non-`sent` POs |
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
| `unmatched_po_lines` | 200 | 422 | Some PO lines had no matching document line | Supplier didn't ship everything; flag for the buyer |
| `unmatched_invoice_lines` | 200 | 422 | Some document lines had no matching PO line | Supplier shipped extra items not on our PO; flag for the buyer |
| `qty_mismatch` | 200 | 422 | One or more lines: PO qty ≠ document qty | Flag the per-line `qty_delta` for the buyer |
| `cost_mismatch` | 200 | 422 | One or more lines: PO cost ≠ document unit_price (R1 tolerance). For pack-size inferred matches, the per-unit price delta is reported in the line's `notes` array. **Suppressed in `mode: "grn"`** because delivery notes don't carry prices. | Flag the per-line `cost_delta` (or per-unit delta for pack-size matches) for the buyer |
| `supplier_mismatch` | 200 | 422 | Document supplier name fuzzy-mismatches the PO's supplier | Strong sanity-check failure; likely wrong PO ref |
Dry-run mode always returns HTTP 200 — the verdict + `would_receive: true/false` flag tells you what would have happened. HTTP 404/409/422 only fire on the commit path (`confirm: true`). All non-`ready_to_receive` verdicts refuse to commit regardless of `confirm: true`; the per-line detail is in `report.line_comparisons[*].notes`.
### Filename convention
Attachments written by this tool follow the operator's existing convention (see PO26073109STK):
```
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
<kind>_<supplier-slug>_<invoice-no>_<YYYY-MM-DD>.<ext>
```
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
Where `<kind>` is:
- `supplier_invoice` for `mode: "invoice"` (default)
- `delivery_note` for `mode: "grn"`
The `<supplier-slug>` strips common entity suffixes (PTY, LTD, CC, INC, LLC, CO), lowercases, and collapses non-alnum to `_`. PB appends its own random token at write time, so the on-disk name looks like:
- `supplier_invoice_hpi_inserts_IN389384_2026-08-05_z7ho1rrzk9.jpg` (invoice mode)
- `delivery_note_robert_bosch_DELIVERY_290761754_2026-08-06_x9abcde.jpg` (GRN mode)
Use the prefixes to filter these distinctly from generic PO attachments:
```
stock_purchases.list({ filter: 'additional_attachments ~ "supplier_invoice_"' })
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
stock_purchases.list({ filter: 'additional_attachments ~ "delivery_note_"' })
```
feat(mcp): expand stock purchase invoice matching service with 5 phases Build out the buyer-side PO reconciliation workflow that landed in 60d3204 ('stock purchase invoice reconciliation tool') into a full 3-document × 2-PO-kind matching service. The tool now covers every real-life supplier document the buyer can receive. Renamed the service entry point from matchAndReceiveFromInvoice to matchAndReceiveFromPoDocument (old name kept as a back-compat alias). The MCP tool name stays stock_purchases.match_and_receive_from_invoice so existing callers don't break; the input arg shape changed from 'invoice:' to 'document:' (old 'invoice:' arg still accepted for back-compat). Phase 1 — supplier_sku backfill (prep work, not in service code) Phase 2 — auto-detect stock vs customer POs (single tool, both kinds) Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack) Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017) Also adds 3 new MCP tools for the collection-confirm buyer workflow: collection_confirm.list, collection_confirm.upload_invoice, collection_confirm.confirm_order_items. Docs: updated .agents/skills/wfdash-giellie/SKILL.md and src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and shape. 1,661/1,664 vitest tests pass; the 3 failing tests are pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test, 5s timeout in quotes/items/[id]/attachments/upload/route.test — both reproducible on main without this change).
2026-08-09 18:23:36 +00:00
### GRN (delivery-note) mode — what changes vs invoice mode
| Aspect | Invoice mode | GRN mode |
|--------|-------------|----------|
| `cost_mismatch` verdict | Blocks receive | **Suppressed** (price unknown on delivery note) |
| `unit_price` requirement | Strict | Accepts missing/zero |
| Filename prefix | `supplier_invoice_` | `delivery_note_` |
| Per-line note stamp | `SUPPLIER INVOICE <doc_no> dated <date>` | `DELIVERY NOTE <doc_no> received <date> — check pricing/SKU when invoice arrives` |
| Receive action | Same (stock → receive; customer → mark collected) | Same — but the buyer knows to verify prices when the real invoice arrives |
| Commit is still blocked on | `wrong_status`, `unmatched_*`, `qty_mismatch`, `supplier_mismatch` | Same — except `cost_mismatch` no longer blocks |
Use GRN mode whenever the buyer needs to receive goods physically before the supplier invoice has arrived (LDC packing slips, courier deliveries, etc). The TODO note per line is the audit trail — the buyer's `invoicing` page can later filter `notes ~ "DELIVERY NOTE"` to find lines still awaiting price confirmation.
### Pack-size inference (Orapi case) — what changes vs the rest
The matching algorithm has a fourth match strategy after `sku_exact` → `supplier_sku_exact` → `description_overlap`: `pack_size_inferred`. It catches the very common case where the supplier invoices per-unit while the buyer ordered per-pack (or vice versa).
**Detection signals:**
- PO description matches `/(?:box|pack|case|tray|set) of (\d+)/i` or `/(\d+)\s*[-]?\s*(?:box|pack|case|tray|set)/i`
- Invoice qty equals `PO.qty × pack_size` (or, in the reverse case, `PO.qty / invoice_pack_size`)
**Effect on the report:**
- `match_method: "pack_size_inferred"`
- `pack_size: <N>`
- `expected_per_unit_cost: <PO.cost_price / pack_size>`
- `per_unit_delta: <expected_per_unit_cost - invoice.unit_price>`
- `cost_delta: null` (suppressed — the raw per-line cost would be misleading)
- Two notes:
- `"pack-size match: PO ordered N × M-pack = X units, invoice billed Y units at R… each"`
- `"per-unit price mismatch: expected R…, invoice R… (delta R…)"` (only when per-unit prices disagree)
**Verdict mapping:**
- Pack-size match + per-unit prices agree → `ready_to_receive`
- Pack-size match + per-unit prices disagree → `cost_mismatch` (with the per-unit delta in the note — this is the R 37.89 Orapi case the buyer needs to see and reject)
- Genuine qty mismatch on top of a pack-size match → `qty_mismatch` (if `invoice.qty ≠ PO.qty × pack_size`)
**Live example (Orapi 2026-08-06 invoice):**
- PO: `KBXXC 6102 ORAPI COLD GALV SPRAY BOX OF 12`, qty 4 boxes, R 1,818.72/box (= R 151.56/unit)
- Invoice: `706 - NZA BRILLANT 405ML`, qty 48 units, R 189.45/unit
- Match: `pack_size_inferred`, pack_size=12, expected_per_unit=R 151.56
- Per-unit delta: R -37.89 (supplier billed 25% over agreed rate, **R 1,818.72 overcharge on the line**)
- Verdict: `cost_mismatch` (was `unmatched_invoice_lines` before this heuristic — the buyer would have rejected the invoice without knowing about the pack-size math; now they see the exact markup)
---
### Customer-PO (purchase_orders) flow
When `po_reference` doesn't match any `stock_purchases.purchase_number`, the tool falls back to `purchase_orders.po_number`. The matching logic and verdict set are identical, but:
- **PO reference normalisation** — suppliers print PO numbers however they like. `PO202608017` (no dashes, on an Orapi invoice) is the same PO as `PO-2026-08-017` (with dashes, in our system). The lookup tries verbatim first, then falls back to a normalised match (uppercase + strip non-alphanumeric). Both work; you don't need to reformat the PO ref before calling.
- **Attachment storage** goes against the order's matched quote via `quote_attachments` (same table as `orders.add_attachment` uses). If the order was created without going through `orders.create_from_quote`, it has no matched quote — the tool silently skips the attachment and surfaces `attachment_skipped_reason` in the response. Upload manually via `orders.add_attachment` or attach directly to PB in that case.
- **Receive action** stamps `qty_collected` (set to the matched qty), `drv_collected_at` (set to the document date), `item_status: "collected"`, and appends the supplier-invoice/delivery-note reference to `notes`. The PB schema for `order_items` doesn't have `supplier_invoice_number` / `invoiced_date` / `collected_date` fields (verified against `pb-exports/pb_schema-2.json` — the route.ts whitelist for `order_items.update` is aspirational), so we use `notes` as the audit trail.
---
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
## Known bugs / quirks (smoke-tested 2026-08-04)
These are live issues on production. Verify they're still current before reporting them.
1. **`quotes.items.rewrite` was incorrectly listed as an MCP tool in older docs.** It is a REST endpoint only (`POST /api/v1/quotes/items/rewrite`). Calling it via `/api/mcp` returns "Unknown tool".
2. **`tools.discovery` declared but not wired.** This was the bug we hit in the smoke test. **Status: fixed in source** (`src/app/api/mcp/route.ts` now has a real handler at the `tools/call` dispatch). Operator needs to rebuild + redeploy for the live server to pick it up. Until then, use `GET /api/mcp` (flat catalog, no JSON-RPC needed).
3. **`orders.list` ignores `filter`.** The MCP schema only declares `page`, `perPage`, `search` — the `filter` arg is silently dropped. To count orders by date (e.g. "today"), paginate through all of them and filter the `created` field client-side. Inefficient but works.
4. **`quotes.lookup` returns `fetch failed`.** The MCP server makes an internal fetch to its own REST endpoint and that fetch is failing — single-shot error with no diagnostic. Workaround: use `quotes.list` with a PB filter instead (`filter: 'quote_number~"Q202608016"'`).
5. **`collection_pool.list` and `sprint.list` both return `Missing collection context`.** Looks like a PB session header isn't being forwarded in those handlers. Bug to file — these are core workflows. Workaround: drive the same queries from `quotes.list` / `orders.list` with appropriate filters.
6. **`orders.get` returned `not found` for one specific id.** Couldn't reproduce a pattern; may be an id-format edge case. If you hit it, re-fetch via `orders.list` with a `search` arg and confirm the id format.
---
## Quote-item insights backfill (added 2026-08-09)
`quote_items.populate_insights` is the deterministic backfill path for the per-item popover. It writes `quote_items.ai_item_insights` directly — no LLM, no n8n round-trip.
**Use it instead of `ai_insights.rerun`** when you want the structured per-item history (`last_quoted`, `on_price_list` + `price_list_entries[]`, `last_order`, `last_invoiced`, `stock`, `suppliers`) to appear on the item-history popover. `ai_insights.rerun` only fires the parent's n8n flow and is fire-and-forget; the per-item fields stay stale.
**Match strategy** (in this order):
1. **`sku_exact`** — target's `sku` matches a row directly.
2. **`sku_normalised`** — whitespace / `_` collapsed to `-`, leading-zero runs stripped. So `DRL-006MM` ≡ `DRL-6MM`, `BOLT 6` ≡ `BOLT-6`.
3. **`description_fuzzy`** — Jaccard on tokenised descriptions, threshold 0.6. Run against the most recent 500 `quote_items` rows that have a SKU and description. If a match clears the threshold, take its SKU and re-run the SKU queries.
4. **`none`** — no SKU, no fuzzy match above threshold. The blob is still written with `match_kind: "none"` and `summary: "No history yet for this item."` so the renderer doesn't show a blank card.
**Two endpoints, one MCP tool:**
| Endpoint | Body | When to use |
|----------|------|-------------|
| `POST /api/v1/quotes/[id]/populate-insights` | `{ write?: boolean }` (default true) | Batch: every active line item on a quote (Promise.all internally). Hermes's normal "go through the board" path. |
| `POST /api/v1/quote_items/[id]/populate-insights` | `{ write?: boolean }` | Single item. Use for spot-fixes. |
| `MCP quote_items.populate_insights` | `{ quote_id }` OR `{ item_id }`, plus optional `write: false` for dry-run | Same as above; pass exactly one of `quote_id` / `item_id`. |
Auth: agent bearer on the REST routes, `AGENT_API_KEY` for the MCP tool.
**Output shape** (same as what's persisted):
```jsonc
{
"item_id": "qi-15-chars",
"match_kind": "sku_exact | sku_normalised | description_fuzzy | none",
"found_data": true,
"insights": {
"id": "qi-15-chars",
"summary": "last quoted R1234.50 30d ago (Q202607150) · last ordered 60d ago @ delivered · invoiced 90d ago (INV-99) · on price list · stock in_stock (100)",
"match": { "sku": "M16-BOLT", "match_kind": "sku_exact", "match_confidence": 1 },
"last_quoted": { "quote_item_id": "...", "quote_id": "...", "quote_number": "Q202607150", "quote_date": "2026-...", "selling_price": 1234.5, "cost_price": 950, "customer_id": "...", "customer_name": "...", "days_ago": 30 },
"on_price_list": true,
"price_list_entries": [{ "price_list_id": "...", "sku": "M16-BOLT", "description": "...", "supplier_id": "...", "supplier_name": "Acme", "cost_price": 950, "selling_price": 1300, "brand": "Acme", "category": "Bolts", "subcategory": "M16", "updated": "2026-..." }],
"last_order": { "order_item_id": "...", "order_id": "...", "order_number": "O202...", "order_date": "...", "delivered_at": "...", "cost_price": 950, "selling_price": 1500, "item_status": "delivered", "supplier_id": "...", "supplier_name": "Acme", "days_ago": 60 },
"last_invoiced": { "order_item_id": "...", "order_id": "...", "order_number": "O202...", "supplier_invoice_number": "INV-99", "customer_invoice_number": "C-INV-7", "invoiced_date": "...", "cost_price": 950, "quantity": 5, "qty_invoiced": 5, "days_ago": 90 },
"stock": { "stock_item_id": "...", "name": "...", "sku": "M16-BOLT", "current_qty": 100, "min_qty": 20, "selling_price": 1300, "markup_percent": 36, "status": "in_stock" },
"suppliers": [{ "supplier_id": "...", "supplier_name": "Acme", "last_cost": 950, "last_bought_at": "...", "times_bought": 5, "last_order_id": "..." }],
"generated_at": "2026-08-09T..."
}
}
```
**Batch response shape** (`populate-insights` on a quote):
```jsonc
{
"ok": true,
"quote_id": "q-15-chars",
"total": 5, // active items on the quote
"populated": 4, // items with at least one historical row
"no_match": 1, // items with no data (could be sku_exact with empty history, or match_kind: "none")
"results": [ /* QuoteItemInsightsResult[] */ ]
}
```
**When to run it:**
- Operator asks "fill in the history popovers for these quotes" → `quote_items.populate_insights { quote_id }` for each.
- Operator adds a new SKU to `price_list` → re-run on open quotes containing that SKU so the operator sees the new catalogue row immediately.
- After a `bulk_update_quote_items` that adds SKUs to previously-SKU-less items → re-run those items so the description-fuzzy fallback isn't doing the work.
- After `orders.create_from_quote` lands a new order → re-run the originating quote's items so the `last_order` field appears.
**Hard limits:**
- This is the **only sanctioned writer** for `ai_item_insights`. The `bulk_update_quote_items` and `quote_items.update` MCP whitelists intentionally exclude it.
- Idempotent. Overwrites the previous blob on every call. Pass `write: false` for a dry-run that returns the computed blob without persisting.
- Skips `moved_to_quote != null` items when run on a quote (those belong to a continuation quote's history).
- Reads `stock_items.current_qty` first; falls back to `pos.stock_qty` if no `stock_items` row matches. The renderer surfaces whichever is returned.
---
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
These are intentional or pre-existing limitations. Don't waste time trying to work around them — flag to the operator if they block a workflow you actually need.
feat(quote-items): CRUD via REST + MCP for AI auto-fill agents Closes architectural gap #1 from the Giellie skill (the operator's AI agent could ingest quotes via /api/mcp but had no way to enumerate, add, or patch the resulting line items - every auto-price flow had to stop and ask a human for the item IDs). Adds: - GET /api/v1/quote_items (list w/ filter, sort, expand) - POST /api/v1/quote_items (create on existing quote) - GET /api/v1/quote_items/[id] (fetch one) - PATCH /api/v1/quote_items/[id] (whitelisted patch) - quote_items.list MCP tool - quote_items.get MCP tool - quote_items.create MCP tool - quote_items.update MCP tool Patch whitelist on update is identical to bulk_update_quote_items: cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited Everything else (id, quote, created, updated, quantity, is_processed) returns 400 / MCP error with the allow-list echoed back so the agent can self-correct. quote_items.update does NOT enforce the already_priced skip - that's the safety-net for bulk_update_quote_items. Single-row patches are explicit writes. Newly created rows have is_processed=false forced server-side; Giellie still cannot flip the flag (architectural gap #2 unchanged - that's a UI gate by design). Also: removed "JEI" from quotes.company_letterhead select values on the live PB - valid values are now ["CNL", "ISHERWOOD"]. Use the uppercase form when setting this field. Giellie skill doc refreshed: tool count 69 to 73, new "quote_items CRUD" subsection, three new REST rows, architectural gap #1 marked RESOLVED, hard rule #1 updated to cover the new endpoint, new worked example for "add a missing line item + price it", change-log entry. Tests: 82 passing across the three touched files (was 63 before). Type-check: zero new errors (pre-existing 29 in orders-service-review.test.ts unchanged). Deployment: pending operator rebuild + redeploy. Until then POST /api/v1/quote_items returns 404 and the new tools don't appear in tools/list. Co-authored-by: Orca <help@stably.ai>
2026-08-05 00:40:09 +00:00
1. ~~**No `quote_items.list` MCP tool.**~~ **RESOLVED 2026-08-04.** Added `quote_items.list / .get / .create / .update` (MCP + REST mirror). Giellie can now enumerate, add, and patch quote items end-to-end over the API. The hard limit below (`is_processed` not writable) still applies.
2. **`is_processed` is NOT in the `bulk_update_quote_items` or `quote_items.update` whitelist.** The whitelist is hard-coded at `src/app/api/mcp/route.ts:103-113` and consists of: `cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited`. `is_processed` is intentionally excluded — it's a UI workflow gate the rep clicks on the pricing modal. By design, Giellie cannot bypass rep review. Newly-created items from `quote_items.create` are forced to `is_processed: false` server-side for the same reason.
3. **End-to-end consequence:** Giellie can ingest → list → price → add lines → email a quote, but the final `is_processed` flip still requires a human in the operator UI before any `/pdf` render. This is by design — silent auto-processing of customer-facing quotes is too risky.
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
4. **`quotes.lookup` is supposed to be the order-page quote picker** (free-text search across quote_number, customer, company). Currently broken (see Known bugs §4).
---
## Hard rules
These come from the operator and from the codebase. Non-negotiable.
feat(quote-items): CRUD via REST + MCP for AI auto-fill agents Closes architectural gap #1 from the Giellie skill (the operator's AI agent could ingest quotes via /api/mcp but had no way to enumerate, add, or patch the resulting line items - every auto-price flow had to stop and ask a human for the item IDs). Adds: - GET /api/v1/quote_items (list w/ filter, sort, expand) - POST /api/v1/quote_items (create on existing quote) - GET /api/v1/quote_items/[id] (fetch one) - PATCH /api/v1/quote_items/[id] (whitelisted patch) - quote_items.list MCP tool - quote_items.get MCP tool - quote_items.create MCP tool - quote_items.update MCP tool Patch whitelist on update is identical to bulk_update_quote_items: cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited Everything else (id, quote, created, updated, quantity, is_processed) returns 400 / MCP error with the allow-list echoed back so the agent can self-correct. quote_items.update does NOT enforce the already_priced skip - that's the safety-net for bulk_update_quote_items. Single-row patches are explicit writes. Newly created rows have is_processed=false forced server-side; Giellie still cannot flip the flag (architectural gap #2 unchanged - that's a UI gate by design). Also: removed "JEI" from quotes.company_letterhead select values on the live PB - valid values are now ["CNL", "ISHERWOOD"]. Use the uppercase form when setting this field. Giellie skill doc refreshed: tool count 69 to 73, new "quote_items CRUD" subsection, three new REST rows, architectural gap #1 marked RESOLVED, hard rule #1 updated to cover the new endpoint, new worked example for "add a missing line item + price it", change-log entry. Tests: 82 passing across the three touched files (was 63 before). Type-check: zero new errors (pre-existing 29 in orders-service-review.test.ts unchanged). Deployment: pending operator rebuild + redeploy. Until then POST /api/v1/quote_items returns 404 and the new tools don't appear in tools/list. Co-authored-by: Orca <help@stably.ai>
2026-08-05 00:40:09 +00:00
1. **Respect field whitelists.** `bulk_update_quote_items` and `quote_items.update` reject unknown fields with `reason: "forbidden_field"` (MCP) or HTTP 400 `forbidden_field(s): …` (REST). You cannot sneak `id`, `quote`, `created`, `updated`, `quantity`, or `is_processed` through. The MCP `quote_items.update` also lists the allowed fields in the error so you can self-correct.
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
2. **Don't overwrite hand-priced rows silently.** Rows where `cost_price > 0 AND selling_price > 0` are skipped with `reason: "already_priced"` unless you pass `force_overwrite: true` per row.
3. **Stamp your work.** When you patch pricing fields, set `pricing_notes` in the patch. The tool does **not** auto-inject an audit string.
4. **Use the right upload route.**
- `attachments.upload_for_quote` (MCP) → quote-level attachment
- `POST /api/v1/quotes/items/{id}/attachments/upload` (REST, multipart) → quote-item attachment with `source: "manual"`
- `POST /api/v1/sprint/items/{id}/attachments/upload` (REST, multipart) → pricing proof with `source: "operator_proof"` or `"agent_proof"`
5. **Don't migrate `suppliers.coordinates`.** It's the legacy `"lat, lng"` text field. `supplier-map.ts` parses that exact format.
6. **Cascade-aware deletes.** Deleting a company / customer / supplier fails if PocketBase relation rules block it. If your delete fails, surface the error — don't recurse and delete the blockers.
7. **One file per call.** Don't batch attachments into one MCP call. Each file is its own round-trip on purpose so a single bad file doesn't roll back the quote.
8. **Respect the per-batch cap.** `bulk_update_quote_items` is capped at 100 rows. Page larger batches.
9. **Use `dry_run: true` on unfamiliar batches.** Response shape is identical; only `status` per row changes.
10. **Attachments on `quotes.create_from_email` is deprecated.** The field is accepted for back-compat but ignored. Use `attachments.upload_for_quote` per file after the create call returns the new quote id.
11. **Don't poke PocketBase directly.** Every call goes through the `AGENT_API_KEY`-gated admin PB client. The PB admin token is server-side only.
12. **Don't assume a freshly-ingested quote is PDF-eligible.** See "Architectural gaps" — the `is_processed` flag is a UI gate. If the operator asks you to "render the quote", confirm items are ready before invoking `/api/v1/quotes/{id}/pdf`, or be ready to surface the 422.
---
## Worked examples
### Health check first
```bash
curl -s https://wfdash.apphub.co.za/api/v1/health
```
Expected: `{ "ok": true, "service": "wfdash-api", "timestamp": "…", "env": "…" }`
### List tools (any of three ways)
```bash
# Canonical: JSON-RPC
curl -s -X POST https://wfdash.apphub.co.za/api/mcp \
-H "Authorization: Bearer $AGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# Cheapest: GET, flat array, no JSON-RPC
curl -s https://wfdash.apphub.co.za/api/mcp \
-H "Authorization: Bearer $AGENT_API_KEY"
# JSON-RPC alternative to the GET (post-redeploy of tools.discovery fix)
curl -s -X POST https://wfdash.apphub.co.za/api/mcp \
-H "Authorization: Bearer $AGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"tools.discovery","arguments":{}}}'
```
### Count today's orders
Because `orders.list` ignores `filter`, paginate through all 91 pages (4 514 total orders in production as of 2026-08-04) and filter the `created` field on your side. Took ~30s in the smoke test; the operator should fix the missing `filter` arg (Known bugs §3) when convenient.
```bash
python3 - <<'PY'
import json, subprocess, datetime as dt
today = dt.datetime.utcnow().strftime("%Y-%m-%d")
total = 0
for page in range(1, 100):
r = subprocess.run([
"curl","-sS","-X","POST","https://wfdash.apphub.co.za/api/mcp",
"-H","Authorization: Bearer $AGENT_API_KEY",
"-H","Content-Type: application/json",
"-d", json.dumps({"jsonrpc":"2.0","id":page,"method":"tools/call",
"params":{"name":"orders.list",
"arguments":{"page":page,"perPage":50}}})
], capture_output=True, text=True)
items = json.loads(json.loads(r.stdout)["result"]["content"][0]["text"])["items"]
total += sum(1 for it in items if it["created"].startswith(today))
if len(items) < 50: break
print(f"Orders created today ({today} UTC): {total}")
PY
```
### Ingest an inbound RFQ email
```jsonc
// 1) Create the quote (with its customer / company / items).
POST /api/mcp
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "quotes.create_from_email",
"arguments": {
"from_email": "buyer@acme.co.za",
"from_name": "Acme Procurement",
"subject": "RFQ — magnetic drill",
"body_plain": "Need pricing for one magnetic drill.",
"body_html": "<p>Need pricing for one magnetic drill.</p>",
"message_id": "<msg-abc@mail.acme.co.za>",
"in_reply_to": "<prior-msg@mail.acme.co.za>",
"received_at": "2026-07-09T10:00:00Z",
"company_hint": "Acme",
"extracted_items": [
{
"description": "Magnetic drill",
"quantity": 1,
"unit": "ea",
"notes": "See attached spec page 2"
}
]
}
}
}
// 2) Parse result.content[0].text → get the new quote.id and items count.
feat(quote-items): CRUD via REST + MCP for AI auto-fill agents Closes architectural gap #1 from the Giellie skill (the operator's AI agent could ingest quotes via /api/mcp but had no way to enumerate, add, or patch the resulting line items - every auto-price flow had to stop and ask a human for the item IDs). Adds: - GET /api/v1/quote_items (list w/ filter, sort, expand) - POST /api/v1/quote_items (create on existing quote) - GET /api/v1/quote_items/[id] (fetch one) - PATCH /api/v1/quote_items/[id] (whitelisted patch) - quote_items.list MCP tool - quote_items.get MCP tool - quote_items.create MCP tool - quote_items.update MCP tool Patch whitelist on update is identical to bulk_update_quote_items: cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited Everything else (id, quote, created, updated, quantity, is_processed) returns 400 / MCP error with the allow-list echoed back so the agent can self-correct. quote_items.update does NOT enforce the already_priced skip - that's the safety-net for bulk_update_quote_items. Single-row patches are explicit writes. Newly created rows have is_processed=false forced server-side; Giellie still cannot flip the flag (architectural gap #2 unchanged - that's a UI gate by design). Also: removed "JEI" from quotes.company_letterhead select values on the live PB - valid values are now ["CNL", "ISHERWOOD"]. Use the uppercase form when setting this field. Giellie skill doc refreshed: tool count 69 to 73, new "quote_items CRUD" subsection, three new REST rows, architectural gap #1 marked RESOLVED, hard rule #1 updated to cover the new endpoint, new worked example for "add a missing line item + price it", change-log entry. Tests: 82 passing across the three touched files (was 63 before). Type-check: zero new errors (pre-existing 29 in orders-service-review.test.ts unchanged). Deployment: pending operator rebuild + redeploy. Until then POST /api/v1/quote_items returns 404 and the new tools don't appear in tools/list. Co-authored-by: Orca <help@stably.ai>
2026-08-05 00:40:09 +00:00
// You will NOT get the item IDs back from `create_from_email` itself.
// Use `quote_items.list` (new in 2026-08-04) to fetch them:
// {
// "method": "tools/call",
// "params": {
// "name": "quote_items.list",
// "arguments": { "filter": "quote=\"<quote.id>\"" }
// }
// }
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
// 3) For each attachment (one MCP call per file, ≤25 MB each):
{
"method": "tools/call",
"params": {
"name": "attachments.upload_for_quote",
"arguments": {
"quote_id": "<quote.id>",
"filename": "spec.pdf",
"content_type": "application/pdf",
"size": 482113,
"base64": "JVBERi0xLjQK..."
}
}
}
// 4) Operator reviews in the UI, flips is_processed on the items, and
// approves. If they then ask you to turn it into an order:
{
"method": "tools/call",
"params": {
"name": "orders.create_from_quote",
"arguments": { "quote_id": "<quote.id>" }
}
}
```
### Render a quote PDF on ISHERWOOD (or CNL) letterhead
```bash
curl -s -X POST https://wfdash.apphub.co.za/api/v1/quotes/<quote_id>/pdf \
-H "Authorization: Bearer $AGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"letterhead":"ISHERWOOD"}'
```
Response on success:
```json
{
"ok": true,
"attachmentId": "hf2cru2o4cx90cm",
"status": "approval_pending", // or preserved if already approved
"pdfGeneratedAt": "2026-08-04T20:22:31Z",// null when status is in terminal set
"filename": "Quote-ISHERWOOD-Q000087.pdf",
"skippedItemIds": [],
"readyItemCount": 1,
"totalItemCount": 1,
"isPartial": false
}
```
422 cases to expect (read the `code` field):
- `placeholder_customer` — customer is a sentinel or has no email.
- `no_items` — quote has no items at all.
- `items_not_ready` — items exist but none have `is_processed=true && supplier && cost_price>0 && selling_price>0`. The response includes `details.issues[]` with per-item `missing` lists. **This is what you'll hit on any quote Giellie ingested and priced — see Architectural gaps §2.**
To download the rendered PDF binary:
```bash
curl -s "https://wfdash.apphub.co.za/api/v1/quotes/<quote_id>/pdf?attachmentId=<attachment_id>&download=1" \
-H "Authorization: Bearer $AGENT_API_KEY" \
-o quote.pdf
```
### Auto-price a batch of quote items (with safety check)
```jsonc
// Always dry-run first on an unfamiliar batch.
POST /api/mcp
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "bulk_update_quote_items",
"arguments": {
"items": [
{
"id": "15-char-quote-item-id",
"patch": {
"cost_price": 1200.00,
"selling_price": 1800.00,
"supplier": "15-char-supplier-id",
"pricing_source": "ai",
"pricing_notes": "Auto-priced from Acme catalogue 2026-07-09"
},
"force_overwrite": false
}
],
"dry_run": true
}
}
}
// If dry_run returns `status: "would_apply"` for each row, re-run
// with dry_run: false. Already-priced rows return status: "skipped"
// with reason: "already_priced" — pass force_overwrite: true on
// those specific rows to override.
//
// ⚠️ You still cannot set is_processed via this tool. Once pricing
// is committed, ask the operator (or trigger a human in the loop) to
// flip is_processed before any /pdf render.
```
feat(quote-items): CRUD via REST + MCP for AI auto-fill agents Closes architectural gap #1 from the Giellie skill (the operator's AI agent could ingest quotes via /api/mcp but had no way to enumerate, add, or patch the resulting line items - every auto-price flow had to stop and ask a human for the item IDs). Adds: - GET /api/v1/quote_items (list w/ filter, sort, expand) - POST /api/v1/quote_items (create on existing quote) - GET /api/v1/quote_items/[id] (fetch one) - PATCH /api/v1/quote_items/[id] (whitelisted patch) - quote_items.list MCP tool - quote_items.get MCP tool - quote_items.create MCP tool - quote_items.update MCP tool Patch whitelist on update is identical to bulk_update_quote_items: cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited Everything else (id, quote, created, updated, quantity, is_processed) returns 400 / MCP error with the allow-list echoed back so the agent can self-correct. quote_items.update does NOT enforce the already_priced skip - that's the safety-net for bulk_update_quote_items. Single-row patches are explicit writes. Newly created rows have is_processed=false forced server-side; Giellie still cannot flip the flag (architectural gap #2 unchanged - that's a UI gate by design). Also: removed "JEI" from quotes.company_letterhead select values on the live PB - valid values are now ["CNL", "ISHERWOOD"]. Use the uppercase form when setting this field. Giellie skill doc refreshed: tool count 69 to 73, new "quote_items CRUD" subsection, three new REST rows, architectural gap #1 marked RESOLVED, hard rule #1 updated to cover the new endpoint, new worked example for "add a missing line item + price it", change-log entry. Tests: 82 passing across the three touched files (was 63 before). Type-check: zero new errors (pre-existing 29 in orders-service-review.test.ts unchanged). Deployment: pending operator rebuild + redeploy. Until then POST /api/v1/quote_items returns 404 and the new tools don't appear in tools/list. Co-authored-by: Orca <help@stably.ai>
2026-08-05 00:40:09 +00:00
### Add a missing line item to an existing quote, then price it
```jsonc
// Scenario: `quotes.create_from_email` only picked up 2 of the 3 items
// from the RFQ. Add the third one, then price the whole quote.
// 1) Create the missing line item.
POST /api/mcp { "method": "tools/call", "params": {
"name": "quote_items.create",
"arguments": {
"quote_id": "<quote.id>",
"description": "Coolant 5L",
"quantity": 2,
"unit": "each",
"sku": "COOL-5L",
"cost_price": 145.00,
"markup": 33
}
}}
// → { "item": { "id": "qi-new", "is_processed": false, "selling_price": 0, … } }
// Note: `selling_price` is 0 — `cost_price` and `markup` were stored,
// but the row needs re-pricing to roll them up.
// 2) Pull every item id under the quote (the new one + the originals).
POST /api/mcp { "method": "tools/call", "params": {
"name": "quote_items.list",
"arguments": { "filter": "quote=\"<quote.id>\"", "perPage": 50 }
}}
// → { "items": [ { "id": "qi-1", … }, { "id": "qi-2", … }, { "id": "qi-new", … } ] }
// 3) Bulk-price them in one round-trip (dry-run first).
POST /api/mcp { "method": "tools/call", "params": {
"name": "bulk_update_quote_items",
"arguments": {
"dry_run": true,
"items": [
{ "id": "qi-1", "patch": { "cost_price": 1200, "markup": 33, "pricing_notes": "AI 2026-08-04" } },
{ "id": "qi-2", "patch": { "cost_price": 450, "markup": 33, "pricing_notes": "AI 2026-08-04" } },
{ "id": "qi-new", "patch": { "cost_price": 145, "markup": 33, "pricing_notes": "AI 2026-08-04" } }
]
}
}}
// If every row came back status="dry_run", re-run with dry_run=false.
// 4) Re-price the parent quote so selling_price = cost × (1 + markup/100)
// is written back on each row.
POST /api/mcp { "method": "tools/call", "params": {
"name": "quotes.re_price",
"arguments": { "id": "<quote.id>" }
}}
// ⚠️ `is_processed` is still false on every row. A human must flip it
// in the operator UI before any /api/v1/quotes/{id}/pdf render will
// succeed. This is intentional — see Architectural gaps §2.
```
docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings Bring the Giellie agent's operating manual in line with the live production state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za. Bugs / quirks documented (with workarounds): - tools.discovery was unwired; now fixed in code (separate commit), live server needs redeploy before response changes - orders.list silently ignores 'filter'; only 'search' is honoured - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead - collection_pool.list + sprint.list return 'Missing collection context' - orders.get 'not found' for at least one valid id - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST) Architectural gaps documented: - No quote_items.list MCP tool; quotes.create_from_email returns item count only, so a freshly-ingested quote's item IDs can't be recovered via the agent API - is_processed is intentionally NOT in bulk_update_quote_items whitelist; end-to-end create→price→render pipeline requires human in the UI Credentials & endpoints now include every token the agent might need: AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six n8n webhook URLs (rewriter, price-list match, PO send, AI insights, markup approval, customer list, QB invoice creator), TomTom map key. Other updates: - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za - Bumped tool count 69 → 70 (tools.discovery added to catalog) - Added three-way tool discovery guide (tools/list, tools.discovery, GET /api/mcp) - Added 'Quote PDF rendering' guide explaining the 422 gates, status transitions, and letterhead codes - Added change log at the bottom - All worked examples updated to the correct host Co-authored-by: Orca <help@stably.ai>
2026-08-04 20:47:55 +00:00
### Maintain a supplier
```jsonc
// Create
POST /api/mcp { "method": "tools/call", "params": {
"name": "suppliers.create",
"arguments": {
"suppliername": "Acme Supplies",
"email": "sales@acme.co.za",
"suppliernumber": "+27 11 555 0200",
"supply_tags": "tools,fasteners",
"coordinates": "-26.20, 28.04", // LEGACY text format
"closingtime": "17:00"
}
}}
// Update (only the fields you include in `patch` are written)
POST /api/mcp { "method": "tools/call", "params": {
"name": "suppliers.update",
"arguments": {
"id": "15-char-supplier-id",
"patch": { "email": "new-email@acme.co.za" }
}
}}
```
### Send a PO to a supplier (or retry after a failure)
```jsonc
POST /api/mcp { "method": "tools/call", "params": {
"name": "purchase_orders.send_email",
"arguments": {
"id": "15-char-po-id",
"to": "buyer@supplier.co.za", // optional override; defaults to supplier.email
"subject": "Purchase Order PO-…",
"message": "Hi, please confirm receipt. — WFDash"
}
}}
// On SMTP failure the PO is flipped to mail_failed. Use
// purchase_orders.retry_email to fire the same payload again.
```
---
## Failure modes
| Symptom | Cause | What to do |
|---------|-------|-------------|
| HTTP 401 from `/api/v1/*`, JSON-RPC error envelope from `/api/mcp` | Wrong / missing bearer | **Stop and check the token**. Tell the operator; the token may need rotation. |
| `{ code: -32601, message: "Method not found" }` | Typo / outdated tool name | Call `tools/list` and retry with the correct name. |
| `{ code: -32000, message: "Unknown tool: <name>" }` | Tool declared in the catalog but not implemented (e.g. `tools.discovery` pre-fix), or a REST endpoint invoked via MCP (e.g. `quotes.items.rewrite`) | Check Known bugs / quirks; if it's a REST endpoint, call the REST route directly. |
| `{ code: -32000, message: "Missing collection context." }` | PB session not forwarded | See Known bugs §5 — `collection_pool.list` and `sprint.list`. Tell the operator. |
| `{ code: -32000, message: "fetch failed" }` | Internal MCP-side fetch broke | See Known bugs §4 (`quotes.lookup`). Workaround: use `quotes.list` with a PB filter. |
| 5xx with `{ error: "PB admin unavailable" }` | PB unreachable from the server | Retry once after a few seconds; if persistent, alert the operator — the issue is infra-side. |
| Per-row `reason: "already_priced"` in `bulk_update_quote_items` | Hand-priced row | Pass `force_overwrite: true` on that specific row, or accept the skip. |
| Per-row `reason: "forbidden_field"` | You patched a field outside the whitelist | Read the whitelist in the "Hard rules" section above. `is_processed` is *not* writable via MCP by design. |
| `POST /api/v1/quotes/{id}/pdf` returns 422 with `code: "items_not_ready"` | Items aren't priced-and-processed yet | See "Architectural gaps" §2 — you'll need the operator to flip `is_processed` in the UI, or you need to find an already-processed quote. |
| `POST /api/v1/quotes/{id}/pdf` returns 422 with `code: "placeholder_customer"` | Customer is a sentinel / has no email | Use `PATCH /api/v1/quotes/{id}/pdf` to swap in a real customer with email first. |
| `POST /api/v1/quotes/{id}/attachments/upload` returns 400 | Wrong mime type or >25 MB | PB enforces the `quote_attachments.file` mime allowlist + maxSize. Re-encode / split. |
---
## What to load alongside this skill
You don't need any other skill to operate Giellie. The MCP server (`/api/mcp`) is the canonical discovery surface — call `tools/list` once at session start (and again whenever the operator says tools have changed) instead of relying on the tool index in this file.
If you need to do screenshots, web automation, or browser interaction with the WFDASH UI (e.g. to verify the result of a write you made), load:
- `agent-browser` — for reading / clicking through the operator UI. Don't use this to **mutate**; always mutate via the API/MCP so there's a server-side audit trail.
- `computer-use` — only for legacy desktop apps. WFDASH is web-first; prefer `agent-browser`.
---
## When in doubt
Ask the operator. WFDASH is a real business CRM — silent mistakes cost people money. Prefer one extra confirmation call over one silent write. If you ever feel about to delete or bulk-overwrite something irreversible, stop and confirm with the operator before the call lands.
---
## Credential rotation
When the operator rotates `AGENT_API_KEY`:
1. Update `.env.local` in the WFDASH repo (this is what the server reads).
2. Update this skill file (`AGENT_API_KEY` row in the Credentials table).
3. Update every n8n HTTP Bearer credential named "WFDASH Agent Key".
4. Update every MCP client config that points at `/api/mcp`.
When the operator rotates the n8n API key:
1. Update `.env.local`.
2. Update this skill file (n8n public API key row).
The same four places for the bearer, two for the n8n key. Out-of-sync credentials are the #1 cause of "401 from a call I just made."
---
## Change log
- **2026-08-04** — Initial live-server smoke test. Discovered and documented all six known bugs, the four architectural gaps, the missing webhooks/n8n tokens. Fixed `tools.discovery` source (`src/app/api/mcp/route.ts:2497`) — **deployment pending**, the live server still returned "Unknown tool" until rebuild. Replaced stale `wfdash.davidels.dev` base URL with `wfdash.apphub.co.za`. Removed `quotes.items.rewrite` from the MCP tool families — it was always a REST endpoint. Catalog count is **69** (unchanged — `tools.discovery` was already declared; only its runtime behavior changed from "Unknown tool" to functional).
feat(quote-items): CRUD via REST + MCP for AI auto-fill agents Closes architectural gap #1 from the Giellie skill (the operator's AI agent could ingest quotes via /api/mcp but had no way to enumerate, add, or patch the resulting line items - every auto-price flow had to stop and ask a human for the item IDs). Adds: - GET /api/v1/quote_items (list w/ filter, sort, expand) - POST /api/v1/quote_items (create on existing quote) - GET /api/v1/quote_items/[id] (fetch one) - PATCH /api/v1/quote_items/[id] (whitelisted patch) - quote_items.list MCP tool - quote_items.get MCP tool - quote_items.create MCP tool - quote_items.update MCP tool Patch whitelist on update is identical to bulk_update_quote_items: cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited Everything else (id, quote, created, updated, quantity, is_processed) returns 400 / MCP error with the allow-list echoed back so the agent can self-correct. quote_items.update does NOT enforce the already_priced skip - that's the safety-net for bulk_update_quote_items. Single-row patches are explicit writes. Newly created rows have is_processed=false forced server-side; Giellie still cannot flip the flag (architectural gap #2 unchanged - that's a UI gate by design). Also: removed "JEI" from quotes.company_letterhead select values on the live PB - valid values are now ["CNL", "ISHERWOOD"]. Use the uppercase form when setting this field. Giellie skill doc refreshed: tool count 69 to 73, new "quote_items CRUD" subsection, three new REST rows, architectural gap #1 marked RESOLVED, hard rule #1 updated to cover the new endpoint, new worked example for "add a missing line item + price it", change-log entry. Tests: 82 passing across the three touched files (was 63 before). Type-check: zero new errors (pre-existing 29 in orders-service-review.test.ts unchanged). Deployment: pending operator rebuild + redeploy. Until then POST /api/v1/quote_items returns 404 and the new tools don't appear in tools/list. Co-authored-by: Orca <help@stably.ai>
2026-08-05 00:40:09 +00:00
- **2026-08-04 (post-redeploy)** — Verified live: `tools.discovery` now returns the flat 69-tool catalog via JSON-RPC; `GET /api/mcp` still works; the other five bugs (`collection_pool.list`, `sprint.list`, `quotes.lookup`, `orders.list` filter, `quotes.items.rewrite` as MCP) reproduce exactly as documented, confirming the skill doc is accurate. Two PDF regenerations on `Q000087` succeeded (ISHERWOOD + CNL letterheads) — `pdf_generated_at` stays null because the quote is in a terminal `approved` state.
- **2026-08-04 (quote_items CRUD shipped)** — Added four MCP tools + three REST routes to close architectural gap §1: `quote_items.list`, `quote_items.get`, `quote_items.create`, `quote_items.update` (MCP), and `GET / POST /api/v1/quote_items` + `GET / PATCH /api/v1/quote_items/{id}`. Same patch whitelist as `bulk_update_quote_items` (`cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited`). `quote_items.update` does NOT enforce the `already_priced` skip — use `bulk_update_quote_items` when you want the safety net. Newly-created rows have `is_processed: false` forced server-side; Giellie still cannot flip that flag on its own (gap §2 unchanged). **Removed `JEI` from `quotes.company_letterhead` select values on the live PB** — valid values are now `["CNL", "ISHERWOOD"]`. Catalog count **73** (+4). Deployment of these changes is pending on the operator's rebuild + redeploy; until then `tools/list` will not surface the new tools and `POST /api/v1/quote_items` will return 404.
- **2026-08-09 (deterministic quote-item insights backfill)** — Added `quote_items.populate_insights` (MCP) + `POST /api/v1/quotes/[id]/populate-insights` + `POST /api/v1/quote_items/[id]/populate-insights` (REST). Replaces the `ai_insights.rerun` flow for the per-item popover — Hermes now populates `quote_items.ai_item_insights` directly with a deterministic compute (SKU exact → normalised → description-fuzzy Jaccard ≥ 0.6 → `none`). New typed shape in `src/lib/types.ts`: `QuoteItemInsights`, `QuoteItemInsightsLastQuoted`, `QuoteItemInsightsLastOrder`, `QuoteItemInsightsLastInvoiced`, `QuoteItemInsightsPriceListEntry[]`, `QuoteItemInsightsStock`, `QuoteItemInsightsSupplier[]`. Renderer is unchanged (already shape-agnostic). `ai_item_insights` is intentionally excluded from the `bulk_update_quote_items` / `quote_items.update` whitelists; this endpoint is the only sanctioned writer. Idempotent, parallel within a quote. Catalog count **74** (+1).
- **2026-08-09 (item-history read-path JSON.parse bug fixed)** — `GET /api/v1/quote_items/[id]/item-history` was calling `JSON.parse(row.ai_item_insights)` on a PB JSON field that returns as an object (not a string). The parse always threw on the happy path; the `try/catch` silently swallowed it; `ai: null` was returned; the operator popover rendered only the deterministic (indigo) side. Fixed by handling both shapes: object (normal case) used directly, JSON-stringified blob (legacy n8n writers) parsed defensively. Type changed from `string | null` to `unknown`. New regression test: `src/app/api/v1/quote_items/[id]/item-history/route.test.ts` (8 cases). After deploy the per-item AI side will surface automatically — no client change needed.