From 5a97b9dc598f654ea78aad54814fa4951332772e Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Thu, 11 Jun 2026 22:01:11 -0700 Subject: [PATCH] feat(products): status-filtered export_catalog snapshot query (PUC-9) --- backend/app/domains/products/repo.py | 17 ++++++++ backend/tests/test_products_export.py | 56 +++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 backend/tests/test_products_export.py diff --git a/backend/app/domains/products/repo.py b/backend/app/domains/products/repo.py index bcdd1fb..467f651 100644 --- a/backend/app/domains/products/repo.py +++ b/backend/app/domains/products/repo.py @@ -104,6 +104,23 @@ def load_catalog(conn: psycopg.Connection, storefront_id: int) -> dict[str, Cata 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: return conn.execute( "SELECT count(*) FROM product WHERE storefront_id = %s", (storefront_id,) diff --git a/backend/tests/test_products_export.py b/backend/tests/test_products_export.py new file mode 100644 index 0000000..5ea3fd4 --- /dev/null +++ b/backend/tests/test_products_export.py @@ -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"]