SLICE-6: export & the round-trip lock — canonical serializer + streamed export (SD-0002 §7.2) #29
+16
-2
@@ -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-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` |
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
|
||||
- Lives at `e2e/` — Playwright, Chromium, four SLICE-5 scenarios
|
||||
(preview/confirm happy path, actionable errors, file rejection, cancel).
|
||||
- Lives at `e2e/` — Playwright, Chromium, six scenarios (SLICE-5:
|
||||
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`
|
||||
database** against the local compose Postgres and serves the built SPA from
|
||||
the backend on **:8765** (the deployed topology), so it needs the dev
|
||||
|
||||
@@ -13,6 +13,8 @@ the spec is SD-0002 in the content repo.
|
||||
- `codec.py` — bytes → `ParsedFile`; file-level gates only.
|
||||
- `validate.py` — rows → `CanonicalProduct` blocks + per-row errors.
|
||||
- `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.
|
||||
Never commits or rolls back.
|
||||
- `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;
|
||||
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)
|
||||
|
||||
- **`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_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_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_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 |
|
||||
@@ -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-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/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) |
|
||||
|
||||
Reference in New Issue
Block a user