# WFDASH Agent Brief — Auto-Fill Missing Quote Prices **Mission:** Find quotes that have line items without pricing, match each line item against our product catalogue, and patch in the missing `cost_price`, `selling_price`, `description`, and `supplier` where a confident match exists. **Stop-and-ask rules** (stick to these or don't act): 1. **Never** write to `price_list` — it's read-only for you. If a SKU is missing, stop and tell the human. 2. **Never** patch a `quote_item` whose `cost_price` is already > 0 unless the human says "force update" — operators often hand-edit prices, respect that. 3. **Never** delete anything. Patch-only. 4. **Tier 3 fuzzy matches (`search_text`)** are flagged "PICKER" — write the `pricing_notes` field with `"needs human review: "` and DO NOT auto-fill. Only Tier 1 + Tier 2 may auto-fill. 5. If the supplier name from a catalogue row can't be resolved to a `suppliers` id, set `pricing_notes = "supplier '' not in PB — add it first"` and stop on that item. --- ## 1. Environment (everything is local on the same PC) | Service | URL | Auth | |---|---|---| | **WFDash web app** | `http://localhost:4000` | browser session (PB token in localStorage) | | **PocketBase API** | `https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io` | admin (see below) | | **WFDash MCP (JSON-RPC)** | `http://localhost:4000/api/mcp` | `Authorization: Bearer 123456` | **PocketBase admin** (has full DB access — only share with yourself / on this PC): ``` email: david.els1@outlook.com password: 1234567890 ``` You'll use this to mint an auth token, then use the token on every subsequent request. Sample login: ```bash PB="https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io" TOKEN=$(curl -s -X POST "$PB/api/collections/users/auth-with-password" \ -H "Content-Type: application/json" \ -d '{"identity":"david.els1@outlook.com","password":"1234567890"}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])") echo "$TOKEN" > /tmp/pb_token ``` Then everything is `curl -H "Authorization: $TOKEN" $PB/api/...`. **WFDash agent key** (for `/api/mcp` JSON-RPC tooling): ``` Authorization: Bearer 123456 ``` The MCP exposes tools for `orders`, `quotes` (create/append/find from email), `attachments.upload_for_quote`, `companies.*`, `customers.*`, `suppliers.*`. It does NOT expose price_list reads or quote_item writes — those go via PocketBase directly. --- ## 2. Health check (run first) ```bash curl -s http://localhost:4000/api/v1/health # {"ok":true,"service":"wfdash-api","timestamp":"..."} ``` If the WFDash app isn't running, the human will need to start it. The PocketBase URL above is the production-style external URL — works from anywhere with network access. --- ## 3. The collections you need ### `price_list` (catalogue — READ ONLY) | Field | Type | Notes | |---|---|---| | `id` | text (15-char PB id) | primary key | | `sku` | text | `"MAK-AG-125-9558"` | | `sku_normalised` | text | canonical form: uppercase, non-alnum stripped | | `description` | text | the operator-facing description | | `cost_price` | number | what we pay | | `selling_price` | number | what we charge (0 means "use cost × 1.25") | | `unit` | text | `"EA"`, `"BOX"`, etc. | | `supplier` | text | display name string, e.g. `"JEISA - WESTSCOTT GROUP"`. NOT a relation id. | | `brand` | text | e.g. `"MAKITA"` | | `category` | text | e.g. `"POWER TOOLS"` | | `subcategory` | text | e.g. `"GRINDERS"` | | `search_text` | text | pre-flattened: SKU + description + brand + category + supplier + every spec key/val/synonym. Use this for Tier 3 fuzzy match. | | `specifications` | json | `{ "voltage": "230V", "synonyms": ["125mm grinder", ...] }` | | `updated` | autodate | for "newest match wins" tie-breaking | ### `quote_items` (PATCH THIS) | Field | Type | Notes | |---|---|---| | `id` | text (15-char) | | | `quote` | relation → `quotes` | required | | `sku` | text | may be empty — see §4 for matching logic | | `description` | text | operator's raw line description | | `quantity` | number | don't touch | | `supplier` | relation → `suppliers` | the relation id, NOT a name | | `cost_price` | number | | | `selling_price` | number | | | `markup` | number | recompute every time (see below) | | `pricing_source` | select | set to `"catalogue"` if you filled prices | | `pricing_notes` | text | explain what you did | | `matched_price_list_id` | text (relation-ish) | the price_list id you matched against | | `user_edited` | bool | set `true` | | `unit` | text | copy from catalogue | ### `suppliers` | Field | Type | Notes | |---|---|---| | `id` | text | | | `suppliername` | text | the display name that matches `price_list.supplier` | Lookup pattern (case-insensitive): ```bash curl -s -H "Authorization: $TOKEN" \ "$PB/api/collections/suppliers/records?filter=suppliername~%22JEISA%22&fields=id,suppliername&perPage=5" ``` If no hits → follow stop-and-ask rule #5. ### `quotes` | Field | Notes | |---|---| | `id` | | | `quote_number` | `Q1782862717620`-style display id | | `customer` | relation | | `status` | `draft`, `sent`, `accepted`, etc. | You don't need to write to `quotes`. You only need to find ones where their `quote_items` are unpriced (§4). --- ## 4. The matcher (use exactly this tiered logic) For a given `quote_item`: 1. **Skip if** `cost_price > 0` AND `selling_price > 0` (already priced — respect operator hand-edits). 2. **Tier 1 — exact SKU match.** ```bash curl -s -H "Authorization: $TOKEN" \ "$PB/api/collections/price_list/records?filter=sku%3D%22${SKU}%22%20%7C%7C%20sku_normalised%3D%22${NORM}%22&sort=-updated&perPage=25&fields=id,sku,description,cost_price,selling_price,unit,supplier,brand,category,subcategory,sku_normalised,updated" ``` Where `NORM` is the SKU uppercased with all non-alphanumeric stripped (`"MAK-AG-125"` → `"MAKAG125"`). If 1+ hits → pick the newest, auto-fill. 3. **Tier 2 — substring on SKU.** Same call, filter is: ``` (sku_normalised~"${NORM}" || sku~"${SKU}") ``` Pick newest, auto-fill. 4. **Tier 3 — fuzzy on `search_text`.** Same call, filter is: ``` search_text~"${SKU_OR_DESCRIPTION_RAW}" ``` With cap of 10 results, sorted newest first. **Do NOT auto-fill.** Set: ```json { "pricing_notes": "needs human review — Tier 3 fuzzy match: " } ``` and stop on this item. ### Markup formula After patch, `markup = round(((selling / cost) - 1) * 100)`. If catalogue `selling_price == 0`, fall back to `selling = round(cost * 1.25 * 100) / 100` then compute markup. ### The full patch payload (Tier 1 or Tier 2 success) ```json { "cost_price": , "selling_price": , "description": "", "supplier": "", "unit": "", "pricing_source": "catalogue", "matched_price_list_id": "", "user_edited": true, "markup": , "pricing_notes": "Auto-filled by agent on from (Tier )" } ``` PATCH example: ```bash curl -s -X PATCH -H "Authorization: $TOKEN" -H "Content-Type: application/json" \ -d '{"cost_price":1450,"selling_price":1812.5,"description":"MAKITA 9558NB ANGLE GRINDER 125MM 840W","supplier":"","unit":"EA","pricing_source":"catalogue","matched_price_list_id":"","user_edited":true,"markup":25,"pricing_notes":"Auto-filled by agent 2026-07-01 from JEISA (Tier 1)"}' \ "$PB/api/collections/quote_items/records/" ``` --- ## 5. End-to-end flow ```bash # (a) Get token (see §1) # (b) Find unpriced quote_items # A quote_item is "unpriced" if cost_price == 0 OR selling_price == 0 # PB filter: (cost_price = 0 || selling_price = 0) curl -s -H "Authorization: $TOKEN" \ "$PB/api/collections/quote_items/records?filter=(cost_price%3D0%20%7C%7C%20selling_price%3D0)&perPage=50&fields=id,quote,sku,description,cost_price,selling_price,quantity,supplier" # Returns a page; page through with ?page=2 etc. until empty. # (c) For each unpriced quote_item — run the tiered matcher from §4 # (d) PATCH the item with the payload from §4 (Tier 1+2 auto-fill, # or Tier 3 → pricing_notes-only) # (e) Report a summary back to the human: # - how many quote_items you touched # - how many you filled at Tier 1 / Tier 2 / flagged at Tier 3 # - any suppliers you couldn't resolve ``` --- ## 6. Test data (already in PB, you can use it) - 5 dummy `TEST_GRINDER` catalogue rows (brand = `TEST_GRINDER`) - 1 test quote id `7lvyrbdmftr3arx` with 3 items (`Q1782862717620`) - 3 items: one exact-match, one fuzzy-match, one no-match You can run your full flow against these without touching real data. When you're done, ask the human whether to clean them up. --- ## 7. Useful one-liners ```bash # Read a full quote with items in one go curl -s -H "Authorization: $TOKEN" \ "$PB/api/collections/quotes/records/?expand=customer,company,quote_items_via_quote" # Read a price_list row by id curl -s -H "Authorization: $TOKEN" \ "$PB/api/collections/price_list/records/" # Paginate unpriced items curl -s -H "Authorization: $TOKEN" \ "$PB/api/collections/quote_items/records?filter=cost_price%3D0&page=2&perPage=50" # Check that a price_list supplier name resolves to a supplier curl -s -H "Authorization: $TOKEN" \ "$PB/api/collections/suppliers/records?filter=suppliername%3D%22${NAME}%22&fields=id,suppliername&perPage=1" ``` --- ## 8. Things you cannot do via this API - The WFDash `/api/v1/quotes/{id}/re-price` route requires a **browser session** cookie, not the agent key. Skip it — you're doing the same job directly against PocketBase here. - You have no file-system or shell access. PB admin doesn't grant that — it only gives PB CRUD. - Production-style constraints carry over: don't touch `price_list`, don't delete anything, leave a `pricing_notes` audit trail on every row you write. That's everything. Confirm you can hit `/api/v1/health` and authenticate to PocketBase before you start listing unpriced rows.