SLICE-6: export & the round-trip lock — canonical serializer + streamed export (SD-0002 §7.2) #29
@@ -104,6 +104,23 @@ def load_catalog(conn: psycopg.Connection, storefront_id: int) -> dict[str, Cata
|
|||||||
return catalog
|
return catalog
|
||||||
|
|
||||||
|
|
||||||
|
_EXPORT_STATUSES = ("all", "active", "draft", "archived")
|
||||||
|
|
||||||
|
|
||||||
|
def export_catalog(
|
||||||
|
conn: psycopg.Connection, storefront_id: int, status_filter: str
|
||||||
|
) -> list[CatalogProduct]:
|
||||||
|
"""The storefront's catalog as an ordered snapshot list, optionally filtered
|
||||||
|
by product status (PUC-9). Reuses load_catalog's snapshot builder; the
|
||||||
|
catalog fits in memory (≤5k rows, INV-18). Ordered by handle for a stable,
|
||||||
|
deterministic export."""
|
||||||
|
catalog = load_catalog(conn, storefront_id)
|
||||||
|
products = sorted(catalog.values(), key=lambda p: p.handle)
|
||||||
|
if status_filter and status_filter != "all":
|
||||||
|
products = [p for p in products if p.fields.get("status") == status_filter]
|
||||||
|
return products
|
||||||
|
|
||||||
|
|
||||||
def product_count(conn: psycopg.Connection, storefront_id: int) -> int:
|
def product_count(conn: psycopg.Connection, storefront_id: int) -> int:
|
||||||
return conn.execute(
|
return conn.execute(
|
||||||
"SELECT count(*) FROM product WHERE storefront_id = %s", (storefront_id,)
|
"SELECT count(*) FROM product WHERE storefront_id = %s", (storefront_id,)
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""Export: status-filtered catalog snapshot + the streamed service (PUC-9, TEL-3)."""
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import psycopg
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.domains import products
|
||||||
|
from app.domains.products import repo, serialize
|
||||||
|
from app.platform import db
|
||||||
|
|
||||||
|
CSV = (
|
||||||
|
b"Handle,Title,Vendor,Status,Variant Price\n"
|
||||||
|
b"active-mug,Active Mug,Acme,active,18.00\n"
|
||||||
|
b"draft-tee,Draft Tee,Acme,draft,24.00\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def migrated_conn(fresh_db_url):
|
||||||
|
with psycopg.connect(fresh_db_url) as conn:
|
||||||
|
db.migrate(conn)
|
||||||
|
yield conn
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def merchant(migrated_conn):
|
||||||
|
acct = migrated_conn.execute(
|
||||||
|
"INSERT INTO account (email) VALUES ('m@example.com') RETURNING id").fetchone()[0]
|
||||||
|
sf = migrated_conn.execute(
|
||||||
|
"INSERT INTO storefront (name) VALUES ('Shop') RETURNING id").fetchone()[0]
|
||||||
|
migrated_conn.execute(
|
||||||
|
"INSERT INTO storefront_membership (account_id, storefront_id) VALUES (%s,%s)", (acct, sf))
|
||||||
|
migrated_conn.commit()
|
||||||
|
return {"account_id": acct, "storefront_id": sf}
|
||||||
|
|
||||||
|
|
||||||
|
def _seed(conn, merchant):
|
||||||
|
draft = products.import_validate(conn, merchant["storefront_id"], merchant["account_id"], "c.csv", CSV)
|
||||||
|
products.confirm_draft(conn, merchant["storefront_id"], merchant["account_id"], draft["id"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_all_returns_both(migrated_conn, merchant):
|
||||||
|
_seed(migrated_conn, merchant)
|
||||||
|
snap = repo.export_catalog(migrated_conn, merchant["storefront_id"], "all")
|
||||||
|
assert {p.handle for p in snap} == {"active-mug", "draft-tee"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_status_filter(migrated_conn, merchant):
|
||||||
|
_seed(migrated_conn, merchant)
|
||||||
|
active = repo.export_catalog(migrated_conn, merchant["storefront_id"], "active")
|
||||||
|
assert [p.handle for p in active] == ["active-mug"]
|
||||||
|
draft = repo.export_catalog(migrated_conn, merchant["storefront_id"], "draft")
|
||||||
|
assert [p.handle for p in draft] == ["draft-tee"]
|
||||||
Reference in New Issue
Block a user