Commit graph

305 commits

Author SHA1 Message Date
SJCoetz
690b8a2c26 fix(po-send): reject sends for POs without a generated PDF
The dashboard's send-email route fires the n8n webhook unconditionally
once pre-send validation passes. If a PO has no generated PDF (legacy
state — `documents` empty + `documents_history: [null]`), n8n tries to
download from PB with an empty filename, gets a 404, and the webhook
returns HTTP 500. The operator sees "Webhook returned HTTP 500. Status
reverted to mail_failed." with no indication of the actual cause.

Add a PDF-existence guard in `sendPurchaseOrderEmail` using the same
`documents` → `documents_history` fallback chain the route uses to
compute `latestFilename`. When both are empty / unusable, return a
`no_pdf` failure that flips the PO to `mail_failed` with a clear
operator-facing message BEFORE the webhook fires.

While here, rename the misleading `code: "no_pdf"` on the line-items
check to `code: "no_line_items"` — the old code name was wrong (it
checked for linked items, not PDFs) and would have collided with the
new `no_pdf` guard.

The new guard also filters null entries out of `documents_history`
before calling `.find()` / `.file` — the legacy `[null]` shape was
making the old route code throw.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 15:47:01 +02:00
SJCoetz
18e1d97b6c test(stock-orders): tighten total-value + edit-reset assertions
The total-value case now locates each cell by its column header so a
regression that swapped Ordered (R) and Received (R) would fail. The
edit-draft case now closes the modal after Edit and asserts
editPurchaseId resets to '', so a regression that leaked the id into
a subsequent 'New' click would fail.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 15:18:39 +02:00
SJCoetz
760f088ec9 test(stock-orders): move page tests to StockOrdersTable + add total-value case
The seven pre-existing assertions on the quick-draft modal and edit
button now exercise the new client component. New case confirms
ordered_value/received_value render in the row. Uses Intl-format
substring assertions because the currency glyph/spacing varies by
ICU version.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 15:13:12 +02:00
SJCoetz
b89a4a7fa3 fix(stock-orders): preserve header nav + sanitize error message
- Header (title, description, Reorder, + New stock order) now
  renders unconditionally, so operators can navigate away even when
  the list query throws.
- Full error is logged server-side via console.error; the user
  sees a generic 'Failed to load stock orders. Please try again.'
  message that doesn't leak PB URLs, admin env var names, or
  filter syntax.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 15:07:46 +02:00
SJCoetz
dedd5013b9 refactor(stock-orders): migrate list page to Server Component
Reads searchParams (status, page), validates both, calls
listStockPurchases via the admin PB, and renders the new
StockOrdersTable client component. Drops the legacy direct-PB
read path per AGENTS.md data-access discipline.

The old page.test.tsx is removed; its assertions are mirrored
in the new StockOrdersTable.test.tsx (next task).

Co-authored-by: Orca <help@stably.ai>
2026-08-03 15:02:59 +02:00
SJCoetz
fcb15a20e2 fix(stock-orders): keyboard a11y on pagination + soft row nav
- Disabled pagination links (Previous at page 1, Next at last page)
  now also set tabIndex=-1 and preventDefault on click, so keyboard
  users can't activate them when at the boundary. aria-disabled +
  pointer-events-none blocked only mouse clicks.
- Row click uses useRouter().push() instead of window.location.href
  for a soft navigation that preserves client state (consistent
  with the original /stock-orders page).

Co-authored-by: Orca <help@stably.ai>
2026-08-03 14:51:41 +02:00
SJCoetz
0c556a0513 feat(stock-orders): extract StockOrdersTable client component
Holds the QuickDraftModal open state, renders status filter tabs as <Link>s to ?status=..., and adds two new columns (Ordered R, Received R) showing per-row aggregates from the service layer.\n\nReplaces <Button asChild><Link/></Button> with <Link className={cn(buttonVariants(...))}> because the project's Button (built on @base-ui/react/button) doesn't support asChild. Visually identical; preserves navigation semantics.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 14:46:50 +02:00
SJCoetz
469b2c0460 fix(api): preserve auth status + guard pagination against NaN
- Catch block now preserves error.status so assertAgentAuth's 401
  surfaces as 401, not 500. Matches sibling /api/v1/stock-purchases
  pattern.
- page/perPage parsing falls back to documented defaults on
  non-numeric or zero input rather than passing NaN to the service.
- Add 2 tests covering the auth-status and pagination boundaries.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 14:38:51 +02:00
SJCoetz
8cb4e9a60c feat(api): GET /api/v1/stock-purchases/list with value aggregation
Thin wrapper around listStockPurchases that validates the status
param against STOCK_PURCHASE_STATUS_VALUES (400 on bad input) and
caps perPage at 200. Mirrors the agent-auth pattern used by the
other v1 list routes.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 14:31:22 +02:00
SJCoetz
92febfb51c feat(stock-purchases): aggregate ordered/received value on list rows
Add ordered_value and received_value to StockPurchaseListRow. Each is
Σ line.{quantity|quantity_received} × line.cost_price, rounded to 2dp.
Replaces countItemsForPurchase with summarizeItemsForPurchase so the
per-row N+1 returns all three numbers in one fetch (no extra round
trip).

Co-authored-by: Orca <help@stably.ai>
2026-08-03 14:24:30 +02:00
SJCoetz
94931524e9 docs(plan): stock-orders total value columns
Six-task TDD plan to widen listStockPurchases with ordered_value +
received_value per row, expose it via a new v1 list route, migrate
the /stock-orders page from a client component reading PB directly
to a Server Component + sibling client table component, and move the
existing 7 page tests + add an 8th.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 14:20:48 +02:00
SJCoetz
79b93096ec fix(po-send): stop auto-flipping order_items.item_status to collection_pool
Previously, when recordPurchaseOrderSendOutcome received a "success"
outcome from the n8n workflow, it auto-flipped every linked
order_items.item_status from "po_generated" to "collection_pool".

This dropped items off the buyer-tasks PO Sent tab immediately on
send — before the supplier had even confirmed receipt. The operator
had no chance to mark items ready-to-collect manually.

New flow: send mail → items stay "po_generated" and remain visible
in buyer-tasks PO Sent → operator manually marks items ready →
items drop off buyer-tasks.

The back-link sync (purchase_orders.order_item) still runs and now
reads all items with purchase_order = poId regardless of status, so
the array stays accurate as items progress through their lifecycle.

Adds src/lib/server/purchase-orders-service.test.ts with 4 cases:
- success does NOT auto-flip linked items
- success flips PO to sent + stamps email_message_id
- success keeps the PO's order_item back-link in sync
- failure flips PO to mail_failed + stamps error (also no item writes)

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 14:10:34 +02:00
SJCoetz
9489cf3a44 docs(stock-orders): design total-value columns on list table
Add an Approved design doc for the /stock-orders list table: two new
currency columns (Ordered, Received) showing sum(line.quantity *
line.cost_price) and sum(line.quantity_received * line.cost_price) per
stock purchase. Migrates the page from client-side direct PB read to
the service layer per AGENTS.md.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 14:09:10 +02:00
SJCoetz
aea1ab67f3 fix(po-send): push dashboardBaseUrl + outcomeUrl on n8n webhook body
The n8n Code node that posts the SMTP outcome back to the
/api/v1/purchase-orders/[id]/send-email/outcome route couldn't
read env vars at runtime (N8N_BLOCK_ENV_ACCESS_IN_NODE=true in
docker-compose), so it composed the callback URL from fields on
the webhook body. The route never pushed dashboardBaseUrl or
outcomeUrl, so n8n fell back to a relative URL, the request
died, n8n returned HTTP 500, and the route flipped the PO to
mail_failed with a misleading '[webhook] Webhook returned HTTP
500' message.

DASHBOARD_URL is already defined in .env (labelled 'used by n8n
workflows to call back into this app') — the route just never
read it. Now the route pushes both the dashboard base and a
pre-built absolute outcome URL, trimmed of trailing slashes, and
omitted (not empty) when DASHBOARD_URL is unset so the existing
'missing' branch in n8n fires clearly.

A new test file asserts all three cases: with DASHBOARD_URL,
with a trailing slash, and without the env var.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 11:23:40 +02:00
SJCoetz
faaa47a4fe test(buyer-tasks): lock wire shape of mail_failed group in po_draft
The route is a pass-through, but the smoke test guards against a
future refactor silently dropping the new emailError field — the
operator's retry affordance depends on it.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 10:55:10 +02:00
SJCoetz
010bba475c fix(buyer-tasks): pass status + email_error into SendPoModal stub
The SendPoModal renders its 'Resend' label and inline failure
banner from purchaseOrder.status / email_error, but the buyer-tasks
page stub didn't include either field. As a result, the modal
showed 'Send' and an empty banner even when the underlying PO was
mail_failed. Now the stub includes both so the operator sees the
correct affordance.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 10:53:54 +02:00
SJCoetz
8616d5dbef fix(buyer-tasks): add emailError to route test fixture
The BuyerTaskGroup type gained an emailError field in the previous
commit. This pre-existing fixture constructs a BuyerTaskGroup
literal and now needs the field to compile. No behaviour change
in the test — emailError: null matches a clean draft.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 10:52:58 +02:00
SJCoetz
c481cbeb1c feat(buyer-tasks): render mail-failed banner + Resend button
When a card's poStatus is 'mail_failed', the card now renders a
red warning row with the email_error reason and switches the
action button label to 'Resend' with the destructive colour. A
new data-po-status attribute on the button lets tests target the
state without depending on the human-readable label.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 10:51:38 +02:00
SJCoetz
5959d2a1aa fix(buyer-tasks): keep mail_failed POs visible in the PO Draft tab
The po_draft filter previously required po.status === 'draft', so
a failed send (status: 'mail_failed') caused the group to vanish
from buyer-tasks entirely. The only recovery path was the per-PO
Resend button on the order detail page, which is friction. Now
mail_failed groups stay in place so the operator can re-open the
SendPoModal and retry from the same list. po_sent is unchanged— mail_failed must not appear there.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 10:46:32 +02:00
SJCoetz
964b3d51b2 feat(buyer-tasks): surface email_error on buyer task groups
Adds an emailError field on BuyerTaskGroup so the UI can render
the send-pipeline failure reason. The value is truncated to 200
chars at the service boundary to keep the card layout bounded.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 10:31:33 +02:00
SJCoetz
83bfd294b7 docs(buyer-tasks): implementation plan for mail-failed PO recovery
Six tasks: service field, service filter, card visual, page wiring,
route smoke test, full verification. All TDD with frequent commits.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 10:28:38 +02:00
SJCoetz
fc17b46f9a docs(buyer-tasks): design for mail-failed PO recovery in PO Draft tab
Mail-failed POs currently vanish from every buyer-tasks tab because
the po_draft and po_sent filters both exclude the 'mail_failed' status.
This adds a focused design that surfaces them in place inside PO Draft
with a red warning row + 'Resend' button that re-opens the shared
SendPoModal.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 10:25:51 +02:00
SJCoetz
4444842ef5 feat: collection-map updates, todays-stops, quote/approval rework, buyer-tasks, e2e
- Add todays-stops test and component to collection-map
- Expand collection-map sidebar and pin-model
- Rework orders/[id]/process page
- Add quotes/[id]/page and process pages
- Add quotes/approvals/[id] page
- Add buyer-tasks generate-po route
- Add quotes partial-approve and pdf routes
- Add purchase-orders [id]/pdf route
- Add e2e create-flows spec
- Add superpowers plan doc
- Update pb_schema-2.json
- Update form components (company, customer, order, quote, supplier)
- Update UI components (button, command, dialog)
- Add collection-pool test components

Co-authored-by: Orca <help@stably.ai>
2026-08-03 09:33:45 +02:00
SJCoetz
0064555f22 chore(collection-confirm): cleanup nice-to-haves from final review
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 03:04:23 +02:00
SJCoetz
d6af21193c fix(collection-confirm): scope costOverrides to current PO in dialog
Co-authored-by: Orca <help@stably.ai>
2026-08-03 02:48:17 +02:00
SJCoetz
fea67b15ba fix(collection-confirm): hoist onCostChange out of setEdit updater + preserve null rollback
Co-authored-by: Orca <help@stably.ai>
2026-08-03 02:46:28 +02:00
SJCoetz
e2896264f4 fix(collection-confirm): add route validation + server-side actor derivation
Co-authored-by: Orca <help@stably.ai>
2026-08-03 02:35:58 +02:00
SJCoetz
c14d07ecc0 test(collection-confirm): add confirm-invoice-dialog tests
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 02:32:44 +02:00
SJCoetz
dbf02e2511 fix(collection-confirm): restore Book to stock for stock rows + cost guard
Restores the 'Book to stock' action that the page rewrite in f7d3032 dropped from collection-confirm/page.tsx, and routes the cost pencil away from stock rows so the existing cost-edit endpoint (which queries order_items) can't 404 on a stock_purchase_items id.

- New POST /api/v1/collection-confirm/book-stock endpoint + route tests.
- New bookStockRows(rowIds, { actor }) service that mirrors the pre-rewire bookStockLineForRow flow per-row: ledger → cache increment → line update → auto-derive parent send→received.
- PoCard footer now switches on the row mix: stock rows → 'Book to stock'; customer rows → 'Confirm for invoicing'; mixed → both.
- ItemsTable hides the cost pencil when row.source === 'stock' (cost cell stays readable).
- Page-level handleBookStock wired through to the new endpoint with a reload on success.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 02:29:20 +02:00
SJCoetz
1edaf36023 test(collection-confirm): update page tests for PoCard layout
Co-authored-by: Orca <help@stably.ai>
2026-08-03 01:33:31 +02:00
SJCoetz
f7d3032438 feat(collection-confirm): rewire page to use PoCard + remove Adjust Quantity
- Replace inline row rendering (ConfirmCard, QuantityDialog, NoteDialog,
  StockItemPickerDialog) with <PoCard> composition: PhotoStrip +
  ItemsTable + Confirm button.
- Cost edits dispatch POST /api/v1/order-items/{id}/cost with optimistic
  UI + rollback on failure.
- Confirm click opens ConfirmInvoiceDialog and POSTs to
  /api/v1/order-items/confirm with itemIds, invoice, costOverrides, actor.
- Keep AttachDialog + ReturnToPoolDialog component definitions per spec;
  render with open=false (no per-row buttons in PoCard layout).
- Switch load() to /api/v1/collection-confirm via fetch().
- Stock-source Book-to-stock, surplus chip, Return-to-pool, Adjust
  Quantity per-row actions are removed from the page body; PoCard is
  the primary grouping. Tests for those actions will fail and are
  scheduled for task 14.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 01:25:31 +02:00
SJCoetz
0df6fb1ff2 feat(collection-confirm): add PoCard composing PhotoStrip+ItemsTable
Wraps a single PO group on the collection-confirm review page.
Renders the photo strip + items table + confirm button, with the
PO number and supplier name in the header. The cost edit/read
flow stays inside ItemsTable; the parent owns onCostChange
optimistic updates and the actual confirm mutation.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 01:20:46 +02:00
SJCoetz
4310e4b211 feat(collection-confirm): add ItemsTable with inline cost edit
Co-authored-by: Orca <help@stably.ai>
2026-08-03 01:19:14 +02:00
SJCoetz
afa3876316 feat(collection-confirm): add PhotoStrip component
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 01:16:09 +02:00
SJCoetz
7360e14c58 refactor(collection-confirm): extract ConfirmInvoiceDialog component
Co-authored-by: Orca <help@stably.ai>
2026-08-03 01:09:37 +02:00
SJCoetz
3f228808ea feat(collection-confirm): add POST /api/v1/order-items/confirm route
Co-authored-by: Orca <help@stably.ai>
2026-08-03 01:03:52 +02:00
SJCoetz
fada6f155f feat(collection-confirm): add POST /api/v1/order-items/[id]/cost route
Co-authored-by: Orca <help@stably.ai>
2026-08-03 01:01:30 +02:00
SJCoetz
e860611980 fix(collection-confirm): log server errors and return generic message to client
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 00:59:02 +02:00
SJCoetz
bb10f1350b feat(collection-confirm): add GET /api/v1/collection-confirm route
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 00:51:37 +02:00
SJCoetz
c856db1617 fix(collection-confirm): thread null cost through appendCostNote to preserve audit on null→0
Co-authored-by: Orca <help@stably.ai>
2026-08-03 00:46:50 +02:00
SJCoetz
47eff9a6fe feat(collection-confirm): implement confirmOrderItemsForInvoicing with createBatch
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 00:40:29 +02:00
SJCoetz
77d2c32df4 fix(collection-confirm): add cost_price to ConfirmRow, rename body to args
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 00:29:56 +02:00
SJCoetz
2997b8dcba feat(collection-confirm): implement updateOrderItemCost service
Co-authored-by: Orca <help@stably.ai>
2026-08-03 00:23:37 +02:00
SJCoetz
8b6b83583d fix(collection-confirm): use getOrderItemsCollectionName + populate stock supplier
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 00:17:56 +02:00
SJCoetz
15f6ec638d fix(collection-confirm): make listCollectedItems stock leg fail-soft
Co-authored-by: Orca <help@stably.ai>
2026-08-03 00:12:02 +02:00
SJCoetz
efe10b9474 feat(collection-confirm): add service skeleton with listCollectedItems
Co-authored-by: Orca <help@stably.ai>
2026-08-03 00:08:57 +02:00
SJCoetz
06d8294c77 refactor(collection-confirm): make buildConfirmPatch take options bag with costOverride
Co-authored-by: Orca <help@stably.ai>
2026-08-02 23:59:22 +02:00
SJCoetz
7c807f3c18 fix(collection-confirm): render cost category label correctly
Co-authored-by: Orca <help@stably.ai>
2026-08-02 23:57:06 +02:00
SJCoetz
529e08eb2a fix(collection-confirm): widen NoteEntry.body and dedup attachments within row
Co-authored-by: Orca <help@stably.ai>
2026-08-02 23:52:21 +02:00
SJCoetz
7e1f22f363 feat(collection-confirm): add appendCostNote and dedupeAttachments helpers
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-02 23:48:51 +02:00