From 24c7728559fd3682d669b0c4967b393d46ccb9ee Mon Sep 17 00:00:00 2001 From: Sam Rolfe Date: Fri, 7 Aug 2026 16:54:03 +1000 Subject: [PATCH] =?UTF-8?q?openspec:=20tag-registry-product-link=20change?= =?UTF-8?q?=20=E2=80=94=20real=20registry=20seed=20+=20products/orders=20s?= =?UTF-8?q?chema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tag-registry-product-link/.openspec.yaml | 2 + .../tag-registry-product-link/README.md | 3 ++ .../tag-registry-product-link/design.md | 41 +++++++++++++++++++ .../tag-registry-product-link/proposal.md | 28 +++++++++++++ .../specs/database/spec.md | 22 ++++++++++ .../specs/product-orders/spec.md | 26 ++++++++++++ .../specs/tag-registry/spec.md | 30 ++++++++++++++ .../tag-registry-product-link/tasks.md | 16 ++++++++ 8 files changed, 168 insertions(+) create mode 100644 openspec/changes/tag-registry-product-link/.openspec.yaml create mode 100644 openspec/changes/tag-registry-product-link/README.md create mode 100644 openspec/changes/tag-registry-product-link/design.md create mode 100644 openspec/changes/tag-registry-product-link/proposal.md create mode 100644 openspec/changes/tag-registry-product-link/specs/database/spec.md create mode 100644 openspec/changes/tag-registry-product-link/specs/product-orders/spec.md create mode 100644 openspec/changes/tag-registry-product-link/specs/tag-registry/spec.md create mode 100644 openspec/changes/tag-registry-product-link/tasks.md diff --git a/openspec/changes/tag-registry-product-link/.openspec.yaml b/openspec/changes/tag-registry-product-link/.openspec.yaml new file mode 100644 index 0000000..878dc31 --- /dev/null +++ b/openspec/changes/tag-registry-product-link/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-07 diff --git a/openspec/changes/tag-registry-product-link/README.md b/openspec/changes/tag-registry-product-link/README.md new file mode 100644 index 0000000..496c0f2 --- /dev/null +++ b/openspec/changes/tag-registry-product-link/README.md @@ -0,0 +1,3 @@ +# tag-registry-product-link + +Seed real preset tag registry (100 manufacturer IDs) + products/orders schema for stronger tag->product->order validation (mirrors 2014 product_id_hashes model) diff --git a/openspec/changes/tag-registry-product-link/design.md b/openspec/changes/tag-registry-product-link/design.md new file mode 100644 index 0000000..1b7b99a --- /dev/null +++ b/openspec/changes/tag-registry-product-link/design.md @@ -0,0 +1,41 @@ +## Context + +Phase 1/2 built the app with a DEV-only tag registry (TEST codes). The real product — 100 preset physical tags — was manufactured with URLs carrying opaque base64 `productid` values (the old system's double-encoded IDs). Those IDs are confirmed real (the user generated the URLs and sent them to the manufacturer) and are now in `db/preset_tag_ids.txt`. This change seeds them as the real registry and adds the products/orders linkage that the 2014 system used for stronger validation and billing. + +## Goals / Non-Goals + +**Goals:** +- The 100 real preset IDs are claimable (seeded registry). +- `products`, `orders` schema + `tags.product_id`/`order_id` exist for the admin (Phase 4) and billing (Phase 5). +- Idempotent migrations and seeds; no regressions to existing flows. + +**Non-Goals:** +- Laravel admin UI to assign products/orders (Phase 4). +- Billing enforcement (Phase 5). +- Changing the claim flow (still registry-by-code; the product link does not gate binding yet). +- Decoding the opaque product IDs (they are intentionally opaque; tag_code IS the identifier). + +## Decisions + +1. **Registry file = single source of truth.** `db/preset_tag_ids.txt` (committed) holds the 100 IDs; the seed reads it. Manufacturing new tags = append to this file + re-seed. *Alternative:* hardcode in Go — rejected, file is data not code. +2. **`seed-registry` is a separate command from `seed`.** `make seed` keeps TEST codes for dev; `make seed-registry` adds the real IDs. Both idempotent (`InsertTag` + unique-violation skip, same pattern as the existing seed). *Alternative:* replace TEST entirely — rejected, dev needs stable TEST codes until production seeding is the only path (Phase 7). +3. **Schema mirrors the 2014 model with modern naming.** `products` ≈ old `products`/`id_products`; `orders` ≈ old `online_orders`; `tags.product_id`+`order_id` ≈ old `online_order_product_details` + `product_id_hashes` mapping. The old `product_id_hashes` table is unnecessary here because `tag_code` IS the hash in our system — registry presence is the validation. +4. **No new sqlc queries yet.** Schema + models only (sqlc regenerates `Product`/`Order` models and extends `Tag`); queries come with the Phase 4 admin. This keeps the change small and behaviour-stable. +5. **Idempotent ALTERs** for existing DBs: `ALTER TABLE tags ADD COLUMN IF NOT EXISTS product_id ...` etc. — same pattern as `sms_enabled` (Phase 2). + +## Risks / Trade-offs + +- [TEST codes + real IDs coexist in dev] → accepted; production seeding documented as registry-only (Phase 7 gate). +- [Opaque IDs can't be human-decoded] → by design (anti-enumeration); the file maps ID → URL format for reference. +- [products/orders unused until Phase 4] → schema-only now, no dead code paths; avoids a breaking migration later. + +## Migration Plan + +1. `make db-up` (idempotent ALTERs + new tables on the existing dev DB). +2. `make seed-registry` (100 real IDs) — verify idempotent on re-run. +3. sqlc `make generate` (new models). No data migration needed (new nullable columns). + +## Open Questions + +- Whether the 100 IDs map 1:1 to physical tags currently in circulation (assumed yes — user created the URLs for the manufacturer). +- Product template contents (sku/name) — seeded later with the Phase 4 admin. diff --git a/openspec/changes/tag-registry-product-link/proposal.md b/openspec/changes/tag-registry-product-link/proposal.md new file mode 100644 index 0000000..bd2755c --- /dev/null +++ b/openspec/changes/tag-registry-product-link/proposal.md @@ -0,0 +1,28 @@ +## Why + +Anti-scam requires that only **preset, manufactured tag IDs** can be claimed — no user-created codes. The 100 real preset product IDs were recovered from the manufacturer list (`Where_woof_0_100.xls` → `db/preset_tag_ids.txt`) and confirmed as the actual tags (URLs sent to the manufacturer). This change (1) seeds that real registry so those IDs are claimable, and (2) adds the **products/orders schema** so each tag can be linked to a sold product and order — the stronger validation model the 2014 system had (`products` / `online_order_product_details` / `product_id_hashes`). + +## What Changes + +- **Registry seeding**: a `seed-registry` command (Makefile target) reads `db/preset_tag_ids.txt` and inserts the 100 real product IDs into `tags` (status `unset`), idempotently. DEV `TEST000001..25` codes remain for local testing; production seeding uses the real registry. +- **Products table**: `products` (sku unique, name, item_type, timestamps) — the product template (old `products` / `id_products`). +- **Orders table**: `orders` (account reference, status lifecycle pending/paid/lapsed/cancelled, timestamps) — sales record (old `online_orders`). +- **Tag linkage**: `tags` gains nullable `product_id` and `order_id` FKs — linking each tag to what it is and who bought it (old `online_order_product_details` + `product_id_hashes` mapping). No change to the claim flow yet (binding remains registry-by-code); the linkage is populated by the Laravel admin (Phase 4) and powers billing (Phase 5). +- No app behaviour change to auth/tags/scan in this change. + +## Capabilities + +### New Capabilities + +- `tag-registry`: real preset IDs are seedable into the registry; binding remains registry-only; unknown/self-made codes stay rejected. +- `product-orders`: products + orders tables and the tag→product→order linkage exist for admin/billing (Phase 4/5). + +### Modified Capabilities + +- `database`: ADDED — `products`, `orders` tables; `tags.product_id`, `tags.order_id` columns. + +## Impact + +- **New**: `frontend/cmd/seed-registry/main.go` (reads `db/preset_tag_ids.txt`), Makefile `seed-registry` target, `db/preset_tag_ids.txt` (already committed, `833c292`). +- **Schema**: `db/schema.sql` gains `products`, `orders` + two `ALTER TABLE tags ADD COLUMN IF NOT EXISTS` lines (idempotent); sqlc regenerated (new models; no new queries needed yet). +- **No change** to handlers, templates, or the scan flow. diff --git a/openspec/changes/tag-registry-product-link/specs/database/spec.md b/openspec/changes/tag-registry-product-link/specs/database/spec.md new file mode 100644 index 0000000..dc527e0 --- /dev/null +++ b/openspec/changes/tag-registry-product-link/specs/database/spec.md @@ -0,0 +1,22 @@ +## ADDED Requirements + +### Requirement: products table +The database SHALL include a `products` table (unique sku, name, item_type, created_at/updated_at). + +#### Scenario: Fresh database +- **WHEN** `db/schema.sql` is applied to an empty database +- **THEN** the `products` table exists with its columns + +### Requirement: orders table +The database SHALL include an `orders` table (account reference, status with allowed values, timestamps). + +#### Scenario: Fresh database +- **WHEN** `db/schema.sql` is applied to an empty database +- **THEN** the `orders` table exists with its columns + +### Requirement: tag product/order columns +The `tags` table SHALL gain nullable `product_id` and `order_id` columns referencing `products(id)` and `orders(id)`, added idempotently for existing databases. + +#### Scenario: Existing database migration +- **WHEN** `db/schema.sql` is re-applied to a pre-existing database +- **THEN** the two columns exist, nullable, without error diff --git a/openspec/changes/tag-registry-product-link/specs/product-orders/spec.md b/openspec/changes/tag-registry-product-link/specs/product-orders/spec.md new file mode 100644 index 0000000..0fd4947 --- /dev/null +++ b/openspec/changes/tag-registry-product-link/specs/product-orders/spec.md @@ -0,0 +1,26 @@ +## ADDED Requirements + +### Requirement: Products table +The `products` table SHALL store product templates with a unique sku, a name, an optional default item type, and timestamps. + +#### Scenario: Unique sku +- **WHEN** a second product row is inserted with an existing sku +- **THEN** the insert is rejected + +### Requirement: Orders table +The `orders` table SHALL record sales with an account reference and a status lifecycle (`pending`, `paid`, `lapsed`, `cancelled`). + +#### Scenario: Order lifecycle +- **WHEN** an order is created as `pending` and later marked `paid` +- **THEN** the status reflects each state + +### Requirement: Tag to product and order linkage +Each `tags` row SHALL optionally reference a `product_id` and an `order_id`, linking a tag to what it is and which sale it belongs to. + +#### Scenario: Linked tag +- **WHEN** an admin links a tag to a product and an order +- **THEN** the tag row carries both foreign keys + +#### Scenario: Unlinked tag +- **WHEN** a tag has no product/order assigned yet +- **THEN** both columns are NULL and the tag still functions (registry-only binding unchanged) diff --git a/openspec/changes/tag-registry-product-link/specs/tag-registry/spec.md b/openspec/changes/tag-registry-product-link/specs/tag-registry/spec.md new file mode 100644 index 0000000..2732ac5 --- /dev/null +++ b/openspec/changes/tag-registry-product-link/specs/tag-registry/spec.md @@ -0,0 +1,30 @@ +## ADDED Requirements + +### Requirement: Seed real preset tag registry +The system SHALL provide a `seed-registry` command that reads `db/preset_tag_ids.txt` and inserts each ID as a `tags` row (status `unset`), skipping IDs that already exist. Re-running SHALL be safe (idempotent). + +#### Scenario: First run +- **WHEN** `make seed-registry` runs against an empty tags table +- **THEN** all IDs from `db/preset_tag_ids.txt` exist as `unset` tags + +#### Scenario: Re-run +- **WHEN** `make seed-registry` runs again +- **THEN** no duplicate `tag_code` errors occur and the registry is unchanged + +### Requirement: Registry-only claimable codes +Only codes present in the `tags` table (the registry) SHALL be claimable. A code not in the registry SHALL be rejected (existing bind behaviour). + +#### Scenario: Real ID claimed +- **WHEN** an owner enters one of the 100 real preset IDs +- **THEN** the tag binds to their account (if unset) + +#### Scenario: Unknown code +- **WHEN** an owner enters a made-up code +- **THEN** binding is rejected with "Tag not found, or already claimed" + +### Requirement: DEV test codes coexist +The existing `TEST000001..25` seed SHALL remain available for local development, separate from the real registry seed. + +#### Scenario: Both seeds applied +- **WHEN** `make seed` and `make seed-registry` have both run +- **THEN** both TEST codes and real IDs exist and are claimable in dev diff --git a/openspec/changes/tag-registry-product-link/tasks.md b/openspec/changes/tag-registry-product-link/tasks.md new file mode 100644 index 0000000..342855e --- /dev/null +++ b/openspec/changes/tag-registry-product-link/tasks.md @@ -0,0 +1,16 @@ +## 1. Schema + +- [ ] 1.1 `db/schema.sql`: add `products` and `orders` tables; add idempotent `ALTER TABLE tags ADD COLUMN IF NOT EXISTS product_id BIGINT REFERENCES products(id)` and `order_id BIGINT REFERENCES orders(id)`; run `make db-up` +- [ ] 1.2 `make generate` (sqlc adds `Product`/`Order` models, extends `Tag`); verify build + +## 2. Registry Seed + +- [ ] 2.1 `frontend/cmd/seed-registry/main.go`: read `db/preset_tag_ids.txt` (skip comment lines), `InsertTag` each ID, skip unique-violations, report counts +- [ ] 2.2 Makefile `seed-registry` target (cwd frontend, schema path aware); README note + +## 3. Verification + +- [ ] 3.1 `make seed-registry` on fresh DB → 100 tags; re-run → no errors (idempotent) +- [ ] 3.2 Bind a real ID (register owner, add tag with one of the 100 IDs) → bound; made-up code → rejected +- [ ] 3.3 Existing suites still pass (`/tmp/verify.sh` Phase 1 + `/tmp/verify2.sh` Phase 2) with TEST codes +- [ ] 3.4 `openspec validate tag-registry-product-link`; commit