PRD: Barcode Scan & Identification at the Counter
| Module | Products | PRD ID | PRD-PID-001 |
| Status | Ready to dev | FEAT | PID |
| Epic | BANA-1319 | Plane | BANA-1752 |
| Date | 2026-07-06 | Version | v1.0 |
| Packages | @nx/commerce | URD | PID |
| Surface | Sale · POS | ||
| Owner | Phát Nguyễn | ||
What barcode scan & identification is
A cashier scans or types a barcode at the counter and gets exactly one answer: the variant it names, priced and ready for the cart, or a plain "not found" so they can fall back to search. This PRD is the shared identification contract - scheme-priority disambiguation, not-found handling, scanner/manual-entry parity - that every counter-selling flow (retail counter mode, product picker's scan bypass) already assumes but none of them fully own today.
Why this needs its own contract
Barcode identifiers already exist on variants (URD-PID-001…004, Built), and a basic lookup already resolves a scanned code to a variant - both the product picker and retail counter mode reference it as a working head start (URD-PCK-007, URD-RTL-005). But neither owns the identification behavior itself - each treats "scan → item in cart" as an assumed black box:
- No defined not-found contract. What the cashier sees when a scanned code matches nothing is unspecified - today's shallow lookup does not guarantee a clean, actionable response.
- No disambiguation rule. Identifiers are unique per scheme (URD-PID-001), not globally - the same string can legally exist as a barcode on one variant and a SKU on another.
- No scanner/manual-entry parity guarantee - the POS already models a dedicated
BARCODE_SCANNERdevice type, but a cashier typing the same digits by hand should behave identically; this is assumed, not specified. - No home for the retail edge case. Grocery-style variable-weight barcodes (a fixed prefix + item code + an embedded weight or price, common on scale-printed labels) need a different resolution rule than a plain product barcode.
One scan, from code to cart
A cashier scans a code at the counter. The system checks identifier schemes in a fixed order - barcode first, then SKU, QR code, slug, system code - and returns the first scheme's match. If that string is both a barcode on variant A and a SKU on variant B, the result is always variant A, because barcode comes first. The resolved variant carries its live price and availability, identical to a manual pick - the item is added to the cart right away. If the string matches nothing, the cashier gets a plain "not found" message and a prompt to search by name or SKU instead of re-scanning the same code.
Where this goes wrong
A string can be valid under two schemes at once
Identifiers are unique per scheme, not globally. A merchant can legally register the exact same string as a barcode on one variant and a SKU on another - both are valid at creation time.
A naive lookup could return the wrong variant, or both. A fixed scheme-priority order is the only thing that makes resolution deterministic.
1. Goals & Non-Goals
Goals
- A barcode entered or scanned at the counter resolves to exactly one variant, ready to add to the cart (URD-PID-005).
- A barcode that matches nothing returns a plain, actionable "not found" - never a crash, never a silent no-op - with a fallback to name/SKU search (URD-PID-006).
- When the same string exists under more than one identifier scheme, resolution is deterministic - a fixed scheme order, never an ambiguous or duplicate result (URD-PID-007).
- A hardware/keyboard-wedge scanner and manual keypad entry go through the same lookup, with identical results (URD-PID-008).
- A resolved variant carries the same channel-scoped price and availability a manually picked item would (URD-PID-009).
- Variable-weight barcodes (scale labels) resolve by their item-code segment, reading the embedded segment as quantity/price (URD-PID-010, Could - v1 stretch).
Non-Goals
- Cart/checkout mechanics once an item is identified - owned by Sale Order (
ORD) and Retail counter mode (RTL). - The option-picker chooser UX and its own scan-bypass wiring - owned by Product Picker (
PCK); this PRD supplies the identification result it bypasses to. - Barcode input, uniqueness, and printing when an owner assigns a code to a variant - already specified in PRD-PRD-001 and built (URD-PID-001…004).
- Hardware scanner pairing/configuration UI - v1 assumes standard keyboard-wedge (HID) mode, matching the existing
BARCODE_SCANNERdevice model; a dedicated pairing flow is a later increment if needed. - Camera-based scanning UX (mobile/tablet POS) - the identification contract is device-agnostic (it only ever sees a decoded string); the camera capture UI itself is a front-end concern (FE handoff doc).
- Offline resolution - Phase 2 has no offline selling (locked decision, per PRD-RTL-001 Non-Goals).
2. Success Metrics
| Metric | Target |
|---|---|
| Scan-to-resolved latency | Feels instant on current POS hardware - no perceptible delay vs today's picker |
| Not-found clarity | 100% of unmatched scans return an explicit "not found" response the counter can act on - zero silent failures |
| Ambiguous resolution incidents | Zero - every scan of a string registered under multiple schemes resolves to one deterministic result |
| Scanner/manual parity | Identical resolution result whether the code arrives via hardware scanner or manual entry |
3. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Retail cashier | Stands at a scan-first counter; needs every scan to land the right item, every time, with no dead ends |
| F&B cashier | Wants packaged/grab-and-go items (bottled drinks, snacks) to skip the picker via a quick scan |
| Grocery cashier | Scans scale-printed labels where the barcode encodes weight/price, not just the item |
| Owner | Assigns barcodes to variants and expects them to simply work at the counter, across schemes, without collisions |
Core scenario: a cashier scans (or types) a code at the counter → the system identifies exactly one variant, with its live price and availability → the item is handed off to the cart. If the code matches nothing, the cashier is told plainly and can search by name/SKU instead.
4. User Stories
| # | As a | I want | So that |
|---|---|---|---|
| 01 | cashier | to scan a product's barcode and have the right variant land in the cart immediately | no picker, no guessing |
| 02 | cashier | to be told clearly it's not found when I scan something the system doesn't recognize | I can search by name instead of re-scanning blindly |
| 03 | owner | to assign a barcode to a variant and trust that scanning it at the counter always resolves to that exact variant | never a different one that happens to share the code under another scheme |
| 04 | grocery cashier | to scan a scale-printed label and have the correct item added with the weighed quantity/price | not a fixed unit price |
5. Functional Requirements
Delivers URD PID URD-PID-005…010 (Musts 005-007; Shoulds 008-009; Could 010).
| # | Requirement | Status | URD ref |
|---|---|---|---|
FR-001 | An entered/scanned code resolves to exactly one variant, never a list. • The lookup takes a raw string plus the caller's merchant and returns either one variant or a not-found result. • A basic version already works (barcode lookup already resolves to a variant for PRD-PCK-001's scan-bypass and PRD-RTL-001's counter mode) - which is why it is 🔶 rather than 🚧. • What's missing: a single, formal contract every caller can rely on, instead of each reimplementing its own resolution. | 🔶 | URD-PID-005 |
FR-002 | A code matching nothing always returns a plain, structured "not found" result (NOT_FOUND), distinct from a server error.• The lookup never throws and never returns an empty success. • The counter surfaces a plain message and offers the name/SKU search field as the next step, not repeated blind re-scans. | 🚧 | URD-PID-006 |
FR-003 | A fixed scheme order resolves a string that matches more than one identifier scheme - barcode → SKU → QR code → slug → system code. • Identifiers are unique only within a scheme, so the same raw string can legally exist as a barcode on one variant and a SKU on another. • Resolution returns the first scheme's match, checked in that order. • Barcode and SKU lead because they're the two schemes cashiers actually scan/type at the counter; slug and system code sit last since they serve internal/API lookups, not counter entry. | 🚧 | URD-PID-007 |
FR-004 | A hardware-scanned code and a manually typed code go through the same lookup call, with identical results. • A hardware/keyboard-wedge scanner emits keystrokes indistinguishable from typing, so there is no separate "scanner path" to keep in sync. • Enforced by construction - a single entry point - rather than tested as a special case. | 🚧 | URD-PID-008 |
FR-005 | The resolved variant carries its channel-scoped price and availability alongside its identity. • The response bundles the variant with its already-resolved fare (via the existing FAR rule) and its channel-scoped availability - the same shape a manual product-pick would return.• A scanned item is indistinguishable from a picked one once it reaches the cart. | 🚧 | URD-PID-009 |
FR-006 | A scale-label barcode's item-code segment is looked up separately from its embedded weight/price segment. • A merchant that sells by weight configures a segment convention over the raw scanned string - typically a fixed prefix, an item-code segment, and an embedded weight/price segment (the common pattern on VN grocery-scale labels). • Only the item-code segment is passed into the normal identifier lookup; the embedded segment is decoded separately into a quantity or price override, never treated as part of the identifier itself. | 🚧 | URD-PID-010 |
5.1 Acceptance Criteria
- Scan a variant's registered barcode at the counter → exactly that variant resolves, with its live price and availability, ready to add to the cart.
- Scan or type a code with no matching identifier in the merchant → the counter receives an explicit "not found" result and offers name/SKU search, never a crash or a blank response.
- Register the same string as a barcode on variant A and, separately, as a SKU on variant B → scanning that string always resolves to variant A (barcode outranks SKU), never both, never an error.
- Feed the same code once via a hardware scanner and once by manual keypad entry → both calls hit the same lookup and return the identical result.
- Resolve a variant by barcode → the response includes its current channel-scoped price and availability, matching what a manual pick would show.
- (Could) Scan a scale-printed label with an embedded weight → the item resolves by its item-code segment, and the embedded weight is read into the line's quantity/price rather than blocking the lookup.
6. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Determinism | The same input string always resolves to the same result for a given merchant state - no randomness, no "first match wins" across schemes without an explicit, documented order |
| Tenancy & authz | Lookup is designed to be scoped to the caller's merchant, same as every other product/variant read (C-01, C-07) - today the barcode lookup path does not yet filter by merchant, see the gap in Q&A |
| Performance | Resolution completes fast enough to feel instantaneous at the counter - no regression vs the existing picker/search path |
| i18n | The "not found" message and search-fallback prompt are bilingual (EN/VI), matching the rest of the POS |
7. UX & Flows
This PRD specifies the identification contract - the request/response shape and its resolution rules. The screens that call it (retail counter mode's scan field, the product picker's barcode bypass) are specified in their own PRDs (PRD-RTL-001, PRD-PCK-001); this increment makes sure they call into one well-defined behavior instead of each assuming their own.
8. Data & Domain
| Concept | Role |
|---|---|
ProductIdentifier | Existing entity (scheme, identifier, principal_id) already backing SYSTEM/SLUG/SKU/BARCODE/QRCODE lookups (URD-PID-001…004) - reused as-is, no schema change |
| Scheme priority order | A resolution rule (barcode before SKU before other schemes), applied when a raw input string matches more than one scheme |
| Variable-weight barcode | A parsing rule over the raw scanned string - a configured prefix isolates the item-code segment from the embedded weight/price segment before the item-code segment is looked up as a normal identifier |
Conceptual only - full schema lives in the commerce domain model.
9. Dependencies & Assumptions
Depends on
| # | Feature | What it depends on |
|---|---|---|
| 01 | Existing identifier data model (URD-PID-001…004, Built) | This increment adds a resolution/lookup contract on top, no new storage. |
| 02 | BARCODE_SCANNER device type (already modeled) | v1 assumes standard HID/keyboard-wedge scanners; no new device integration required. |
| 03 | Fare resolution (FAR) | A resolved variant's live price still goes through the existing fare-resolution rule. |
Consumed by
- PRD-RTL-001 Retail Business Type - the scan-first counter mode calls this lookup on every scan (URD-RTL-005).
- PRD-PCK-001 Product Picker - the barcode-bypass path (URD-PCK-007) calls this lookup instead of opening the option chooser.
Assumptions
| # | Assumption | What breaks if wrong |
|---|---|---|
| 01 | A merchant that wants variable-weight barcode support configures its prefix/segment convention; v1 does not auto-detect it. | The prefix convention differs by scale vendor; a wrong configuration splits the item-code segment incorrectly and that scan fails to identify the right variant. |
| 02 | The requesting device is already authenticated/merchant-scoped, same as any other POS request. | A request that isn't properly merchant-scoped could resolve an identifier lookup against another merchant's data. |
10. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 - July 2026 (WK27-31), alongside Retail end-to-end |
| Rollout | All merchants; no flag needed - additive to an already-scoped-per-merchant lookup |
| Migration | None - reuses the existing ProductIdentifier data as-is |
| Launch criteria | AC-PID-02 and AC-PID-03 pass on a live merchant menu with both retail and F&B item mixes; not-found and scheme-priority behavior verified before PRD-RTL-001 pilots in August |
| Monitoring | Scan-to-resolve latency, not-found rate (signal for data-quality gaps), any scheme-collision occurrences |
References
- URD: Product - Identifiers (
PID) - Related PRD: PRD-PRD-001 Product catalogue, variants & identifiers · PRD-PCK-001 Product Picker · PRD-RTL-001 Retail Business Type
- Module: Product - URD · Phase 2 - By Month
- Epic BANA-1319
- Developer: @nx/commerce
Risks & Open Questions
| # | Risk / question | Mitigation / status |
|---|---|---|
| 01 | A merchant registers the same string as barcode on one variant and SKU on another (legal today - uniqueness is per-scheme, not global) - a naive lookup could return either, or both | Fixed scheme-priority order (URD-PID-007) makes resolution deterministic; recommend a follow-up: warn owners at identifier-assignment time when a string is already claimed under a different scheme |
| 02 | Variable-weight barcode prefix convention differs by country/vendor (common VN grocery scales use specific prefix ranges) | Out of scope to standardize in v1 (Could-priority, URD-PID-010); treat as merchant-configurable and revisit once a pilot grocery merchant surfaces real label formats |
| 03 | Camera-based scanning (mobile/tablet) decodes differently than a hardware scanner (may include extra whitespace/control characters) | The identification contract only accepts a clean decoded string; any device-specific decoding/normalization is a front-end concern, not part of this PRD |
| 04 | The identifier lookup does not yet filter by merchant on the barcode path; merchant needs to be added to both the uniqueness constraint and the lookup query | Tracked gap - today's system-level uniqueness constraint is global (no merchantId in the unique index) and the barcode lookup path does not filter by merchant; recommended follow-up: add merchantId to both the uniqueness constraint and the lookup query before wide rollout |
FAQ
| # | Question | Answer |
|---|---|---|
| 01 | Does this replace the product picker or retail counter mode? | No - both keep their own UX and cart handling; this PRD is what they call underneath to turn a scanned string into a variant. |
| 02 | What happens if two variants in different merchants really do share the exact same barcode string? | Identifiers are designed to be unique per merchant; today the system-level uniqueness constraint is still global (merchant is not part of the unique index) and the barcode lookup path does not yet filter by merchant either - this is a tracked gap toward letting two merchants reuse the same physical barcode without colliding. Same-merchant, cross-scheme collisions (URD-PID-007) already go through the priority rule as usual. |
| 03 | Is QR code scanning covered? | Yes - QR code is an existing identifier scheme and resolves through the same lookup and priority order as barcode/SKU. |