Skip to content

Amazon Import — Task Plan

Author: Miguel Pinilla Date: 2026-05-20 Status: Planning

Implement the POST /api/amazon/search BFF route in arda-frontend-app per the design in spec-analysis.md, plus the shared URL/ASIN input normalisation layer that broadens the set of accepted URL/ASIN shapes for both /api/amazon/import and /api/amazon/search. The work covers: schemeless / path-only / mixed-case URL handling; extended US sub-hosts (m., smile., read.); old-form product URL paths; plain-text ASIN extraction (lenient on /import only); defensive input filter for /search; ASIN extract-and-dispatch (including multi-ASIN paste); smart UPC/EAN/ISBN identifier mode; silent Amazon-side query relaxation; and observability parity with the workspace’s recent Sentry conventions (including retrofitting /api/amazon/import if it falls short). All work lands in a single PR (#836) on the existing jmpicnic/amazon-import-search branch.

Tasks are grouped by phase; phases reflect the dependency graph. All tasks use persona front-end-engineer.

#TaskDepends OnStatusNotes
1Verification passCompleteRead existing /api/amazon/import route, creators-client, MSW handlers, env.ts, observability. Output: short audit note appended to this file (see Audit Outputs). Resolves I1, I2, I3 before implementation.
#TaskDepends OnStatusNotes
2Extend extractAsin (strict) in src/lib/shared/amazon/asin.ts1CompleteCase-fold bare ASIN; add m./smile./read.amazon.com to US allow-list; add /exec/obidos/ASIN/ and /o/ASIN/ to path regex; scheme-prepend retry; path-only matching with or without leading /. Existing tests pass except flipped m./smile./read. expectations; ~22 new unit-test cases.
3Add extractAsinLenient (plain-text fallback)2CompleteNew sibling export. Runs strict extractAsin first; only falls through to ASIN-in-text extraction when no authoritative Amazon URL was parsed (i.e. the input is plain text). A US Amazon URL that parsed but pointed at a non-product path is preserved as a recognised rejection and is not scanned for bare ASINs. Plain-text scan uses \b(B[A-Za-z0-9]{9}|\d{9}[\dXx])\b with digit-presence guard for the B-branch; requires exactly one distinct match. Unit tests cover the 8 plain-text cases.
4Switch /api/amazon/import route to extractAsinLenient3CompleteUpdate src/server/routes/amazon/import.ts to call extractAsinLenient instead of extractAsin. Rewrite the UNRECOGNIZED_AMAZON_URL user-facing message to “We could not identify an Amazon Reference in your input.” Existing route tests + new tests for the broader acceptance set. Response contract preserved verbatim.
#TaskDepends OnStatusNotes
5Server-side constants module1CompleteDeclare all MAX_* / RELAXATION_* / BATCH_ASIN_MAX constants in one place with the values from spec.
6Shared resource selector + resourcesForMode1CompleteFactor BASE_ITEM_RESOURCES out of existing getItems path; add resourcesForMode("default" | "identifier").
#TaskDepends OnStatusNotes
7Defensive input filter1, 5CompletePure function per spec; unit tests for every filter step + the “at-least-one-search-term” rule.
8Category resolver1, 5CompleteresolveCategory(label) + curated synonym table (n:1) + unit tests; covers Q1 option (a) for multi-category.
9ASIN multi-token extraction (strict)1, 2, 5CompleteTokeniser + per-token strict extractAsin (Phase 2 Task 2 output); batch-cap enforcement; unit tests including mixed-token fall-through; verifies plain-text-ASIN extraction is NOT used here.
10Identifier classifier + filter + dedup1, 5, 6CompleteUPC/EAN/ISBN patterns, all-tokens-identifier check, filterByExternalIds, ASIN dedup; unit tests.
#TaskDepends OnStatusNotes
11BFF→SDK request builder6, 8, 10CompletebuildSearchRequest(filtered) in search-request-builder.ts; pure; 24 unit tests covering default mode (query, keywords, categories, primeOnly, sortBy), identifier mode (pipe-join, searchIndex=All, deliveryFlags/categories ignored), and edge cases.
#TaskDepends OnStatusNotes
12creatorsClient.searchItems wrapper6, 11CompleteAsync wrapper mirroring getItems shape; error→AMAZON_API_ERROR; Layer-2 tests using the existing SDK-mock pattern.
#TaskDepends OnStatusNotes
13Silent Amazon-side relaxation orchestrator12CompleteBounded retry ladder (drop deliveryFlags → drop category); RELAXATION_MAX_RETRIES + RELAXATION_TIME_BUDGET_MS; unit tests.
#TaskDepends OnStatusNotes
14Route handler src/server/routes/amazon/search.ts7, 8, 9, 11, 13CompleteValidates input; dispatches ASIN shortcut / identifier mode / default; returns response envelope. Shared item-mapper.ts extracted. 30 unit tests.
15Next.js POST handler src/app/api/amazon/search/route.ts14CompleteThin POST handler; auth wired identically to /api/amazon/import. 12 unit tests.
#TaskDepends OnStatusNotes
16MSW Layer-1 handlers + fixtures14, 15Completesrc/mocks/handlers/amazon.ts + src/mocks/data/: success multi-item, zero results, INVALID_SEARCH_INPUT, AMAZON_API_ERROR, identifier mode.
#TaskDepends OnStatusNotes
17Layer-1 consumer tests16Complete1–2 jest tests that fetch('/api/amazon/search', body) through MSW; double as PDEV-477 reference.
18Route-level tests (full matrix)14, 15CompletePer-branch coverage of the test matrix (see Acceptance Criteria).
#TaskDepends OnStatusNotes
19Observability audit + retrofit14, 15CompleteOption (a) chosen — spans + breadcrumbs + captureException + relaxation metric landed in commit 2ec2f9f3. 7 files modified (creators-client.ts, search.ts, import.ts, their 3 test files, __mocks__/@sentry/nextjs.ts). 32 new test assertions. No follow-up ticket required. See decision I6.
#TaskDepends OnStatusNotes
20Final integration + local gates + CHANGELOG roll-up1–19CompleteLocal gates: eslint clean, tsc clean, jest 5300/310 suites passing, npm run build succeeds. PR #836 title + body rewritten to consolidate the project-setup drive-bys, PDEV-318 AGENTS.md shim, URL/ASIN normalisation, new /api/amazon/search route, and Sentry observability into a single ### Added block (plus the existing ### Fixed for the Playwright tsc fix). The 27 pre-existing tsc errors and the npm run build failure observed earlier were resolved by aligning node_modules to the lockfile via npm install (the lockfile already pinned design-system 5.3.0; node_modules had been left at the older 5.2.1).

Single directory — no new worktrees needed. All work lands on the existing jmpicnic/amazon-import-search branch in the /Users/jmp/code/arda/projects/amazon-import-search-worktrees/arda-frontend-app worktree, extending PR #836. Documentation updates land in the sibling documentation worktree on the same branch name, extending PR #83. No parallel agents.

Base branch: main (in Arda-cards/arda-frontend-app and Arda-cards/documentation).

Single agent — tasks execute strictly in dependency order. No concurrent work. The phases shown in the task list above reflect the dependency graph; within a phase, tasks are still serialised because there is only one agent. If the project is later multi-agent, Phase 4’s four pure helpers (Tasks 7–10) and Phase 8’s two route artifacts (Tasks 14–15) are the natural parallelisation seams.

Accept idle time — single-agent plan, no idleness to manage.

PersonaTasks AssignedWorktreeSpawn Order
front-end-engineer1–20/Users/jmp/code/arda/projects/amazon-import-search-worktrees/arda-frontend-app (+ documentation)First

All scope-level questions resolved during exploration (see spec-analysis.md — Open questions). Implementation-detail questions deferred to Task 1’s verification output:

#QuestionOptionsRecommendationDecision
I1Existing error envelope shape on /api/amazon/importRead from codeMatch exactlyResolved: success wire shape is { ok: true, data: <body> }; error wire shape is { ok: false, code, message }. Next.js handler renames route module’s internal field to data. Status codes per statusMap in src/app/api/amazon/import/route.ts:77-86. Success status is 200 when name+image+price+productUrl all non-null, 206 otherwise. For /search, items are list-shaped so partial-completeness 206 does not apply — always 200 on success.
I2Existing Sentry / observability primitives used in this repoRead from codeMatch exactly, retrofit /import if behindResolved: repo uses addBreadcrumb + metrics.count + captureException directly at call sites. Sentry.startSpan is NOT used in production code today (only a demo page). /api/amazon/import is NOT wrapped in any span; creators-client.ts does NOT wrap Amazon SDK calls. The “retrofit” decision in Phase 11 becomes: introduce spans (new pattern) or match existing breadcrumbs+metrics. Surface to user.
I3Existing Amazon-API mocking pattern in creators-client.test.tsSDK-method mock vs. msw/node vs. transport-levelMatch the existing pattern verbatimResolved: jest.mock('amazon-creators-api', () => { class MockApiClient ... }, { virtual: true }). Class mocks for ApiClient, TypedDefaultApi, GetItemsRequestContent with per-method jest.fn(). virtual: true because the package is not installed locally. __resetApiClientCacheForTests() in beforeEach. Match verbatim for searchItems tests.
I4Coverage thresholds actually enforcedRead from jest.config.jsConfirmed: jest.config.js coverageThreshold.global is lines: 84, statements: 84, functions: 81, branches: 72 (global, not per-file). README’s 11–18% numbers refer to merged Jest+E2E thresholds (a different gate) and are stale. The new code targets are higher; see Acceptance Criteria.Resolved
I5API reference page in current-system/Skip / inline / follow-upPostpone until after merge; capture as Linear commentResolved by user
I6/api/amazon/import observability retrofit scope(a) retrofit in this PR / (b) follow-on PR / (c) no retrofitOption (a) for Medium scopeResolved (user-authorized): Option (a) chosen. Retrofit landed in commit 2ec2f9f3 alongside the new /search observability. Both routes now ship at parity: outer Sentry.startSpan (op function.bff), dispatch + validation breadcrumbs, captureException on Amazon API errors, and amazon.search.relaxation metric when relaxation fires. creators-client.ts wraps both SDK calls (getItems, searchItems) in http.client spans. 7 files modified, 32 new test assertions, no follow-up ticket required.

The Team Lead (or single agent) knows the work is complete when every item below is checked.

  • extractAsin (strict) accepts schemeless URLs (www.amazon.com/dp/<ASIN>, amazon.com/dp/<ASIN>).
  • extractAsin accepts path-only inputs both with and without leading / (e.g., /dp/<ASIN>, dp/<ASIN>, Some-Product/dp/<ASIN>, gp/product/<ASIN>, gp/aw/d/<ASIN>).
  • extractAsin accepts lowercase / mixed-case bare ASINs (case-folded before pattern test).
  • extractAsin accepts m.amazon.com, smile.amazon.com, read.amazon.com (added to US allow-list).
  • extractAsin accepts old-form paths /exec/obidos/ASIN/<ASIN> and /o/ASIN/<ASIN> (with or without leading /).
  • extractAsin returns UNRECOGNIZED_AMAZON_URL (not falling through) when a URL parses with a US Amazon host but the path is non-product (search/cart/homepage).
  • extractAsinLenient calls extractAsin first and only falls back to plain-text ASIN extraction when no authoritative Amazon URL was parsed (i.e. the input is plain text). A URL that parsed but pointed at a non-product US Amazon path returns UNRECOGNIZED_AMAZON_URL without scanning for bare ASINs. UNSUPPORTED_SHORT_LINK and UNSUPPORTED_AMAZON_LOCALE short-circuit.
  • Plain-text ASIN extraction uses \b(B[A-Za-z0-9]{9}\|\d{9}[\dXx])\b with a digit-presence guard for the B-branch; rejects 10-letter English words.
  • Plain-text ASIN extraction requires exactly one distinct match; zero or ≥2 matches → UNRECOGNIZED_AMAZON_URL.
  • /api/amazon/import route handler uses extractAsinLenient.
  • /api/amazon/search ASIN-shortcut dispatcher (multi-token tokeniser) uses strict extractAsin — verified by a test that a search query like B08N5WRWNW alternatives does NOT trigger the ASIN shortcut.
  • POST /api/amazon/import preserves its response contract verbatim (DTO shape, error codes, HTTP statuses). Previously accepted inputs still succeed; the broadened acceptance set (per the criteria above) succeeds on inputs that previously returned UNRECOGNIZED_AMAZON_URL.
  • POST /api/amazon/search accepts { query?, keywords?, categories?, primeOnly?, sortBy? } and returns { items: AmazonImportDto[], totalResultsHint? } on success (200).
  • Server-side constants module exports MAX_QUERY_LENGTH, MAX_KEYWORDS, MAX_KEYWORD_LENGTH, MAX_CATEGORIES, MAX_CATEGORY_LENGTH, MAX_RESULTS, RELAXATION_MAX_RETRIES, RELAXATION_TIME_BUDGET_MS, BATCH_ASIN_MAX with the values from spec-analysis.md — Constants.
  • BASE_ITEM_RESOURCES is shared between getItems and searchItems call sites (no duplicate resource list).
  • Over-length queryINVALID_SEARCH_INPUT (400).
  • Over-cardinality keywords[] or categories[]INVALID_SEARCH_INPUT.
  • No search terms (query empty/absent and keywords[] empty/absent post-filter) → INVALID_SEARCH_INPUT.
  • Unknown sortBy value → INVALID_SEARCH_INPUT.
  • Multi-ASIN paste exceeds BATCH_ASIN_MAXINVALID_SEARCH_INPUT.
  • Empty keywords: [] and categories: [] arrays treated as absent (not malformed).
  • Single bare ASIN → getItems shortcut, returns one-item list.
  • Single Amazon product URL on amazon.comgetItems shortcut.
  • Multiple ASINs/URLs (≤10) separated by whitespace, commas, semicolons, or newlines → single batched getItems call.
  • ASIN embedded in surrounding free text → no shortcut; passes through to SearchItems as keywords.
  • Short-link URL (a.co/, amzn.to/) → UNSUPPORTED_SHORT_LINK.
  • Non-US-locale Amazon URL → UNSUPPORTED_AMAZON_LOCALE.
  • Pure-identifier list (all tokens UPC/EAN/ISBN-10/ISBN-13) → identifier mode (SearchIndex=All, pipe-joined keywords, ItemInfo.ExternalIds in resources, response filtered + deduped).
  • Mixed identifier + keyword tokens → falls through to plain keyword search.
  • Zero result → drop deliveryFlags and retry (if Prime was set).
  • Still zero result → drop searchIndex / browseNodeId and retry (if category was set).
  • At most 1 + RELAXATION_MAX_RETRIES Amazon S2S calls per request.
  • Total wall-clock for relaxation respects RELAXATION_TIME_BUDGET_MS (early-exit on overshoot).
  • Relaxation never mutates keywords content.
  • Zero results post-relaxation → 200 with items: [] (not an error).
  • Amazon “adjacent products” filtered out via ItemInfo.ExternalIds.{EANs,UPCs,ISBNs}.DisplayValues exact-match.
  • Items echoed by multiple input identifiers deduped by ASIN (first occurrence preserved).
  • Output ordered as Amazon returned them, with non-matches removed and ASIN duplicates collapsed.
  • Amazon SDK throws / rejects / 5xx / throttle → AMAZON_API_ERROR (502).
  • src/mocks/handlers/amazon.ts adds handlers for /api/amazon/search covering: success (multi-item), zero results (200 + empty), INVALID_SEARCH_INPUT, AMAZON_API_ERROR, identifier-mode success.
  • Layer-1 consumer tests (1–2) fetch through MSW and assert the response shape.
  • No live Amazon calls run in CI.
  • All defensive-filter steps unit-tested independently.
  • resolveCategory covers identity matches, synonym matches, and unresolved labels.
  • filterByExternalIds covers exact match, no-match, multiple identifier types per item.
  • ASIN dedup tested with multi-identifier input pointing to the same ASIN.
  • Layer-2 tests cover creatorsClient.searchItems happy paths per mode and each error class.
  • Route-level tests cover the full matrix above.
  • Coverage gates pass: jest.config.js enforces global thresholds lines: 84, statements: 84, functions: 81, branches: 72. New code must keep the global aggregates above these values, and new modules should individually exceed them by a safety margin (target ≥ 90% lines / statements, ≥ 85% branches on new files, since the new code is mostly pure functions).
  • Sentry user-span wraps the route handler.
  • Each Amazon S2S call (search + relaxation retries + identifier resolution) carries its own timed span as a child of the request span.
  • Errors caught at the route boundary are reported to Sentry with correlation to the request span.
  • If the Task 1 audit flagged any of the above missing in /api/amazon/import, the retrofit lands in this PR.
  • No new external dependencies, server secrets, env vars, or operations-backend changes.
  • Reuses existing creatorsClient, OAuth2 credentials, marketplace header, affiliate-tag helper.
  • No UI/UX changes; no React component, page, hook, or Storybook story modified beyond keeping existing tests green.
  • npx eslint . clean.
  • npx tsc --noEmit clean.
  • npx jest --no-coverage --watchAll=false --forceExit green.
  • npm run build succeeds locally (with the placeholder AMAZON_CREATORS_* env vars from knowledge-base/amazon-creators-local-env.md).
  • PR #836 body has a single consolidated ## CHANGELOG section rolling up the project-setup, AGENTS.md shim, and BFF additions into one ### Added block (plus the existing ### Fixed).
Risk / BlockerImpactMitigation
Task 1 audit surfaces an existing error-envelope or auth pattern that conflicts with the spec’s informal shapeForces a contract revision late in the projectTask 1 explicitly outputs an audit note before Task 2 starts. Any mismatch with the spec is escalated to the user before committing implementation.
/api/amazon/import is materially behind on Sentry conventions and the retrofit is largeInflates PR #836 scope and review timeTask 19 produces a sized estimate of the retrofit and surfaces it to the user with options and trade-offs (see Retrofit decision protocol). User decides scope; decision recorded in this file before Task 19 proceeds.
Coverage thresholds (jest.config.js: 84/84/81/72 global)Global aggregates could dip if large new code lands sparsely testedNew modules are pure functions with deterministic input/output. Target ≥ 90% lines/statements / ≥ 85% branches on new files, well above the global gates.
Amazon SDK quirks (e.g. response shape variations across modes)Identifier-mode filtering or default-mode mapping silently mishandles edge casesLayer-2 tests use realistic fixtures captured from the SDK’s documented types; identifier-mode “adjacent products” case is an explicit test scenario.
Branch divergence from main while implementation is in flight (significant refactors land that touch BFF / Sentry / mocks / shared helpers, likely outside this project’s scope)Conflict resolution cost; risk of silently inheriting a contract or pattern change without noticingCheck origin/main staleness at every phase boundary. Run git -C <fe-worktree> fetch origin && git -C <fe-worktree> log --oneline HEAD..origin/main -- <touched-paths> (see Staleness-check command). If commits land that touch the route, the client wrapper, MSW handlers, env, observability, or any helper this project depends on, pause and either rebase or surface the conflict to the user before continuing. Force-push (with --force-with-lease) after each rebase.
Smart-identifier work uncovers further sub-bits not documentedImplementation paused for spec editsSpec already documents the sub-bits (conditional resource selector, response filter, dedup, ordering). New findings get flagged to the user, not silenced.

Task 19 begins with an audit phase only — read every observability touchpoint in /api/amazon/import and the surrounding stack, then compare against the workspace’s current Sentry conventions. The output is a one-paragraph summary recorded in Audit Outputs (under a Task 19 subheading, alongside Task 1’s audit) with:

  • A list of probes present today.
  • A list of probes the conventions expect but that are missing.
  • A sized estimate (Small / Medium / Large) of how much code change the retrofit would require.

The implementation pauses there. Three options are presented to the user, with the recommendation:

OptionDescriptionTrade-off
(a) Retrofit /import in this PR (recommended for Small)Apply the same observability envelope to /import as part of PR #836. Both routes ship at parity.PR grows by N lines; review surface grows.
(b) Retrofit /import in a follow-on PRShip /search with full observability; file a separate ticket for /import retrofit, link to it from PDEV-457’s completion notes.Parity gap exists between merges; ticket may drift if not picked up promptly.
(c) No retrofitShip /search matching /import’s current (lower) observability bar.Codifies the gap; harder to argue for retrofit later when both routes look the same.

The user’s decision is recorded as a row in Open Questions and Decisions (new row I6 — /api/amazon/import observability retrofit scope) before Task 19 proceeds beyond the audit. Default if no response within the work window: option (b) — file the follow-on ticket and continue.

At every phase boundary, before starting the next phase’s first task, run:

Terminal window
git -C /Users/jmp/code/arda/projects/amazon-import-search-worktrees/arda-frontend-app fetch origin
git -C /Users/jmp/code/arda/projects/amazon-import-search-worktrees/arda-frontend-app log --oneline HEAD..origin/main -- \
'src/app/api/amazon/**' \
'src/server/routes/amazon/**' \
'src/server/lib/amazon/**' \
'src/lib/env.ts' \
'src/lib/shared/amazon/**' \
'src/mocks/handlers/amazon.ts' \
'src/mocks/data/**' \
'instrumentation.ts' \
'instrumentation-client.ts' \
'sentry.*.config.ts' \
'jest.config.js' \
'package.json' \
'package-lock.json'

If the output is non-empty, stop and triage before continuing:

  • Inspect each commit. Most edits in adjacent areas are safe to inherit via rebase.
  • If a commit changes a contract this plan depends on (error envelope, Sentry primitive, MSW pattern, route conventions, jest thresholds), surface it to the user with the commit SHA and a one-line summary. Decide whether the plan needs an amendment before proceeding.
  • Rebase the branch (git -C <worktree> rebase origin/main), reconcile any conflicts, re-run the local gates, force-push with --force-with-lease.

Refresh the staleness check at every phase boundary, not just on errors — the smaller the increment, the cheaper the rebase.

When the work is complete, the following artifacts should exist:

ArtifactStatus
arda-frontend-app PR #836 includes the full BFF implementation (route handler, Next.js handler, client wrapper, helpers, constants, MSW handlers, tests).Pending
All local gates green; PR-body ## CHANGELOG consolidated.Pending
documentation PR #83 includes this task-plan.md and any verification-pass output that warrants archival.Pending
roadmap/in-progress/amazon-import-search/ moves to roadmap/completed/amazon-import-search/ after the PRs merge.Complete
PDEV-457 marked Done in Linear.Pending

Holds the audit notes for the two audit-phase tasks in this project: Task 1 (verification pass — locks the existing /api/amazon/import shape, auth wiring, Sentry primitives, MSW conventions, and per-file coverage thresholds before implementation begins) and Task 19 (observability retrofit audit — compares the existing /import observability surface against the workspace’s Sentry conventions and estimates retrofit scope before the Phase 11 implementation continues).

Intentionally blank at plan-time. Task 1’s output goes here as a short audit note covering:

  • Existing error-envelope shape on /api/amazon/import
  • Existing auth wiring (Cognito JWT verification path)
  • Existing Sentry integration primitives used in the repo
  • Existing Amazon-API mocking pattern in creators-client.test.ts
  • Existing MSW handler conventions in src/mocks/handlers/amazon.ts
  • Where BASE_ITEM_RESOURCES should be factored out of
  • Per-file coverage threshold actually enforced
  • Anything else that should be locked before implementation

Intentionally blank at plan-time. Task 19’s audit summary goes here per the Retrofit decision protocol:

  • Probes present today on /api/amazon/import (spans, breadcrumbs, metrics, captureException sites).
  • Probes the current Sentry conventions expect but that are missing.
  • Sized estimate (Small / Medium / Large) of the retrofit code change.

Decision recorded against this audit goes as a row in Open Questions and Decisions (row I6).


Copyright: (c) Arda Systems 2025-2026, All rights reserved