The runbook and AGENTS.md described the old word-only shortlist and
did not mention the shared matcher. Both now say how the size is used,
that a price is written only for an exact match, and what
ai_fill.closest holds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rule-based matcher no longer writes or shows suggestions: on
2026-10-24 it had left 2,888 on unlinked orders, one quote suggested
to 309 of them.
- createOrder runs the AI order assistant (matchOrder) straight after
intake when it is switched on, instead of the rule-based suggestion
- order page: Re-match / Try again and the rule-based suggestion card
are gone; Match with AI is the action, and "Match by reference" saves
the reference then runs the AI (the run route takes { reference })
- the unauthenticated /orders/[id]/quote-match route is removed
- the sweep route and MCP orders.quote_match_sweep run the AI batch
(matchUnlinkedOrders, shared with the tick)
- scripts/2026-10-24-clear-rule-quote-suggestions.ts clears the old
suggestions (dry run unless --apply); replaces the rematch script,
which would have written rule-based ones again
- also fixes the setState-in-effect lint error on the order page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runbook now matches the code as built.
- New section: filling the lines from the quote — which lines may be
filled, what is written, the 5% price doubt, the purple review and
how it ends, and the board badge.
- The "Fill lines from the quote" switch is no longer described as
reserved for a later phase, and the tick's bounds now say what the
code does: 10 orders matched, 10 orders filled, and an order with
nothing to do does not use up that budget.
- Reading order_items.ai_fill is documented alongside orders.ai_quote_match.
- Live steps confirmed against the code: the migration is
pb:migrate:order-assistant, ai_quote_match and ai_fill are both in
IGNORED_FIELDS, the n8n tick is POST /api/v1/orders/assistant/tick,
and the settings labels are "Order assistant enabled", "Run every
(minutes)", "Auto-link when sure" and "Fill lines from the quote".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Task 7 of the AI order assistant plan.
- POST /api/v1/orders/assistant/tick (n8n, assertAgentAuth)
- POST /api/v1/orders/{id}/assistant/run (browser session; 409 for
"AI is disabled" and "not set up yet")
- MCP orders.assistant_match { orderId } -> matchOrder(force) with a
TOOL_HINTS entry; deliberately NOT in READ_TOOLS
- docs/order-assistant.md runbook and the AGENTS.md section
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The driver app records the full quantity as received when it collects a
stock pickup, and Collection Confirm only listed stock lines that were
not fully received — so collected stock orders skipped it and sat at
"sent", outside the accountant's booking queue, until someone clicked
Mark received (29 stuck on 2026-10-19).
- STOCK_ON_PAGE_FILTER / stockLineOnPage: a collected stock line stays on
Collection Confirm until its stock order is received; cancelled orders
never show. Shared by the list and the AI check (so those lines get
photos read and ticks too).
- Confirm stock: markStockOrdersReceived flips each stock order to
"received" once every line is collected/booked (→ booking queue);
orders with lines still out stay open, reported as stillOpen. The
route returns { updatedIds, received, stillOpen }; the toast says so.
- Stock photos: `npm run pb:migrate:stock-photos` makes
stock_purchases.additional_attachments multi-file;
addStockPurchaseAttachment, Move photo and Delete photo add/remove with
`+`/`-` instead of replacing the single slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Delete photo next to "Add supplier invoice" removes the photo open in
the previewer (wrong upload, or one of several for the same PO) after an
inline "Delete …? Yes, delete" confirm; the confirm resets when you
switch photos. Not offered for the PO PDF.
- POST /api/v1/collection-confirm/delete-photo (browser session) →
deleteCollectionPhoto: takes the file off every order line carrying it
(or the stock purchase's photo slot) and deletes the AI's reads of it
plus the checks built on them (shared forgetReads with Move photo).
- Removed the header's blue "Run agent scan" button and its results panel
(not in use); the /api/v1/agent/scan route stays for the agent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drivers photograph invoices per item and can't see which order a photo
lands on. B&R invoice 00282979 (stock order PO26100702STK) ended up on
the thinners line of PO202610017BLY; the AI read "PO26100702STK" off it
but the card only said "not found".
- The card list now carries each read's po_refs and source file, and sets
ai_doc.other_po when the document names a PO that exists and isn't the
row's PO (none named, or this card's PO among them: no hint).
- PO card: "Photo may be for <PO>" in the header; when open, "Invoice <no>
is for <PO>, not this PO" with a Move photo button. Nothing moves on its
own.
- POST /api/v1/collection-confirm/move-photo (browser session) →
movePhotoToPo: copies the photo to the target (a collected line of a
customer PO, or the stock purchase's single photo slot — refused when
taken), removes it from the source, deletes the AI's read of it and the
checks built on it so it is re-read where it now belongs.
- normalizePoReference moves to the client-safe src/lib/po-reference.ts
(re-exported from the matching service).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plumblink hands out delivery notes with prices on them. The Price tick
was always grey on a delivery note, so a card could go purple without
its prices ever being checked, and the AI's reading notes ("delivery
note, not a tax invoice", "year not printed") were never shown.
- Price tick: compare whenever the document line has a price, invoice
or delivery note; grey only when no price is printed. The AI still
only corrects a cost from a tax invoice.
- ai_doc now carries the read's notes; cardNotes() unions them per card
and the AI findings box lists them under "Reading notes" (the box now
shows when there are notes but no findings).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HYCO (I009753) prints full prices on the lines and "Discount 25.00%" in
the totals; Swagelok (622831) has a "Disc %" column on each line. The AI
compared our cost (already the discounted price) with the printed price,
so the Price tick went red on both.
- The read now captures a line's discount_percent and a whole-document
doc_discount_percent (prompt rule 5b) and stores net_unit_price on each
line (both discounts stacked).
- The Price tick compares the cost with the price after discount; the
tooltip and the document-lines panel spell it out
("R15.10 less 25% invoice discount = R11.33").
- Owner's call: when a "sure" tax-invoice line has a discount and our
cost isn't the discounted price, the AI sets the cost to it (customer
lines through updateOrderItemCost, which adds a cost note; stock lines
through updateStockPurchaseItem) and records why in ai_check.cost_changed,
shown in purple under the line. No discount, a delivery note or a
"likely" match: the cost is never touched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F2: tick table (SKU green via the code match incl. AI fills, grey per tick,
pairing failure -> all grey, different product -> red), stock lines use the
line SKU, Add to POS needs a SKU, SKU fills audited for order lines only.
F3: content_hash is a non-unique index (identical uploads share a hash,
skipped PDFs have an empty hash) - matches the migration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Runbook: pre-migration settings don't persist (not "saves fail"),
eligible statuses, 15 lines per quote, audit attribution, clone/template
behaviour, supplier kept, doubts dot, confidence guidance (Medium is the
default; raise to High if reps often change AI prices).
- quotes.assistant_fill added to docs/api-mcp-layer.md,
docs/audit/api-mcp-coverage.md and the Giellie skill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- decideWithAi throws on stop_reason max_tokens instead of returning a
partial answer; fillQuote marks a requested line the AI did not answer
as error (retried after 1 h), never a permanent no_match.
- QUOTE_MAX_LINES 30 -> 15; the tick also caps lines per quote so the
60-line budget reaches other quotes.
- runAssistantTick stops starting quotes after a 90 s time budget and
returns reason running while a newer start than the last summary is
under 10 minutes old (lease).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quote assistant card in Settings, AI (on/off, interval, min confidence,
editable prompt with reset, last-run line); parseSettingsForm handles the
qa_* fields. docs/quote-assistant.md has the runbook and the n8n tick
workflow spec; AGENTS.md gets a short section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aligns the tool policy, the system prompt, the MCP route and the skill
doc so they describe the same boundary, with tests pinning the policy
and prompt text.
The docs and the executable policy had drifted apart — the skill doc
advertised capabilities the policy does not grant, which is worse than
not documenting them: an agent reading the doc will plan around a tool
it isn't allowed to call.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
actor.id must be a 15-char user id (stock_ledger.created_by is a relation to
users; a non-user id left the cost patched but unaudited). Each updates[i].id
is validated, actor.name defaults to the actor id, and the Giellie prompt now
says to pass both selling_price and markup on create and to read the item
before a partial price change. Adds tests for null / numeric-string prices
and the explicit markup:0 default-markup behaviour.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Added an audit trail feature that logs every create, update, and delete action on delivery notes and orders, capturing detailed changes per field.
- Introduced the `AuditTimeline` component to display the history of changes for delivery notes and orders, enhancing traceability.
- Updated the `page.tsx` files for delivery notes and orders to fetch and display audit logs, ensuring users can view the history of modifications.
- Enhanced the `package.json` with a new migration script for setting up the audit log functionality.
This feature improves accountability and transparency in the management of delivery notes and orders.
Splits the quote attachment model along the seam that actually matters:
'operator-only references' (drawings the operator consulted, supplier
emails they filed away) vs 'customer-facing files' (supplier quotes,
drawings, NDAs that need to ride along on the email).
Schema (PB): new `internal_attachments` + `outbound_attachments` fields
on the `quotes` collection, maxSelect 10 each. Existing `attachments`
field kept for back-compat but is no longer the source of truth for
outbound emails.
API:
- GET/POST/DELETE /api/v1/quotes/[id]/internal-attachments[/{filename}]
- GET/POST/DELETE /api/v1/quotes/[id]/outbound-attachments[/{filename}]
- POST /api/v1/quotes/[id]/attachments/upload kept for legacy `attachments`
MCP: `quotes.send_email` description now correctly states
`selectedAttachmentIds` are PB file tokens from `outbound_attachments`
(supplier quotes, drawings, NDAs), not IDs of the legacy `attachments`
collection.
Frontend: `ProcessOutboundAttachmentsCard` component lets the operator
upload/remove customer-facing files during the process page; approval +
process pages now read the new fields.
Outbound Send workflow (deployed as `PIEAkPYYYZLGl6Pj`):
- Old workflow (id `WF_Email_Send_SMTP`) and stale repo file
(`WFDASH — Quote Outbound Send.json`) replaced.
- New revision intersects `quotes.outbound_attachments` with
`email_outbound_attachment_ids`, downloads each from
`/api/files/quotes/<id>/<token>`, and emits
`options.attachments: [{name, content, type}]` consumed by
`Send Email (SMTP)` (PO Send pattern).
- Failure path flipped from `send_failed` to `mail_failed` so
`retryQuoteEmail` can find failed sends (it only accepts the
latter). Previously failed sends were stranded.
Email Monitor (v5) workflow fixes:
- Reply-branch auth: any authenticated request now passes through;
pre-fix bug rejected branches that the IMAP trigger hadn't seen yet.
- Reply detection: bulletproof subject regex
(/Q-?\d{4}-?\d{2}-?\d{3}/) accepts both legacy `Q-2026-09-246`
and current `Q202609246` formats; canonicalised digits-only for
PB `~` substring match.
Delivery notes: new pricing endpoint at
/api/v1/delivery-notes/items/[id]/pricing + service-layer methods.
Snapshots kept for forensics: PRE-FIX-REPLY-{BRANCH-AUTH,DETECTION}.json
and push-payload.baseline.json are the previously-deployed workflow
shapes, retained so we can diff/rollback if regression hits.
Verified:
- PUT workflow PIEAkPYYYZLGl6Pj -> 200, active, 25 nodes
- quotes-service.test.ts: 117 pass (4 pre-existing failures unchanged)
- mcp/route.test.ts: 118 pass (2 pre-existing failures unchanged)
- node --check on new Code node JS passes
Docs: docs/quotes-outbound-attachments-deploy.md covers the deploy
procedure; docs/api-mcp-layer.md description sync.
PocketBase treats `max: 0` on a text field as "use the default 5000-char cap", not unlimited. A Redpath reply with a 5029-char body 400'd on every 2-minute poll for hours on 2026-09-08, wedging every RFQ behind it because the failed email stayed UNSEEN.
Added clampEmailBody (with a truncation marker) and routed email_body writes through it so oversized replies still become quotes instead of blocking the queue. The untruncated body remains in the mailbox, and attachments carry the detail. Raise this cap in lockstep with any PB admin change to the field's max.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Groundwork for the rep visit check-in feature: a rep taps "I'm on site",
the vehicle's tracker position surfaces which customers are there, and
the visit is logged against them.
Pure/unit-testable layers only — no PocketBase writes and no UI yet,
both of which are blocked on schema that has to be added in the PB
admin first. Design doc: docs/superpowers/specs/2026-09-05-rep-visit-check-in-design.md
- lib/visit-matching.ts rankNearbyCustomers: radius match around a
point, rep's own customers first
- lib/visit-stops.ts detectStops: breadcrumb trail -> stops.
Anchor-based clustering rather than a running
centroid, which would let a slow crawl chain
into one bogus stationary stop
- lib/tracker-parsing.ts Netstar FleetAI response decoding
- lib/server/tracker-service.ts
FleetAiAdapter + FakeTrackerAdapter behind a
TrackerAdapter seam. The fake is auto-selected
when no credentials are configured, so the rest
of the feature builds and tests without vendor
access.
- lib/geo.ts isRealGeoPoint, lifting the Null-Island guard
that was inlined identically in customers/[id]
and companies/[id]
Three FleetAI conventions drive real code here and are each pinned by a
test: errors arrive as HTTP 200 with a failure envelope; "--" means
"unavailable" and is not null (and for IGN, not false); timestamps are
platform-local UTC+2 with no offset marker, so parsing them naively puts
every fresh fix two hours in the past.
FleetAI has no position-history endpoint, so the design polls
getTokenBaseLiveData (whole fleet in one call) and keeps its own trail.
scripts/fleetai-probe.mjs is a throwaway read-only probe to confirm the
three account-shaped assumptions before more is built on top.
102 tests pass. The 9 pre-existing failing test files elsewhere in the
suite were verified identical on a stashed tree and are unrelated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the reorderable block-list editor for sale/delivery/cash-up
slips, per-letterhead persistence, and the delivery-note pricing
default, worked out via brainstorming with the visual companion.
Added support for discounts in the POS checkout process, allowing operators to apply either a fixed amount or a percentage off the gross total. Updated the state management to include discount details and modified the receipt and cart rendering to display pre- and post-discount values. Enhanced the checkout API to handle discount inputs and ensure proper calculations. Updated relevant tests to cover new discount logic.
The 'Total {sumQty}{unitSuffix}' header line now explicitly documents
how mixed / missing units are handled: suffix is omitted when all
items have empty/null units (operator defaults to 'EA'); when units
are mixed, no suffix is shown; when all items share a non-EA unit
(e.g. BOX), the suffix is shown.
Self-review pass; no other changes.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Click-to-expand on each delivery-note row reveals an inline items
panel (description + qty) below the row. Operator can match the items
against the printed DN paper in their hand before loading. Pure
client-side render — items already loaded by /api/v1/deliveries are
filtered by delivery_note == dn.id. Empty state with fallback link to
/delivery-notes/[id] for DNs whose items aren't in the page slice.
Single-file edit on src/app/(app)/deliveries/page.tsx. No server,
schema, API, or MCP changes.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The plan + summary landed locally during the implementation but were
not committed alongside the code. Both documents describe the
rep-scoped Orders / Quotes / Invoicing / Delivery Notes list filter
work that already shipped (commits 8faa443, ed07617, 29eb003, 9cf8e39
etc).
- 2026-08-31-rep-scoped-list-filter.md — the implementation plan
- 2026-09-01-rep-scoped-list-filter-SUMMARY.md — post-implementation
summary with the commit log + verification notes + the known
limitation (Orders/Quotes tab counts are unfiltered for reps)
Co-Authored-By: Claude Code <noreply@anthropic.com>
Three bite-sized tasks with full code:
1. TDD the order-age util (test → impl → run)
2. Wire OrderAgeBadge into /orders (replace bare date)
3. Enrich /deliveries DN row subline (customer/order/paperclip)
All UI/rendering only. No server, no schema, no MCP changes.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Two small, independent rendering improvements for /deliveries and /orders:
1. /deliveries — each delivery-note row gets a compact subline with
customer name, company name, parent order number, and a signed-copy
indicator (paperclip + filename tooltip). All data already on the
row payload — no server-side changes.
2. /orders — order card right-aligned metadata slot gets a colour-
coded age badge (today / Xd old) replacing the bare date string.
New src/lib/order-age.ts as a pure function with bucket thresholds
(green <3d, amber 3-7d, orange 8-14d, red 15+d) and a unit test.
No PB schema changes. No /api/v1 route changes. No MCP tool changes.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Two spec-vs-implementation drifts caught by the final review:
- Move 'per-row validation → 200 with reason in results[]' out of the
'Reject 400' list. The route enforces body shape (json / empty /
too-many / bad id / missing actor) as 400; the service enforces
forbidden_field / non-finite / not_found / pb_error per row so one
bad sibling never aborts the rest.
- Move the 'realtime-conflict banner' from Page-behaviour to Out of
scope (the implementation deferred it; the spec still listed it).
Refine the handling of order_number values in the MCP API. Custom free-text order_numbers are now allowed to repeat across orders without triggering collision checks, while only values formatted as ORD-YYYY-MM-NNN will be subject to collision verification. Update tests to reflect this new behavior and ensure proper functionality.
Backend-first TDD plan (pos-auth, pos-service, /api/v1/pos/* routes)
followed by relocating and rewiring the existing static POS mockup
into public/pos/, removing the hardcoded PB superuser credentials.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>