diff --git a/docs/AGENT_INSTRUCTIONS.md b/docs/AGENT_INSTRUCTIONS.md index 9e55556..71b1d0f 100644 --- a/docs/AGENT_INSTRUCTIONS.md +++ b/docs/AGENT_INSTRUCTIONS.md @@ -134,8 +134,10 @@ specific** tool that fits the task. | `stock_purchase_items.bulk_update` | Batch patch stock_purchase_items (≤100 rows). Same whitelist + hand-edit guard as `.update`. Per-row independence — one row's failure never affects another. `dry_run: true` returns the same shape with `status: "dry_run"` per row. | | `stock_purchase_items.assign_driver` | Bind a stock_purchase_item to a driver. Transitions line status `null → assigned_to_driver` or `collection_pool → assigned_to_driver`. Wrong_status (409) when the line is already past the assignment step. | | `stock_purchase_items.mark_collected` | Mark a line as physically delivered. Status → `collected`, bumps `quantity_received`. Does NOT book into stock — that's `stock_purchases.receive`. Header auto-flips to `received` when every line on the PO has `quantity_received >= quantity`. | -| `stock_items.update` | Patch catalogue fields on a master stock_item: `sku`, `description`, `notes`, `unit`, `selling_price`, `markup`. For inventory qty use `stock_items.adjust`. Returns `{ok: true, item, previous}` on success or `{ok: false, reason, message, field?}` on rejection. | -| `stock_items.set_pricing` | Set `cost_price` / `selling_price` / `markup` on existing stock_items in one batch (max 100 per call). The POS till reads `selling_price` (not `markup`), so set both consistently: selling = cost × (1 + markup/100). Cost changes are audited in `stock_ledger` with the `actor`; quantities are NOT changed (use `stock_items.adjust`). Requires `actor.id`. Returns per-row `{ok, item, previous}` / `{ok: false, reason, message}`. | +| `stock_items.update` | Patch catalogue fields on a master stock_item: `sku`, `description`, `notes`, `unit`, `selling_price`, `markup`. For inventory qty use `stock_items.set_quantities`. Returns `{ok: true, item, previous}` on success or `{ok: false, reason, message, field?}` on rejection. | +| `stock_items.set_pricing` | Set `cost_price` / `selling_price` / `markup` on existing stock_items in one batch (max 100 per call). The POS till reads `selling_price` (not `markup`), so set both consistently: selling = cost × (1 + markup/100). Cost changes are audited in `stock_ledger` with the `actor`; quantities are NOT changed (use `stock_items.set_quantities`). Requires `actor.id`. Returns per-row `{ok, item, previous}` / `{ok: false, reason, message}`. | +| `stock_items.set_quantities` | Set or change the stock quantity on up to 100 pasted lines. `mode: "set"` (the number is the new quantity on hand) or `mode: "add"` (the number is added to the current quantity). `lines`: 1–100 entries of `{ sku?, description?, qty }` — `qty` is required, and each line needs a `sku` or a `description` to match on. `dry_run: true` plans the change and writes nothing at all. Quantities may be negative — the till oversells on purpose. A line that cannot be resolved is reported, never guessed. Every `stock_ledger` row is stamped with the acting user. Requires `actor.id`. **This is the tool for quantities.** | +| `stock_items.adjust` | Record a one-off signed adjustment to ONE item's quantity (`change_qty` positive = in, negative = out; zero is rejected) with a `reason` of your own. **Superseded for quantity work — use `stock_items.set_quantities`.** Requires `actor.id`; the ledger row is written first and reversed if the quantity write fails. | ### 4.5 CRM maintenance diff --git a/docs/api-mcp-layer.md b/docs/api-mcp-layer.md index 48374f8..b292f68 100644 --- a/docs/api-mcp-layer.md +++ b/docs/api-mcp-layer.md @@ -219,8 +219,10 @@ JSON-RPC over HTTP. Same `Authorization` bearer as `/api/v1/*`. | Tool | Purpose | | --- | --- | -| `stock_items.update` | Patch catalogue fields on a master `stock_item` (`sku`, `description`, `notes`, `unit`, `selling_price`, `markup`). For inventory qty use `stock_items.adjust`. | -| `stock_items.set_pricing` | Set `cost_price` / `selling_price` / `markup` on existing `stock_items` in one batch (max 100). The POS till reads `selling_price`, not `markup` — set both. Cost changes are audited in `stock_ledger` with the `actor`; quantities are not changed (use `stock_items.adjust`). | +| `stock_items.update` | Patch catalogue fields on a master `stock_item` (`sku`, `description`, `notes`, `unit`, `selling_price`, `markup`). For inventory qty use `stock_items.set_quantities`. | +| `stock_items.set_pricing` | Set `cost_price` / `selling_price` / `markup` on existing `stock_items` in one batch (max 100). The POS till reads `selling_price`, not `markup` — set both. Cost changes are audited in `stock_ledger` with the `actor`; quantities are not changed (use `stock_items.set_quantities`). | +| `stock_items.set_quantities` | Set or change the stock quantity on up to 100 lines, matched by SKU and description. `mode: "set"` (the number is the new quantity on hand) or `mode: "add"` (the number is added to the current quantity). `lines`: 1–100 entries of `{ sku?, description?, qty }` — `qty` is required. `dry_run: true` plans the change and writes nothing at all. Quantities may be negative (the till oversells on purpose). Every `stock_ledger` row is stamped with the acting user. Requires `actor.id`. | +| `stock_items.adjust` | Record a one-off signed adjustment to ONE item's quantity (`change_qty` positive = in, negative = out; zero is rejected) with a `reason` of your own. **Superseded for quantity work — use `stock_items.set_quantities`.** Requires `actor.id`; the ledger row is written first and reversed if the quantity write fails. | ### 3.3 Tool input shapes (condensed) @@ -321,7 +323,7 @@ from `sent` → `received`). Whitelist: `sku`, `description`, `notes`, `unit`, `selling_price`, `markup`. Anything else returns `reason: "forbidden_field"`. For inventory qty use -`stock_items.adjust`. Returns `{ok: true, item, previous}` on success or +`stock_items.set_quantities`. Returns `{ok: true, item, previous}` on success or `{ok: false, reason, message, field?}` on rejection. `stock_items.set_pricing` (required: `updates`; `actor.id` is required at runtime and must be a 15-char user id — it is written to `stock_ledger.created_by`, a relation to `users`; `actor.name` defaults to the id): @@ -346,6 +348,29 @@ thrown. `stock_items.create` also accepts optional `cost_price`, `selling_price` and `markup`; a cost with neither selling price nor markup gets the app default markup (80%). +`stock_items.set_quantities` (required: `mode`, `lines`; `actor.id` is required at runtime and must be a 15-char user id — it is written to `stock_ledger.created_by`, a relation to `users`; `actor.name` defaults to the id): + +```json +{ + "mode": "set", + "lines": [ + { "sku": "HYD68-20", "qty": 50 }, + { "description": "M6 x 40mm hex bolt", "qty": 120 } + ], + "dry_run": false, + "actor": { "id": "user-id", "name": "Dave" } +} +``` + +`mode` is `"set"` (the number is the new quantity on hand) or `"add"` (the +number is added to the current quantity). `lines` holds 1–100 entries; each +needs a numeric `qty`, plus a `sku` or a `description` to match on. A +repeated SKU rejects the whole batch — split the paste. `dry_run: true` +plans the change and writes nothing at all. Quantities are truncated to whole +units and may be negative — the till oversells on purpose. A line that cannot +be resolved is reported, never guessed. Every `stock_ledger` row is stamped +with the acting user. + `bulk_update_quote_items` (required: `items`): ```json @@ -490,7 +515,7 @@ keys aren't set, so a clean checkout reports `passed, N skipped`. -This catalogue is auto-generated from the `tools` array in `src/app/api/mcp/route.ts` by `scripts/mcp-doc-gen.py`. There are **144 tools** as of this run. Re-run the script after adding or renaming a tool. +This catalogue is auto-generated from the `tools` array in `src/app/api/mcp/route.ts` by `scripts/mcp-doc-gen.py`. There are **148 tools** as of this run. Re-run the script after adding or renaming a tool. Grouped by resource family. The full description is in the table column; the description text the agent sees is preserved verbatim. @@ -513,7 +538,8 @@ Grouped by resource family. The full description is in the table column; the des | `orders.mark_delivered` | Mark every eligible line item on the order as `delivered`, stamping `delivered_at` and `delivered_date`. Mirrors the REST POST /api/v1/orders/{id}/mark_delivered endpoint. Eligible statuses: `ready_delivery \| collected \| with_driver \| ch... | | `orders.update` | Patch whitelisted header fields on an order. Mirrors the REST PATCH /api/v1/orders/{id} endpoint. Whitelist (from `ORDER_PATCHABLE_FIELDS`): `notes`, `contact_name`, `description`, `rep`, `email_cc`, `email_bcc`, `quote_reference_text`, ... | | `orders.set_quote` | Link an EXISTING order to an EXISTING quote. Mirrors the REST POST /api/v1/orders/{id}/quote endpoint. Why this exists: only `createOrderFromQuote` writes the `orders.quote` relation today, so walk-in orders leave it null. When Hermes di... | -| `orders.quote_match_sweep` | Run quote auto-detection over orders that have never been attempted. Mirrors the REST POST /api/v1/orders/quote-match/sweep endpoint. Why this exists: orders created from now on are matched automatically at intake. This is the catch-up p... | +| `orders.assistant_match` | Run the AI order assistant on one order now: it works out which quote the order came from, and links the order to that quote when it is sure. Mirrors the REST POST /api/v1/orders/{id}/assistant/run endpoint. Auto-linking happens only whe... | +| `orders.quote_match_sweep` | Run the AI order assistant over unlinked orders from the last 30 days. Mirrors the REST POST /api/v1/orders/quote-match/sweep endpoint. Catch-up path: new orders are matched by the assistant at intake (when it is switched on) and by its ... | | `orders.add_note` | REPLACE the `notes` field on an order with `text`. Thin wrapper over `orders.update` (`patch: { notes: text }`). The `notes` field is a free-text header field, distinct from the per-line `order_items.notes` JSON audit feed. If you want t... | | `orders.add_attachment` | Upload a single attachment (proof, drawing, signed PO, supplier quote) to an order. The file is stored on the order's OWN `order_attachements` multi-file field (PB-side misspelling kept in sync with the live schema) — the same field the ... | | `orders.list_attachments` | List the attachments associated with an order. Merges two sources: (a) files written to the order's own `order_attachements` multi-file field — operator uploads via `orders.add_attachment` and counter-sale photos from the in-store PWA; a... | @@ -558,8 +584,8 @@ Grouped by resource family. The full description is in the table column; the des | `quotes.partial_approve` | Mark a subset of quote items as `approved` (or `rejected`) on a quote. Body: `{ quote_id, decisions: [{ item_id, decision }] }`. Useful when the customer accepts some lines but not others. | | `quotes.submit` | Flip a `new` quote to `approval_pending` once every line is priced. Idempotent — re-submitting an already-`approval_pending` or `partial_approval_pending` quote is a no-op (returns `statusChanged: false`). Gated: every line on the quote ... | | `quotes.re_price` | Re-run pricing on every quote_item under a quote. Whitelist of patchable fields is the same as `bulk_update_quote_items`. Returns the per-row result list. | -| `quotes.assistant_fill` | Run the AI quote assistant on one quote now (`{ quote_id }`): fills unpriced lines from matching POS items as purple, unreviewed drafts and notes the closest items on unmatched lines. Only quotes with status new / follow_up / claimed, never templates; at most 15 lines per run. Problems come back in `error` (not thrown). Never marks lines done, changes the quote status or sends anything. Wraps `fillQuote` (same as `POST /api/v1/quotes/[id]/assistant/run`). | | `quotes.next_number` | Reserve the next quote number (`Q{yy}{mm}{NNN}`). Atomic counter; gaps are possible if the caller never persists the resulting draft. | +| `quotes.assistant_fill` | Run the AI quote assistant on one quote now: fills unpriced lines from matching POS items (purple, unreviewed) and notes the closest items for unmatched lines. Never marks lines done or sends anything. | | `quotes.lookup` | Free-text lookup over quotes for the order page's quote-picker dropdown. Substring match across quote_number, quote_reference, rfq_number, customer name, company name. Excludes templates by default. | | `quotes.list_attachments` | List all attachments on a quote (the generated PDF, original RFQ email attachments, operator-uploaded proofs, outbound attachments). Newest first. Returns up to 50 per page. | | `quotes.get_pdf` | Retrieve the rendered quote PDF as a base64 string in the MCP response. The PDF is auto-rendered if one doesn't already exist on the quote. Pass `letterhead` to force CNL or ISHERWOOD; defaults to the quote's stored `company_letterhead`.... | @@ -657,9 +683,10 @@ Grouped by resource family. The full description is in the table column; the des | `stock_items.list` | List stock_items. Supports substring search on name/sku/description, low-stock filter (`current_qty <= min_qty AND is_active = true`), pagination, and `expand` (defaults to `supplier`). | | `stock_items.get` | Fetch a single stock_item by id. The response includes the item (expand=supplier) and a paginated recent-ledger slice (`ledger.page`, `ledger.items`, etc.). Pass `ledger=false` to skip the ledger fetch. | | `stock_items.create` | Create a new stock_item (a POS/till item). This is the DEFAULT place to put a product and its prices — use it unless the user explicitly asked for the price list / catalogue. The till reads `selling_price` from this collection and nothin... | -| `stock_items.adjust` | Record a manual stock adjustment. Inserts a `stock_ledger` row and updates `current_qty` on the parent item. `change_qty` is signed (positive = in, negative = out); zero is rejected. `reason` must be one of STOCK_REASON_VALUES. | -| `stock_items.update` | Patch catalogue fields on a master stock_item: sku, description, notes, unit, selling_price, markup. For inventory qty use stock_items.adjust. Returns {ok: true, item, previous} on success or {ok: false, reason, message, field?} on rejec... | +| `stock_items.adjust` | Record a manual stock adjustment on ONE stock item: `change_qty` is signed (positive = in, negative = out) and zero is rejected. `reason` must be one of STOCK_REASON_VALUES and is kept on the ledger row. `actor.id` is required — the ledg... | +| `stock_items.update` | Patch catalogue fields on a master stock_item: sku, description, notes, unit, selling_price, markup. For inventory qty use stock_items.set_quantities. Returns {ok: true, item, previous} on success or {ok: false, reason, message, field?} ... | | `stock_items.set_pricing` | Set cost price / selling price / markup on existing stock_items in one batch (max 100 per call). The POS till reads `selling_price` (NOT markup), so set both consistently: selling = cost × (1 + markup/100). Cost changes are audited in st... | +| `stock_items.set_quantities` | Set or change the stock quantity on one or more POS/stock items, matched by SKU and description. | ### Collection pool @@ -673,6 +700,7 @@ Grouped by resource family. The full description is in the table column; the des | --- | --- | | `collection_confirm.list` | List every order_item and stock_purchase_item currently in the 'collected' status (the buyer-side collection_confirm queue). Returns a unified row shape with `source: 'customer' \| 'stock'` so the caller can filter / process each kind sep... | | `collection_confirm.upload_invoice` | Upload a supplier invoice image (or PDF) and attach it to one or more order_items' `order_item_attachments` file field. Use this as the second step of the buyer-side collection-confirm flow: (1) `stock_purchases.match_and_receive_from_in... | +| `collection_confirm.ai_check` | Run the AI document check on one Collection Confirm card (one PO) now: reads the driver's invoice/delivery-note photos, pairs document lines with PO lines, fills an empty line SKU and records `ai_check` per line. When a line it is sure a... | | `collection_confirm.confirm_order_items` | Mark a batch of order_items as confirmed (item_status `collected` → `ready_invoice`). Stamps the audit fields (`notes`, `order_item_details`) with the supplier invoice number + date and the agent's identity. Optional `cost_overrides` pat... | ### Sprint @@ -747,6 +775,6 @@ Grouped by resource family. The full description is in the table column; the des ### Alphabetical index -`ai_insights.rerun`, `ai_settings.get`, `ai_settings.update`, `attachments.upload_for_quote`, `audit_log.get`, `bulk_update_quote_items`, `buyer_tasks.generate_po`, `buyer_tasks.list`, `buyer_tasks.mark_ready`, `collection_confirm.confirm_order_items`, `collection_confirm.list`, `collection_confirm.upload_invoice`, `collection_pool.list`, `companies.create`, `companies.delete`, `companies.get`, `companies.list`, `companies.update`, `customers.create`, `customers.delete`, `customers.get`, `customers.list`, `customers.update`, `dashboard.daily_summary`, `delivery_notes.cancel`, `delivery_notes.combine`, `delivery_notes.create`, `delivery_notes.create_quote`, `delivery_notes.list`, `delivery_notes.mark_complete`, `delivery_notes.mark_invoiced`, `delivery_notes.regenerate_pdf`, `delivery_notes.set_customer_po`, `delivery_notes.set_item_quantity`, `delivery_notes.set_order`, `drivers.list_stops`, `drivers.update_stop_status`, `files.fetch`, `order_items.confirm_collected`, `order_items.create`, `order_items.get`, `order_items.list`, `order_items.update`, `order_items.update_status`, `orders.add_attachment`, `orders.add_note`, `orders.archive`, `orders.cancel`, `orders.create`, `orders.create_from_quote`, `orders.daily_summary`, `orders.find_by_email_message_id`, `orders.find_by_order_number`, `orders.find_duplicates`, `orders.get`, `orders.list`, `orders.list_attachments`, `orders.mark_delivered`, `orders.mark_invoiced`, `orders.matched_attachments`, `orders.quote_match_sweep`, `orders.review`, `orders.review_save`, `orders.set_quote`, `orders.split`, `orders.sprint_counts`, `orders.stats`, `orders.update`, `price_list.add_items`, `purchase_orders.add_attachment`, `purchase_orders.diff`, `purchase_orders.get`, `purchase_orders.list`, `purchase_orders.record_diff_report`, `purchase_orders.record_send_outcome`, `purchase_orders.retry_email`, `purchase_orders.scan`, `purchase_orders.send_email`, `purchase_orders.stale`, `quote_items.create`, `quote_items.get`, `quote_items.list`, `quote_items.populate_insights`, `quote_items.update`, `quotes.append_reply`, `quotes.assistant_fill`, `quotes.clone`, `quotes.create`, `quotes.create_from_email`, `quotes.create_from_template`, `quotes.find_by_message_id`, `quotes.get`, `quotes.get_pdf`, `quotes.ingest_from_email`, `quotes.list`, `quotes.list_attachments`, `quotes.lookup`, `quotes.next_number`, `quotes.partial_approve`, `quotes.process_pending`, `quotes.re_price`, `quotes.re_run_ai`, `quotes.retry_email`, `quotes.save_as_template`, `quotes.send_email`, `quotes.split`, `quotes.submit`, `quotes.update`, `sprint.attachment_counts`, `sprint.claim`, `sprint.item_attachments`, `sprint.list`, `stock.fulfill`, `stock.surplus_book`, `stock_items.adjust`, `stock_items.create`, `stock_items.get`, `stock_items.list`, `stock_items.set_pricing`, `stock_items.update`, `stock_purchase_items.assign_driver`, `stock_purchase_items.bulk_update`, `stock_purchase_items.list`, `stock_purchase_items.mark_collected`, `stock_purchase_items.update`, `stock_purchases.add_attachment`, `stock_purchases.create`, `stock_purchases.get`, `stock_purchases.list`, `stock_purchases.match_and_receive_from_invoice`, `stock_purchases.next_number`, `stock_purchases.receive`, `stock_purchases.retry_email`, `stock_purchases.send`, `stock_purchases.send_email`, `stock_quick_draft.add_line`, `stock_quick_draft.search`, `stock_quick_draft.suppliers`, `suppliers.create`, `suppliers.delete`, `suppliers.get`, `suppliers.list`, `suppliers.update`, `tools.discovery`, `users.lookup` +`ai_insights.rerun`, `ai_settings.get`, `ai_settings.update`, `attachments.upload_for_quote`, `audit_log.get`, `bulk_update_quote_items`, `buyer_tasks.generate_po`, `buyer_tasks.list`, `buyer_tasks.mark_ready`, `collection_confirm.ai_check`, `collection_confirm.confirm_order_items`, `collection_confirm.list`, `collection_confirm.upload_invoice`, `collection_pool.list`, `companies.create`, `companies.delete`, `companies.get`, `companies.list`, `companies.update`, `customers.create`, `customers.delete`, `customers.get`, `customers.list`, `customers.update`, `dashboard.daily_summary`, `delivery_notes.cancel`, `delivery_notes.combine`, `delivery_notes.create`, `delivery_notes.create_quote`, `delivery_notes.list`, `delivery_notes.mark_complete`, `delivery_notes.mark_invoiced`, `delivery_notes.regenerate_pdf`, `delivery_notes.set_customer_po`, `delivery_notes.set_item_quantity`, `delivery_notes.set_order`, `drivers.list_stops`, `drivers.update_stop_status`, `files.fetch`, `order_items.confirm_collected`, `order_items.create`, `order_items.get`, `order_items.list`, `order_items.update`, `order_items.update_status`, `orders.add_attachment`, `orders.add_note`, `orders.archive`, `orders.assistant_match`, `orders.cancel`, `orders.create`, `orders.create_from_quote`, `orders.daily_summary`, `orders.find_by_email_message_id`, `orders.find_by_order_number`, `orders.find_duplicates`, `orders.get`, `orders.list`, `orders.list_attachments`, `orders.mark_delivered`, `orders.mark_invoiced`, `orders.matched_attachments`, `orders.quote_match_sweep`, `orders.review`, `orders.review_save`, `orders.set_quote`, `orders.split`, `orders.sprint_counts`, `orders.stats`, `orders.update`, `price_list.add_items`, `purchase_orders.add_attachment`, `purchase_orders.diff`, `purchase_orders.get`, `purchase_orders.list`, `purchase_orders.record_diff_report`, `purchase_orders.record_send_outcome`, `purchase_orders.retry_email`, `purchase_orders.scan`, `purchase_orders.send_email`, `purchase_orders.stale`, `quote_items.create`, `quote_items.get`, `quote_items.list`, `quote_items.populate_insights`, `quote_items.update`, `quotes.append_reply`, `quotes.assistant_fill`, `quotes.clone`, `quotes.create`, `quotes.create_from_email`, `quotes.create_from_template`, `quotes.find_by_message_id`, `quotes.get`, `quotes.get_pdf`, `quotes.ingest_from_email`, `quotes.list`, `quotes.list_attachments`, `quotes.lookup`, `quotes.next_number`, `quotes.partial_approve`, `quotes.process_pending`, `quotes.re_price`, `quotes.re_run_ai`, `quotes.retry_email`, `quotes.save_as_template`, `quotes.send_email`, `quotes.split`, `quotes.submit`, `quotes.update`, `sprint.attachment_counts`, `sprint.claim`, `sprint.item_attachments`, `sprint.list`, `stock.fulfill`, `stock.surplus_book`, `stock_items.adjust`, `stock_items.create`, `stock_items.get`, `stock_items.list`, `stock_items.set_pricing`, `stock_items.set_quantities`, `stock_items.update`, `stock_purchase_items.assign_driver`, `stock_purchase_items.bulk_update`, `stock_purchase_items.list`, `stock_purchase_items.mark_collected`, `stock_purchase_items.update`, `stock_purchases.add_attachment`, `stock_purchases.create`, `stock_purchases.get`, `stock_purchases.list`, `stock_purchases.match_and_receive_from_invoice`, `stock_purchases.next_number`, `stock_purchases.receive`, `stock_purchases.retry_email`, `stock_purchases.send`, `stock_purchases.send_email`, `stock_quick_draft.add_line`, `stock_quick_draft.search`, `stock_quick_draft.suppliers`, `suppliers.create`, `suppliers.delete`, `suppliers.get`, `suppliers.list`, `suppliers.update`, `tools.discovery`, `users.lookup` \ No newline at end of file