# Barcodes

Anonymous barcode lookup through a product catalog adapter, and learning unknown codes without any household id (US-11, US-14, ADR-0019).

## What it does [#what-it-does]

The household app scans a barcode (EAN-8, EAN-13, UPC) and asks `barcodes.lookup` what it is. The server answers in this order:

1. a **learned barcode**: the list rule most homes filed the code under;
2. the **product catalog** behind an adapter: Open Food Facts in production, a synthetic fake everywhere else. The product's categories map to a list rule (`ruleForCategories`), and its net content to one pack in the rule's unit (9000 ml → 9 L);
3. otherwise `{ status: "unknown" }`. A catalog that times out (3 s) or fails also answers unknown, so the app goes on from the list instead of waiting.

After an unknown scan the app shows the list; the item the household picks is sent to `barcodes.learn` as the code and the rule id, nothing else. The app also keeps the code on the device, so the next scan works offline.

Only the code reaches the catalog, and from the server, never from the phone (ADR-0019). The app shows the ODbL attribution next to every catalog result.

## Procedures [#procedures]

| Procedure                                          | Who                | Failures                                                         |
| -------------------------------------------------- | ------------------ | ---------------------------------------------------------------- |
| `barcodes.lookup` · `GET /api/barcodes/{code}`     | anyone, no account | `BAD_REQUEST` (not 8 to 14 digits)                               |
| `barcodes.learn` · `POST /api/barcodes/learned`    | anyone, no account | `BAD_REQUEST` (shape, any extra field, rule on no published list) |

`learnInput` is strict: a payload that carries a household id, or any other field, is refused.

## Data and isolation [#data-and-isolation]

| Table             | Tenant access                                                                                                   |
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
| `learned_barcode` | select and insert for every trusted session; update of `votes` and `learned_on` only; no delete. Policies on `app.session_trusted()`, like `feature_flag` |

A row is `(code, rule_id)`, `votes` and `learned_on` (a date, never a finer time). There is no Organization, user, device or household column. Teaching a code twice for the same rule adds a vote; the lookup takes the rule with the most votes, so one wrong answer cannot replace the others.

Migrations: `20261106213000_learned_barcode` (table, policies) and `20261106213001_learned_barcode_grants`.

## Configuration [#configuration]

| Variable          | Values                     | Default |
| ----------------- | -------------------------- | ------- |
| `BARCODE_CATALOG` | `fake`, `openfoodfacts`    | `fake`  |

Production sets `BARCODE_CATALOG=openfoodfacts`. Unset or invalid keeps the fake, which knows `4005906003021` and `4008501011009` (still mineral water, a crate of 6 × 1.5 L), `4000000000017` (spaghetti) and `4000000000024` (chocolate, no rule fits).

## Tests [#tests]

```sh
node scripts/test-local-postgres.mjs exec pnpm --filter @varu/api exec vitest run src/features/barcodes
```

