Skip to content

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.

LayerFileChange
Shared util (new)src/lib/shared/scanning/barcode.tsPure, generic GTIN/barcode primitives
BFF identifysrc/server/lib/amazon/identifier-mode.tsUse primitives to classify; GTIN-normalize the response filter
BFF dispatchsrc/server/routes/amazon/search.tsIdentifier→keyword fallback (reuse-skip-filter)
BFF requestsrc/server/lib/amazon/search-request-builder.tsBuild identifier query from the enumerated set
Tests*.test.ts + src/mocks/handlers/amazon.tsUnit + 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 trailing X handling stays in asin.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; null if 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) — both
    • 11 → 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.
  • allIdentifiers stays “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 enumerateBarcodes candidate (handles "0 41333 27035 7"). Checksum guards against false concatenation of unrelated tokens.
  • identifiers output = 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).

  • 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 shared enumerateBarcodes (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 in shared.)
  • 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).

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:

  1. UPC → results list — enter a valid UPC in the Amazon search bar; assert the results list renders the identifier-matched product.
  2. Select → form prefill — pick a result; assert the item form prefills (name, sku = ASIN, supplier = Amazon), matching the existing select-from-search assertions.
  3. Formatted UPC (separators) — enter a spaced/hyphenated UPC; assert the same product resolves (exercises separator normalization end-to-end).
  4. 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 whose SearchItems response 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.

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:

CommitContentsGreen gate before committing
1src/lib/shared/scanning/barcode.ts + barcode.test.tsjest (new file), tsc, lint
2identifier-mode.ts (classification + GTIN filter) + its testsjest, tsc, lint
3search-request-builder.ts + search.ts fallback + their testsjest, tsc, lint
4MSW handler reuse + fixtures (+ any unit-level handler tests)jest, tsc, lint
5E2E specs + page-object additionsmake 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:

  1. | = true OR in Amazon Keywords. If not a true union, switch the ambiguous-8-digit path to one call per interpretation (≤ 2), merged/deduped.
  2. 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).
  3. Throttle (HTTP 429) path → Sentry/error mapping behaves as the existing code expects.
  4. 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.
  5. 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