Implementation Changes — UPC Fallback for Amazon Search
Concrete, per-file changes to deliver the scope locked in the
goal and scope definition. Frontend-only,
/search path only, single PR. No GetItems, /import, backend, or SPA-UI
changes.
Overview
Section titled “Overview”| Layer | File | Change |
|---|---|---|
| Shared util (new) | src/lib/shared/scanning/barcode.ts | Pure, generic GTIN/barcode primitives |
| BFF identify | src/server/lib/amazon/identifier-mode.ts | Use primitives to classify; GTIN-normalize the response filter |
| BFF dispatch | src/server/routes/amazon/search.ts | Identifier→keyword fallback (reuse-skip-filter) |
| BFF request | src/server/lib/amazon/search-request-builder.ts | Build identifier query from the enumerated set |
| Tests | *.test.ts + src/mocks/handlers/amazon.ts | Unit + MSW fixtures |
1. New: src/lib/shared/scanning/barcode.ts
Section titled “1. New: src/lib/shared/scanning/barcode.ts”Pure, dependency-free, generic (not Amazon-specific — reusable by future non-Amazon / scan-based searches). Exports:
-
stripSeparators(raw: string): string— remove spaces, hyphens, dots; keep digits. (ISBN trailingXhandling stays inasin.ts/ISBN path.) -
isValidGtin(digits: string): boolean— GS1 mod-10 over 8/12/13/14-digit strings. -
isValidIsbn10(token: string): boolean— mod-11 (kept for completeness; see reachability note below). -
expandUpcE(d8: string): string | null— UPC-E(8) → UPC-A(12) via the standard last-data-digit rule;nullif the expansion’s check digit fails. -
toGtin14(digits: string): string— left-pad to 14 (canonical match form). -
enumerateBarcodes(raw: string): string[]— the core. Strips separators, then by length yields every checksum-valid interpretation in its natural search form (deduped):8→ EAN-8 (if valid) and UPC-E→UPC-A(12) (if valid) — both11→ UPC-A after padding a leading zero (if valid)12→ UPC-A (if valid)13→ EAN-13 (if valid)14→ GTIN-14 (if valid)- else →
[]
Returns natural forms (12/13/8-digit) — not GTIN-14 — because Amazon’s keyword index matches the human barcode, not a zero-padded GTIN-14. GTIN-14 is used only for the response filter (below).
Unit tests: src/lib/shared/scanning/barcode.test.ts — valid/invalid checksums
per length, UPC-E expansion (incl. invalid), separator stripping, the 8-digit
dual interpretation, 11-digit padding, and non-barcode rejection.
2. src/server/lib/amazon/identifier-mode.ts
Section titled “2. src/server/lib/amazon/identifier-mode.ts”classifyIdentifierTokens — replace the raw length regexes with
enumerateBarcodes (from @/lib/shared/scanning/barcode):
- A token is an identifier iff
enumerateBarcodes(token)is non-empty. allIdentifiersstays “every token is an identifier” (preserves the bare-vs-mixed split — mixed still falls to keyword mode, per scope).- Add the spaced-barcode case: if per-token classification fails, test the
whole cleaned query with separators removed as a single
enumerateBarcodescandidate (handles"0 41333 27035 7"). Checksum guards against false concatenation of unrelated tokens. identifiersoutput = the union of natural-form interpretations (for the OR query).- ISBN-10 note: bare/pure ISBN-10 is claimed upstream as an ASIN (precedence), so the ISBN-10 path here is reachable only in a mixed-identifier query — keep it minimal.
filterByExternalIds — normalize both sides to GTIN-14 before comparing
upcs/eans (via toGtin14); keep isbns exact. This closes the
UPC-stored-as-EAN cross-bucket gap (Improvement 3). Preserve dedupe-by-ASIN and
order.
3. src/server/lib/amazon/search-request-builder.ts
Section titled “3. src/server/lib/amazon/search-request-builder.ts”buildIdentifierRequest(identifiers) — keywords = the enumerated natural-form
set joined with | (unchanged mechanism; the set may now hold >1 interpretation
for an ambiguous 8-digit code). SearchIndex='All' unchanged.
4. src/server/routes/amazon/search.ts — keyword fallback
Section titled “4. src/server/routes/amazon/search.ts — keyword fallback”At Step 5 (identifier mode), after filterByExternalIds:
- If the filtered set is non-empty → use it (exact matches).
- If the filtered set is empty but the raw response had items → use the raw items (reuse-skip-filter = the keyword result; no extra API call).
- If the raw response was empty → empty result (terminal).
No relaxation change needed (runWithRelaxation already returns the identifier
primary directly). Follow the existing addBreadcrumb pattern for the new
fallback branch (observability follows the code).
5. Unit tests
Section titled “5. Unit tests”barcode.test.ts(new) — see §1.identifier-mode.test.ts— checksum accept/reject, spaced barcode, 8-digit dual interpretation, GTIN-normalized cross-bucket filter (UPC query ↔ EAN-stored product).search.test.ts— identifier→keyword fallback (raw reused when filter empties), terminal empty, and no-regression on existing ASIN/keyword paths.
6. Mock handler (MSW) — reuse the shared util
Section titled “6. Mock handler (MSW) — reuse the shared util”src/mocks/handlers/amazon.ts today carries its own duplicate identifier
regex (IDENTIFIER_RE / isIdentifier) and a MOCK_SEARCH_DTO_IDENTIFIER
fixture (upc: '044600012049'). Two changes:
- Replace
isIdentifier’s regex with the sharedenumerateBarcodes(import from@/lib/shared/scanning/barcode— pure, browser-safe). This keeps mock classification in lock-step with production, so E2E exercises the real checksum/UPC-E/separator behavior rather than a drifting copy. (This is the concrete payoff of placing the util inshared.) - Add fixtures for the paths E2E asserts: a UPC/EAN identifier match, and a keyword-fallback case (an identifier-shaped-but-checksum-invalid or no-exact-match query returning keyword results).
7. E2E tests (Playwright, mock mode)
Section titled “7. E2E tests (Playwright, mock mode)”The SPA UI is unchanged, so E2E is regression + happy-path coverage of the
user-facing flow through the updated mock. Extend the existing
e2e/specs/items/amazon-import.spec.ts (and its page object), not a new
harness:
- UPC → results list — enter a valid UPC in the Amazon search bar; assert the results list renders the identifier-matched product.
- Select → form prefill — pick a result; assert the item form prefills
(name,
sku= ASIN, supplier = Amazon), matching the existing select-from-search assertions. - Formatted UPC (separators) — enter a spaced/hyphenated UPC; assert the same product resolves (exercises separator normalization end-to-end).
- Identifier bypass → keyword — enter a checksum-invalid barcode-shaped
number; assert the panel shows keyword results (not an empty/error state).
Note this exercises the bypass path only: a checksum-invalid token fails
enumerateBarcodes, so it never enters identifier mode and runs ordinary keyword search. The distinct reuse-skip-filter identifier→keyword rung (a valid barcode whoseSearchItemsresponse has items but no external-ID match) is covered by unit tests, where the mocked Amazon response can be shaped precisely.
Keep E2E lean (edge cases live in unit tests); these four cover the new
user-visible behavior and its fallback. Run under NEXT_PUBLIC_MOCK_MODE=true
via the existing E2E infra; full parity through make ci-replicate pre-push.
8. Method, sequencing & commits
Section titled “8. Method, sequencing & commits”Test-first (TDD). For each change set: write the tests first (they will not compile — the target symbols don’t exist yet), then implement until the code compiles and the tests pass. No production code is written ahead of a failing test that motivates it.
One commit per change set (production code and its unit tests together, green), plus one final commit for the E2E specs. Commit sequence:
| Commit | Contents | Green gate before committing |
|---|---|---|
| 1 | src/lib/shared/scanning/barcode.ts + barcode.test.ts | jest (new file), tsc, lint |
| 2 | identifier-mode.ts (classification + GTIN filter) + its tests | jest, tsc, lint |
| 3 | search-request-builder.ts + search.ts fallback + their tests | jest, tsc, lint |
| 4 | MSW handler reuse + fixtures (+ any unit-level handler tests) | jest, tsc, lint |
| 5 | E2E specs + page-object additions | make ci-replicate (E2E, mock mode) |
Each commit leaves the tree green (compiles + all tests pass). Pre-push, run the
full gate once more: npm run lint, npx tsc --noEmit, npx jest, and
make ci-replicate. PR-body CHANGELOG under Added/Fixed. Branch already
exists: jmpicnic/amazon-search-by-upc-pdev1237.
9. Post-PR verification checklist (run while CI is green-lighting)
Section titled “9. Post-PR verification checklist (run while CI is green-lighting)”The implementation is optimistic on these; correct if verification disproves them:
|= true OR in AmazonKeywords. If not a true union, switch the ambiguous-8-digit path to one call per interpretation (≤ 2), merged/deduped.- Keyword-on-barcode reliability across a sample of products (the fallback rung and the mixed-input decision rest on it — verified on one product so far).
- Throttle (HTTP 429) path → Sentry/error mapping behaves as the existing code expects.
- UPC-E in practice — sample what real scanners/cameras emit (raw 8-digit vs pre-expanded 12) to confirm the UPC-E branch is exercised.
- EAN-only product — confirm GTIN normalization changes a real outcome (the probe blocked earlier on 1Password biometric).
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved