Skip to content

lumexia: lot capture, validation and lot lookup - #168

Merged
isaacvarg merged 15 commits into
mainfrom
lumexia/lot-capture
Nov 10, 2026
Merged

isaacvarg merged 15 commits into
mainfrom
lumexia/lot-capture

Conversation

@isaacvarg

Copy link
Copy Markdown
Owner

Implements the lumexia module: SPEC-lumexia, against Lumexia's API contract in docs/lumexia-api.md. It also carries the approved spec commits from lumexia/spec.

Closes #30

Tickets

#167 was built in parallel with #164–#166 and merged in fb63fb6. The order detail page uses #167's LotList, and there is now a single LotStatus component.

Review fixes

  • Lot lookup matched with ILIKE, so % returned every lot. It now matches the upper-cased number exactly, which also uses the index.
  • A void that shrinks a box line left its lots unchanged. Now the lots are reduced too.
  • A lot scanned once by number and once by QR code (while Lumexia was down) stayed as two half-filled rows that lookup never joined. Validation now merges them.
  • Save count stayed disabled until the number field committed on blur. The scan banner named the latest lot instead of the one scanned.
  • A Lumexia response body that stalls past the timeout is now reported as Lumexia being down.
  • The lot sweep:
    • queues nothing while Lumexia isn't healthy, so a refused key doesn't create failed jobs every 10 minutes;
    • skips lots already waiting in a job;
    • queues at most 2,000 lots per run.
  • /api/health runs the Lumexia ping alongside the other checks.
  • QA shows Unverified lots as a warning.

Rollout

  • Lumexia is inactive until its URL and key are set in Settings. Lot rules start empty, so no product needs a lot until an admin switches a type on. Switching the rules off again is the kill switch.
  • One migration (20261109231847_lumexia_lots) adds the lot tables and columns, plus a quantity > 0 CHECK. The init migration's hand-written constraints are untouched.

Testing

  • biome passes. Typecheck passes; the web app is checked with plain tsc --noEmit, because next typegen hits a local SWC error.
  • Unit and integration tests pass: core 3120, contracts 126, logger 57, worker 7, i18n 3, plus the web health route test.
  • Each Required review fix has a test that fails on the code before the fix.
  • Not run: Playwright (settings.spec.ts, lumexia.spec.ts, lumexia-lots.spec.ts, lot-lookup.spec.ts) and pnpm build. Both fail in agent shells on this machine with ERR_SWC_NATIVE_CACHE, so they need a local run before merge.
  • pnpm audit reports 2 high advisories, in deepmerge-ts and mysql2, both pulled in through prisma. They exist on main too; this PR changes no dependencies.

Add SPEC-lumexia: product links to Lumexia items, lot rules by item,
procurement and inventory type, lot capture at Packing with the
single-lot split rule, background validation, QA block on wrong-item
lots, and Lot lookup. Record Lumexia's as-built REST API v1 in
docs/lumexia-api.md. Mark lumexia as spec approved in SPEC.md.

Refs #30
… migration

- LumexiaClient adapter in core/adapters/lumexia: HTTP client (Zod schemas,
  5 s timeout, cursor paging, findItems in batches of 200, 404 → null) and an
  in-memory fake, both run through one contract suite; fixtures written from
  docs/lumexia-api.md, plus an opt-in LUMEXIA_LIVE test.
- lumexiaClientFor(db), lumexiaHealth (cached 30 s) and testLumexiaConnection;
  /api/health reports lumexia ok/unconfigured/down without failing or
  degrading; Settings → Lumexia gets Test connection (en, es).
- New lumexia.manage permission (Admin).
- Migration: Product link fields and lotOverride, the three Settings lot-rule
  lists, LotStatus, PackageItemLot with its quantity > 0 CHECK; drops
  PackageItem.lot_code / lumexia_lot_id and their index.

Refs #160
- core/lumexia/rules.ts: pure needsLot(product, settings) (physical + linked,
  then override, else any of the three type ids switched on); getLotRules
  joins Lumexia's three type tables with the saved switches, reports
  switched-on ids Lumexia no longer returns, and falls back to the saved ids
  read-only (with the reason) when Lumexia is unconfigured or unreachable;
  setLotRules replaces the three lists offline, audited Settings /
  lumexiaLotRules with before/after. Both need lumexia.manage.
- Contracts lumexia.lotRules / setLotRules (GET/PUT /lumexia/lot-rules).
- Settings → Lumexia → Lot rules: a switch per type, "Tracked in Lumexia" tag,
  "Removed in Lumexia" with Switch off, read-only state; en and es.
- Playwright: saving a rule, the tag and removed ids in en/es, read-only.

Refs #161
- core/lumexia/items.ts: lumexiaProducts (every product with its link, item
  type name and needsLot), searchLumexiaItems, suggestLinks (SKU ↔ reference
  code or alias, case-insensitive, labels matchedOn, skips ambiguous SKUs,
  writes nothing), linkProduct / linkProducts (≤200, one findItems, all or
  nothing), unlinkProduct (clears link + override, keeps lots), setLotOverride
  (errors.lumexia.notLinked). All lumexia.manage; Lumexia errors map to their
  messageKey (errors.lumexia.unavailable etc.). Link changes audited on Product.
- Daily lumexia.refreshItems job (04:00 Chicago) updates reference codes and
  type ids; a missing item keeps its values and shows "Not found in Lumexia"
  (lumexiaSyncedAt null). New lumexiaActor system actor audits refreshes.
- Contracts + RPC under /lumexia/products, /lumexia/items, /lumexia/suggest-links.
- /lumexia page (Admin): TanStack table, filters, Match by SKU review dialog,
  item search combobox, Unlink, three-way Lot select; unconfigured message
  with a link to Settings; nav entry. en and es.
- Store sync test now pins every lumexia* field and lotOverride.
- Playwright: Match by SKU → linked with Needs lot: Yes; unconfigured; Admin-only.

Refs #162
At Packing a lot-needing box line shows "Lot needed"; scanning a printed lot
number with the package selected applies the split rule (first lot covers
every unit, later scans move one unit at a time). Pack and unpack keep lots
covering the box line. A lot for another item is refused with one internal
note on the package, written outside the refused scan's transaction. The
packed gate gains lotMissing.

- core/lumexia/codes.ts: parseLotCode (also @woolrus/core/lot-codes)
- core/lumexia/split.ts: the split rule, pure, with a property test
- core/lumexia/lots.ts: scanLot; lot-store.ts: lot reads/writes
- core/lumexia/needs-lot.ts: needsLot moved here (re-exported from rules)
  so the gates can use it without an import cycle
- packing reads: package items gain needsLot and lots

Refs #163
scanLot resolves a QR code (the lot's UUID) through Lumexia with a 2 s
budget: a found lot picks its item's line, a lot on another item is
refused with the wrong-item note, and when Lumexia is down, slow,
unconfigured or doesn't know the lot it is saved UNVERIFIED with only
lumexiaLotId. When several lot-needing lines fit, scanLot refuses with
errors.lumexia.chooseLine and the candidates; the client's Which line?
dialog retries with packageItemId. setLotQuantity and removeLot edit a
box line's lots from the chip dialog (the default lot gives or takes
the difference).

Refs #164
Lot lookup (/lots, order.read): one field for a lot number (any case) or a
lot QR's UUID. lookupLot matches lotNumber, or lumexiaLotId for a UUID, and
returns every package item holding the lot (repack-discarded packages left
out, unshipped orders kept with no date). Download CSV streams the same rows
and columns through core/csv. The order detail page and the pack screen's
order drawer show lots per package item with their status; orders.get's
package items gain needsLot and lots.

Refs #167
Every new lot enqueues lumexia.validateLots on its write's transaction.
The job resolves each lot (getLot by id, else findLotsByNumber) and sets
VALID (filling a missing number or id), WRONG_ITEM (plus one internal
note) or NOT_FOUND, audited as the lumexia actor. Lumexia down retries
5x from 30 s; a refused key ends the retries. A 10-minute sweep re-queues
unverified lots and re-checks NOT_FOUND ones daily for 7 days. Pack
chips and the order detail show each lot's status as icon + word.

Refs #165
The verified gate gains lotWrongItem for every WRONG_ITEM lot, so Complete
QA and the label purchase refuse it (REST too); NOT_FOUND and UNVERIFIED
only warn. QA rows show their lots with status, and a wrong-item lot has
Replace lot (qaReplaceLot: same item check and rejection note as Packing)
and Remove (qaRemoveLot), both needing order.qa, QA and the claim.

Refs #166
Merge claude/lot-lookup-167-a194eb into the module branch.

- Order packages: keep #167's `needsLot` + `lots` on package items and
  show them with `LotList`, dropping #165's inline lot list on the
  order detail page (LotList covers it, plus "Lot needed").
- One lot status component: `LotStatus` / `useLotText` in
  `components/lots/lot-status.tsx`. #167's `LotStatusBadge`, its
  `lotText` and the `lotLookup.status.*` strings are removed;
  `LotList` and the lot lookup page use `LotStatus` and
  `lots.status.*`.
…he lot dialog

- Lot lookup matches the upper-cased number exactly instead of ILIKE, so
  `%` and `_` aren't wildcards (`%` returned every lot) and the
  lot_number index is used.
- A void that shrinks a box line takes the units out of its lots too, so
  the lots keep adding up to the box line.
- Validation merges a lot scanned once by number and once by QR code
  (while Lumexia was down) into one row with both identifiers, so lookup
  finds every unit by either one. It used to leave both half-filled.
- Lumexia's HTTP client reports a body that stalls past the timeout as
  LumexiaUnavailable, not as a bad response.
- Pack: "Save count" is no longer disabled until the NumberField commits
  (it only commits on blur or Enter), and the scan banner names the lot
  that was scanned rather than the latest one.
…verified

- The 10-minute lot sweep queues nothing while Lumexia isn't healthy
  (unconfigured, down or refusing the key), so a bad key no longer adds
  failed jobs every 10 minutes. It skips lots a validation job still
  holds, so a slow Lumexia doesn't check lots twice, and queues at most
  2,000 lots per run, oldest first.
- /api/health starts the Lumexia ping right after the database check and
  awaits it last, so its up-to-3 s no longer adds to the serial checks.
- QA shows Unverified lots in the warning tone, like Not found
  (SPEC-lumexia "Reads and UI"); elsewhere Unverified stays neutral.
These specs had never run: Next can't load SWC in the agent sandbox.
CI showed four kinds of failure, each the same on desktop and tablet:

- settings: the Lumexia card holds the Lot rules section, so the card
  has two status lines (the URL form's "Saved." and Lot rules' "isn't
  set up yet"). The URL tests now read the URL form's own status, and
  Test connection's result has a `lumexia-test-result` test id.
- settings: the lot-rule switch was clicked as a normal element, but
  React Aria's switch input is visually hidden, so the click waited
  until the test timed out. It now uses `click({ force: true })`, as
  the carriers specs do.
- lumexia: in Spanish, "Sin vincular" is both a filter option and the
  table cell. The check now looks in the table.
- lumexia-lots: "Put everything left here" moved into the package's
  ⋯ menu in the pack card redesign (#159). The spec now opens the menu.
@isaacvarg
isaacvarg merged commit 25904b1 into main Nov 10, 2026
1 check passed
@isaacvarg
isaacvarg deleted the lumexia/lot-capture branch November 10, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[spec] lumexia: lot capture and SKU↔item link

1 participant