openspec: tag-registry-product-link change — real registry seed + products/orders schema

This commit is contained in:
2026-08-07 16:54:03 +10:00
parent 833c292b6a
commit 24c7728559
8 changed files with 168 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-07

View File

@@ -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)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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