feat(products): confirm/apply in one transaction, runs, summary — INV-10/11/14 + TEL-2/6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 16:02:27 -07:00
parent fcbf1393f5
commit 0c7865e9e1
5 changed files with 439 additions and 39 deletions
+79
View File
@@ -0,0 +1,79 @@
"""SD-0002 invariants: INV-10 (never deletes), INV-14 (two-storefront zero bleed),
apply transactionality (§6.8), TEL-6."""
import json
import logging
import psycopg
import pytest
from app.domains import products
from app.domains.products import repo, service
from app.platform import db
CSV_A = b"Handle,Title,Variant Price\nmug,Mug,10.00\ntee,Tee,20.00\n"
CSV_PARTIAL = b"Handle,Title,Variant Price\nmug,Mug,12.00\n"
@pytest.fixture()
def migrated_conn(fresh_db_url):
with psycopg.connect(fresh_db_url) as conn:
db.migrate(conn)
yield conn
def _merchant(conn, email="m@example.com", shop="Shop"):
acct = conn.execute("INSERT INTO account (email) VALUES (%s) RETURNING id", (email,)).fetchone()[0]
sf = conn.execute("INSERT INTO storefront (name) VALUES (%s) RETURNING id", (shop,)).fetchone()[0]
conn.execute("INSERT INTO storefront_membership (account_id, storefront_id) VALUES (%s,%s)", (acct, sf))
conn.commit()
return acct, sf
def _import(conn, acct, sf, data):
d = products.import_validate(conn, sf, acct, "f.csv", data)
return products.confirm_draft(conn, sf, acct, d["id"])
def test_inv10_partial_file_never_deletes(migrated_conn):
acct, sf = _merchant(migrated_conn)
_import(migrated_conn, acct, sf, CSV_A)
before = migrated_conn.execute("SELECT count(*) FROM product").fetchone()[0]
_import(migrated_conn, acct, sf, CSV_PARTIAL)
after = migrated_conn.execute("SELECT count(*) FROM product").fetchone()[0]
assert after >= before == 2
def test_inv14_two_storefronts_zero_bleed(migrated_conn):
acct1, sf1 = _merchant(migrated_conn)
acct2, sf2 = _merchant(migrated_conn, "n@example.com", "Other")
_import(migrated_conn, acct1, sf1, CSV_A)
assert products.summary(migrated_conn, sf2)["product_count"] == 0
assert products.list_runs(migrated_conn, sf2) == []
_import(migrated_conn, acct2, sf2, CSV_A)
assert products.summary(migrated_conn, sf2)["product_count"] == 2
run1 = products.list_runs(migrated_conn, sf1)[0]
with pytest.raises(products.RunNotFound):
products.get_run(migrated_conn, sf2, run1["id"])
def test_apply_failure_rolls_back_whole_transaction_tel6(migrated_conn, monkeypatch, caplog):
acct, sf = _merchant(migrated_conn)
d = products.import_validate(migrated_conn, sf, acct, "f.csv", CSV_A)
def boom(*a, **k):
raise RuntimeError("mid-apply crash")
monkeypatch.setattr(service.repo, "insert_run_errors", boom)
lg = logging.getLogger("ecomm")
prior = lg.propagate
lg.propagate = True
try:
with caplog.at_level(logging.INFO, logger="ecomm.telemetry"):
with pytest.raises(RuntimeError):
products.confirm_draft(migrated_conn, sf, acct, d["id"])
finally:
lg.propagate = prior
assert migrated_conn.execute("SELECT count(*) FROM product").fetchone()[0] == 0
assert migrated_conn.execute("SELECT count(*) FROM import_run").fetchone()[0] == 0
assert migrated_conn.execute("SELECT count(*) FROM import_draft").fetchone()[0] == 1
events = [json.loads(r.message) for r in caplog.records if r.name == "ecomm.telemetry"]
assert any(e["event"] == "import_apply_failed" and e["error_class"] == "RuntimeError" for e in events)
+86
View File
@@ -115,3 +115,89 @@ def test_tel1_emitted(migrated_conn, merchant, caplog, telemetry_propagation):
and "duration_ms" in e and e["unknown_columns_count"] == 0
for e in events
)
UPDATE_CSV = b"Handle,Title,Vendor,Variant Price\nmoon-mug,Moon Mug,Acme,21.00\nstar-tee,Star Tee,Acme,24.00\n"
MIXED_CSV = b"Handle,Title,Variant Price\ngood-mug,Mug,10.00\nbad-tee,Tee,not-a-price\n"
def _validate(conn, m, data=GOOD_CSV):
return products.import_validate(conn, m["storefront_id"], m["account_id"], "cat.csv", data)
def test_confirm_applies_adds_and_records_run(migrated_conn, merchant):
draft = _validate(migrated_conn, merchant)
run_id = products.confirm_draft(
migrated_conn, merchant["storefront_id"], merchant["account_id"], draft["id"])
assert migrated_conn.execute("SELECT count(*) FROM product").fetchone()[0] == 2
run = products.get_run(migrated_conn, merchant["storefront_id"], run_id)
assert run["products_added"] == 2 and run["status"] == "complete"
assert run["by"] == "m@example.com"
assert run["image_progress"] == {"done": 0, "total": 0} and run["image_outcomes"] == []
assert migrated_conn.execute("SELECT count(*) FROM import_draft").fetchone()[0] == 0
def test_confirm_update_changes_only_diffed_fields(migrated_conn, merchant):
d1 = _validate(migrated_conn, merchant)
products.confirm_draft(migrated_conn, merchant["storefront_id"], merchant["account_id"], d1["id"])
d2 = _validate(migrated_conn, merchant, UPDATE_CSV)
assert d2["summary"] == {"adds": 0, "updates": 1, "unchanged": 1, "errors": 0}
products.confirm_draft(migrated_conn, merchant["storefront_id"], merchant["account_id"], d2["id"])
price = migrated_conn.execute(
"SELECT v.price FROM variant v JOIN product p ON p.id = v.product_id WHERE p.handle='moon-mug'"
).fetchone()[0]
assert str(price) == "21.00"
assert migrated_conn.execute("SELECT count(*) FROM product").fetchone()[0] == 2 # no dupes (BUC-3)
def test_confirm_mixed_applies_valid_records_errors(migrated_conn, merchant):
draft = _validate(migrated_conn, merchant, MIXED_CSV)
run_id = products.confirm_draft(
migrated_conn, merchant["storefront_id"], merchant["account_id"], draft["id"])
assert migrated_conn.execute("SELECT count(*) FROM product").fetchone()[0] == 1
run = products.get_run(migrated_conn, merchant["storefront_id"], run_id)
assert run["rows_errored"] == 1
assert run["errors"][0]["column"] == "Variant Price"
def test_confirm_stale_fingerprint_409_inv11(migrated_conn, merchant):
draft = _validate(migrated_conn, merchant)
other = _validate(migrated_conn, merchant)
products.confirm_draft(migrated_conn, merchant["storefront_id"], merchant["account_id"], other["id"])
with pytest.raises(products.PreviewStale):
products.confirm_draft(migrated_conn, merchant["storefront_id"], merchant["account_id"], draft["id"])
def test_confirm_nothing_to_apply_puc10(migrated_conn, merchant):
d1 = _validate(migrated_conn, merchant)
products.confirm_draft(migrated_conn, merchant["storefront_id"], merchant["account_id"], d1["id"])
d2 = _validate(migrated_conn, merchant)
assert d2["summary"]["unchanged"] == 2
with pytest.raises(products.NothingToApply):
products.confirm_draft(migrated_conn, merchant["storefront_id"], merchant["account_id"], d2["id"])
def test_runs_history_newest_first(migrated_conn, merchant):
d1 = _validate(migrated_conn, merchant)
products.confirm_draft(migrated_conn, merchant["storefront_id"], merchant["account_id"], d1["id"])
d2 = _validate(migrated_conn, merchant, UPDATE_CSV)
r2 = products.confirm_draft(migrated_conn, merchant["storefront_id"], merchant["account_id"], d2["id"])
runs = products.list_runs(migrated_conn, merchant["storefront_id"])
assert [r["id"] for r in runs][0] == r2
def test_summary_counts(migrated_conn, merchant):
assert products.summary(migrated_conn, merchant["storefront_id"]) == {
"product_count": 0, "image_problem_count": 0, "latest_run_id": None}
d = _validate(migrated_conn, merchant)
rid = products.confirm_draft(migrated_conn, merchant["storefront_id"], merchant["account_id"], d["id"])
s = products.summary(migrated_conn, merchant["storefront_id"])
assert s == {"product_count": 2, "image_problem_count": 0, "latest_run_id": rid}
def test_tel2_emitted_on_confirm(migrated_conn, merchant, caplog, telemetry_propagation):
draft = _validate(migrated_conn, merchant)
with caplog.at_level(logging.INFO, logger="ecomm.telemetry"):
products.confirm_draft(migrated_conn, merchant["storefront_id"], merchant["account_id"], draft["id"])
events = [json.loads(r.message) for r in caplog.records if r.name == "ecomm.telemetry"]
assert any(e["event"] == "import_run_completed" and e["added"] == 2 for e in events)