The till was painting all 373 active stock items at once, which is slow
and buries the stock staff actually reach for. The catalog now comes back
newest-first (PosCatalogItem carries created) and renders 50 at a time,
with a Show-more button that also auto-loads as it scrolls into view.
Header reads 'Recently added' when browsing and 'Results' once a search
or category is applied; changing either resets paging to the first chunk.
Scroll position is preserved across re-renders so adding to the cart no
longer throws the operator back to the top of the list.
Co-Authored-By: Claude <noreply@anthropic.com>
The Include-prices toggle drives both the on-screen slip and the
Bluetooth thermal print, so what prints matches what the operator saw.
Co-Authored-By: Claude <noreply@anthropic.com>
Method split now reads pos_sales.payment_method directly. The four
method rows cashup already renders (cash/card/eft/account) would have
been permanently R0 for eft and account under a cash/card-only backend.
Co-Authored-By: Claude <noreply@anthropic.com>
Payment method now read from pos_sales.payment_method rather than
sniffed out of an order's notes with regexes.
Co-Authored-By: Claude <noreply@anthropic.com>
Sends client_ref (the till's localId) so the offline queue can retry a
sale that already landed without recording it twice. Customer search
moves to /api/v1/pos/customers and now carries the company name for the
delivery-note slip.
Co-Authored-By: Claude <noreply@anthropic.com>
pb-config.js deliberately not copied — it hardcoded a PocketBase
superuser email/password in client-shipped JS.
Co-Authored-By: Claude <noreply@anthropic.com>
All three gated on POS_API_KEY. Checkout accepts all four tender types
and forwards client_ref for idempotent offline-queue replays.
Co-Authored-By: Claude <noreply@anthropic.com>
Records all four tender types. Only cash sets cash_given/change_given;
card, EFT and account settle outside the till, so cashup can reconcile
the physical drawer. Stock decrements for every tender including
account, since the goods leave on a delivery note.
client_ref makes checkout idempotent: an offline-queued sale that was
already committed server-side replays to the original record instead of
decrementing stock twice.
Co-Authored-By: Claude <noreply@anthropic.com>
Supports all four POS tender types (cash/card/eft/account) rather than
just cash/card: only cash puts money in the drawer, so cashup needs the
distinction. PosSale.client_ref carries the till's local sale id as an
idempotency key for the offline queue.
Co-Authored-By: Claude <noreply@anthropic.com>
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.
The route accepted ?skip=N but always called getList(1, ...) — every
chunk re-processed the FIRST limit orders, leaving offsets > limit
unprocessed. Convert skip to page (floor(skip/limit) + 1) and add a
regression test asserting getList is called with the right page.
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>
Scopes hardening the existing POS mockup (superuser creds removed,
catalog moved to stock_items, checkout wired to stock_ledger) as a
route inside WFDASH-WEBAPP rather than a separate service.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The backfill is N+1-bounded (each order = 1 + N candidate queries),
so a 5,300-order table takes ~3 hours at ~25 orders/min on a busy
PB instance. Splitting into 200-order chunks keeps each call under
Next.js's maxDuration (10 min) and gives the operator a clear
progress signal: every chunk returns nextSkip + done.
Operator-driven soft delete at item scope — flips item_status to
'cancelled' rather than removing the PB row, so any PO / DN /
invoice history the line carries is preserved for audit.
Mirrors cancelOrder's blocked-status list (delivered / invoiced /
cancelled): a line that's already reached the customer can't be
soft-deleted without orphaning the customer-facing record — a UI
gate should prevent the call from reaching the service, but the
service gate is the last line of defence.
The exclude_from_invoice flag doesn't affect eligibility — a normal
line and a non-invoiceable extra cancel the same way. The flag
only governs what reaches the customer invoice / delivery note.
When an actor is supplied, stamps [ISO timestamp] cancelled by
{name|id} on the row's notes field (appended, not replaced — preserves
any operator note that was already on the line). Soft delete leaves
a full audit trail in PB.
5 tests + 1 mutation-test prove:
- pending → cancelled (happy path)
- 404 when order doesn't exist
- 404 when item belongs to a different order
- 409 when item is already terminal (invoiced)
- actor stamp lands on notes
- mutation (don't flip the status): the happy-path test fails
Co-Authored-By: Claude <noreply@anthropic.com>
Three documents behind the two features on this branch:
- specs/2026-08-30-non-invoiceable-order-extras-design.md — the design
the implementation followed, including why the flag must be named for
the exception (a positively-named field defaults to false and would
mark every existing row non-billable the moment it is created).
- plans/2026-08-30-non-invoiceable-order-extras.md — the task-by-task
plan, now fully implemented.
- specs/2026-08-30-quantity-handling-audit.md — the order_qty /
quantity audit that produced the preceding fix, plus the partial
collection findings that are still open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review follow-up to the non-invoiceable-extras work. Task 6 of the plan
called for the billable-subset guard on `sendToQuickBooks` as well as
in the modals; it landed in the modals only.
`rows.length === 0` counts excluded rows, which ride along for the n8n
status flip but never bill. A group where every row is excluded passes
that check and ships an empty `items` array, so QuickBooks would create
a zero-line invoice while n8n still flipped every row to `invoiced`.
Not reachable through the current UI — both live modals disable submit
on this case. This is defence-in-depth for the callers that bypass
them: the (currently unrendered) bulk modal and any legacy call passing
`overrides: undefined`, which falls back to every row in the group.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`order_items.order_qty` is the SUPPLIER-facing quantity — what prints
on the PO. Five call sites read it as the customer quantity, which is a
money bug in both directions:
- supplier MOQ (customer 4, we buy 10) → invoiced 10, over-billing 6
- shorted line (customer 100, supplier covers 40) → invoiced 40,
under-billing 60 until the operator splits the remainder
The invoicing screen never fetched `quantity` at all (zero occurrences
in page.tsx), so it could not bill the customer quantity even in
principle. Added to the PB field projection, which is load-bearing:
drop it and PB returns undefined and every line silently falls back to
order_qty. A page-level test now fails if the projection drifts.
`maxSurplusFor` measured the surplus as `collected - order_qty`, which
on an MOQ line is always 0 — the overflow was mathematically unbookable
and vanished from inventory by construction, not by operator error.
Four call sites built delivery-note lines by hand; two passed
`quantity` and two forgot it, so the same DN printed the customer's 4
via the service layer and the supplier's 10 via the API routes. All
four now share `buildDeliveryNoteLine`, so they cannot diverge again.
`order_qty` had no doc comment at all next to an 11-line one on
`quantity` — that absence is why five subsystems guessed differently.
Now documented, with a new `src/lib/order-item-qty.ts` holding
`customerQty` / `supplierQty` as the single definition.
driver-stops.ts and collection-confirm/po-card.tsx read `order_qty`
CORRECTLY (they check what the supplier delivered against what we
ordered from them) and carry comments saying so, so the next cleanup
doesn't "fix" them into breaking.
23 new tests. Full suite: no regressions — the 15 failures are
identical before and after (verified by stashing and re-running).
Audit: docs/superpowers/specs/2026-08-30-quantity-handling-audit.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 9 of the non-invoiceable-order-extras plan.
order_items.create MCP tool:
- Input schema gains an optional boolean 'exclude_from_invoice'.
- Tool description rewritten to explain the supplier-side component
use case, the PO + driver + collection flow, the silent drop from
the customer invoice and delivery note, and the force-zero selling
price.
- operationData hint / nextActions block updated — the prior text
documented a '409 order_locked' refusal that no longer fires
(Task 2 removed the pending gate).
- Handler forwards the boolean to createOrderItemForOrder (which
itself forces selling_price=0 when the flag is set — Task 2).
Also fixed a TypeScript strict-mode complaint in the T7 test that
the previous commit left behind (vi.fn mock-call tuple type
needed an explicit parameter so mock.calls[0] is typed).
Co-Authored-By: Claude <noreply@anthropic.com>
Task 8 of the non-invoiceable-order-extras plan.
Two regression tests, no production code change. The picking-slip
feature relies on a property of the existing print pipeline:
excluded rows DO receive item_status = 'invoiced' (Task 7 / n8n
batch flip) and DO carry the invoice number / DN number, so the
print queries pick them up automatically. The picker must still
pull the washers even though they never reach the customer.
- print-bundle: explicitly verifies the picking slip renderer is
called with both billable and excluded rows (mocked
PickingSlipPdf → assert lines array has both descriptions).
- delivery-note-print: explicitly verifies the PB filter does NOT
carry an exclude_from_invoice clause — adding one would
silently filter the picker slip and break Task 7's design.
Co-Authored-By: Claude <noreply@anthropic.com>