feat(products): streamed export service + TEL-3 + EmptyCatalog (PUC-9, §9.1)

This commit is contained in:
2026-06-11 22:03:09 -07:00
parent 5a97b9dc59
commit 1c2a6af986
4 changed files with 91 additions and 5 deletions
+38 -2
View File
@@ -7,14 +7,22 @@ writes exactly one row — the import_draft. TEL events per §9.1.
from __future__ import annotations
import time
from collections.abc import Iterator
from datetime import datetime, timezone
import psycopg
from app.platform import telemetry
from . import codec, diff, repo, validate
from .errors import DraftExpired, DraftNotFound, NothingToApply, PreviewStale, RunNotFound
from . import codec, diff, repo, serialize, validate
from .errors import (
DraftExpired,
DraftNotFound,
EmptyCatalog,
NothingToApply,
PreviewStale,
RunNotFound,
)
def import_validate(conn: psycopg.Connection, storefront_id: int, account_id: int,
@@ -218,6 +226,34 @@ def get_run(conn: psycopg.Connection, storefront_id: int, run_id: int) -> dict:
return run
def export_catalog(
conn: psycopg.Connection, storefront_id: int, status_filter: str
) -> Iterator[str]:
"""Stream the storefront's catalog as canonical CSV (PUC-9; INV-12 codec).
Read-only: builds the snapshot, then streams the serializer over it. TEL-3
is emitted once the stream is exhausted, with the product count and elapsed
time. Raises EmptyCatalog before yielding anything if the (filtered) catalog
is empty, so the BFF can answer 409 cleanly with no partial body.
"""
started = time.monotonic()
snapshot = repo.export_catalog(conn, storefront_id, status_filter)
if not snapshot:
raise EmptyCatalog()
def _stream() -> Iterator[str]:
yield from serialize.catalog_to_csv(snapshot)
telemetry.emit(
"catalog_exported",
storefront_id=storefront_id,
status_filter=status_filter,
product_count=len(snapshot),
duration_ms=int((time.monotonic() - started) * 1000),
)
return _stream()
def summary(conn: psycopg.Connection, storefront_id: int) -> dict:
"""The products dashboard counts (§6.4)."""
return {