docs(products): DOC-1 export ops + TEL-3, DOC-4 serializer/round-trip (SLICE-6)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 22:14:30 -07:00
parent 8077f2e07b
commit 767011fbae
2 changed files with 47 additions and 2 deletions
+16 -2
View File
@@ -41,8 +41,21 @@ never file names, URLs, catalog content, or secret bytes.
| --- | --- | --- | | --- | --- | --- |
| TEL-1 `import_draft_created` | validation completes, draft stored | `storefront_id, dialect, row_count, adds, updates, unchanged, errors, unknown_columns_count, duration_ms` | | TEL-1 `import_draft_created` | validation completes, draft stored | `storefront_id, dialect, row_count, adds, updates, unchanged, errors, unknown_columns_count, duration_ms` |
| TEL-2 `import_run_completed` | apply transaction commits | `run_id, storefront_id, added, updated, errored, duration_ms` | | TEL-2 `import_run_completed` | apply transaction commits | `run_id, storefront_id, added, updated, errored, duration_ms` |
| TEL-3 `catalog_exported` | export stream completes | `storefront_id, status_filter, product_count, duration_ms` |
| TEL-6 `import_apply_failed` | apply transaction aborts unexpectedly | `draft_id, storefront_id, error_class` | | TEL-6 `import_apply_failed` | apply transaction aborts unexpectedly | `draft_id, storefront_id, error_class` |
### Export (PUC-9, SLICE-6)
`GET /api/products/export?status=all|active|draft|archived` streams the
storefront's catalog as a canonical-format CSV (one codec, two directions — the
same format the importer parses). It is **read-only** (no draft, no run) and
storefront-scoped (INV-14). An empty catalog — no products, or none matching the
status filter — returns `409 empty_catalog`; the Products page disables the
Export action with a note in that case. The round-trip is lossless (INV-12):
re-importing an unmodified export previews as all-unchanged with the import
action disabled (PUC-10). TEL-3 (`catalog_exported`) is emitted once the stream
completes — counts and duration only, never catalog content.
### RB-2 — import apply failed ### RB-2 — import apply failed
Triggered by ALR-2 (any TEL-6 event). The apply raised mid-transaction and Triggered by ALR-2 (any TEL-6 event). The apply raised mid-transaction and
@@ -94,8 +107,9 @@ gcloud beta monitoring channels list
### E2E browser suite ### E2E browser suite
- Lives at `e2e/` — Playwright, Chromium, four SLICE-5 scenarios - Lives at `e2e/` — Playwright, Chromium, six scenarios (SLICE-5:
(preview/confirm happy path, actionable errors, file rejection, cancel). preview/confirm happy path, actionable errors, file rejection, cancel;
SLICE-6: `e2e_export_download`, `e2e_roundtrip_noop`).
- Run with `bash scripts/e2e.sh`. The harness boots a **fresh `ecomm_e2e` - Run with `bash scripts/e2e.sh`. The harness boots a **fresh `ecomm_e2e`
database** against the local compose Postgres and serves the built SPA from database** against the local compose Postgres and serves the built SPA from
the backend on **:8765** (the deployed topology), so it needs the dev the backend on **:8765** (the deployed topology), so it needs the dev
+31
View File
@@ -13,6 +13,8 @@ the spec is SD-0002 in the content repo.
- `codec.py` — bytes → `ParsedFile`; file-level gates only. - `codec.py` — bytes → `ParsedFile`; file-level gates only.
- `validate.py` — rows → `CanonicalProduct` blocks + per-row errors. - `validate.py` — rows → `CanonicalProduct` blocks + per-row errors.
- `diff.py` — catalog × canonical products → apply plan + preview records. - `diff.py` — catalog × canonical products → apply plan + preview records.
- `serialize.py` — the export half: `CatalogProduct` snapshot → canonical CSV
(the inverse of `codec`/`validate`). DB-free, like `diff.py`.
- `repo.py` — SQL only: catalog snapshot, draft/run CRUD, apply primitives. - `repo.py` — SQL only: catalog snapshot, draft/run CRUD, apply primitives.
Never commits or rolls back. Never commits or rolls back.
- `service.py` — use-case orchestration; owns every transaction boundary and - `service.py` — use-case orchestration; owns every transaction boundary and
@@ -85,6 +87,31 @@ mismatch at confirm means the catalog drifted since preview → `PreviewStale`
product/variant/image writes, error rows, draft delete — is one transaction; product/variant/image writes, error rows, draft delete — is one transaction;
any exception rolls it back and emits TEL-6. any exception rolls it back and emits TEL-6.
## Export & the round-trip (SLICE-6)
`serialize.py` is the export half of "one codec, two directions": it turns the
`CatalogProduct` snapshot (the same one `repo.load_catalog` builds for the diff
engine) back into canonical CSV, writing exactly the columns `codec.py` /
`validate.py` parse, in the §6.5.1 row grammar — `HEADER` is the full canonical
column set; product fields + option names sit on the first row; one variant per
row; images interleave; a product with more images than variants emits
image-only rows.
`repo.export_catalog` returns the status-filtered snapshot list (sorted by
handle, deterministic); the `service.export_catalog` generator streams
`serialize.catalog_to_csv` over it and emits TEL-3 (`catalog_exported`) once the
stream is exhausted. The BFF wraps it in a `StreamingResponse`; an empty
(filtered) catalog raises `EmptyCatalog` **eagerly**`409 empty_catalog`
before any bytes stream.
**INV-12** (`diff(catalog, import(export(catalog))) = ∅`) is locked two ways: a
property test (`test_products_serialize.py`) runs the real
export→parse→diff loop over 200 generated **text-field** catalogs and asserts
every product is `unchanged`; the `e2e_roundtrip_noop` browser scenario does the
same through the UI (export download → re-upload → all-unchanged preview, import
disabled). Numeric/`Decimal` fields — the property test's deliberate blind spot
(string-form vs value-identity) — get explicit round-trip unit tests.
## Named seams (what later slices replace) ## Named seams (what later slices replace)
- **`import_draft.file_bytes` → objectstore key (SLICE-7).** The upload - **`import_draft.file_bytes` → objectstore key (SLICE-7).** The upload
@@ -107,6 +134,8 @@ any exception rolls it back and emits TEL-6.
| `backend/tests/test_products_codec.py` | file-level gates: parse, caps (INV-18), required columns, dialect | | `backend/tests/test_products_codec.py` | file-level gates: parse, caps (INV-18), required columns, dialect |
| `backend/tests/test_products_validate.py` | every §6.5.1 row-error rule, one fixture each | | `backend/tests/test_products_validate.py` | every §6.5.1 row-error rule, one fixture each |
| `backend/tests/test_products_diff.py` | classification, blank-vs-absent, option matching, fingerprint | | `backend/tests/test_products_diff.py` | classification, blank-vs-absent, option matching, fingerprint |
| `backend/tests/test_products_serialize.py` | serializer grammar + INV-12 property test (round-trip no-op over generated catalogs) + decimal round-trip |
| `backend/tests/test_products_export.py` | status-filtered snapshot, streamed export, EmptyCatalog, TEL-3 |
| `backend/tests/test_products_service.py` | draft lifecycle: validate/preview/discard, expiry, TEL-1 | | `backend/tests/test_products_service.py` | draft lifecycle: validate/preview/discard, expiry, TEL-1 |
| `backend/tests/test_products_invariants.py` | INV-10 (never deletes), INV-14 (storefront isolation), apply transactionality, TEL-6 | | `backend/tests/test_products_invariants.py` | INV-10 (never deletes), INV-14 (storefront isolation), apply transactionality, TEL-6 |
| `backend/tests/test_products_endpoints.py` | §6.4 API scenarios + auth/storefront gates | | `backend/tests/test_products_endpoints.py` | §6.4 API scenarios + auth/storefront gates |
@@ -114,3 +143,5 @@ any exception rolls it back and emits TEL-6.
| `e2e/tests/import-errors.spec.ts` | actionable row errors at preview and on the run report | | `e2e/tests/import-errors.spec.ts` | actionable row errors at preview and on the run report |
| `e2e/tests/import-file-rejected.spec.ts` | file-level rejection, picker stays live, no trace | | `e2e/tests/import-file-rejected.spec.ts` | file-level rejection, picker stays live, no trace |
| `e2e/tests/import-cancel.spec.ts` | cancel at preview leaves no trace | | `e2e/tests/import-cancel.spec.ts` | cancel at preview leaves no trace |
| `e2e/tests/export-download.spec.ts` | export downloads canonical CSV, status filter respected |
| `e2e/tests/roundtrip-noop.spec.ts` | export → re-import → all-unchanged, import disabled (PUC-10) |