Problem
POST /seeder/phase2-enrichment (app/features/seeder/service.py phase2_enrichment(...); route at app/features/seeder/routes.py:313) inserts into replenishment_event, exogenous_signal, and sales_returns without conflict handling. A second call against an already-enriched database raises IntegrityError on uq_exogenous_signal_per_store (constraint name confirmed during the PRP-38 dogfood), and the unhandled exception surfaces as HTTP 500 instead of a clean RFC 7807 problem+json.
Repro
docker compose up -d
uv run alembic upgrade head
# Seed once, then enrich twice.
curl -s -X POST http://localhost:8123/seeder/generate -d '{"scenario":"showcase_rich","seed":42,"force":true}' -H 'Content-Type: application/json'
curl -s -X POST http://localhost:8123/seeder/phase2-enrichment -H 'Content-Type: application/json' -d '{}' # → 200 (first call succeeds)
curl -s -X POST http://localhost:8123/seeder/phase2-enrichment -H 'Content-Type: application/json' -d '{}' # → 500 (IntegrityError: uq_exogenous_signal_per_store)
Scope
The bug is reachable only on a manual second call. The showcase pipeline (POST /demo/run with scenario=showcase_rich) always seeds first inside the same run before calling phase2_enrichment, so the pipeline path never hits the duplicate state.
Fix options (~10-20 LOC)
- Idempotent insert. Add
.on_conflict_do_nothing() to the three Phase-2 inserts (replenishment_event, exogenous_signal, sales_returns) so a repeat call is a no-op. Cleanest, matches the app/features/ingest/service.py upsert idiom.
- Clean 422. Catch
IntegrityError in the service, raise UnprocessableEntityError("Phase 2 already enriched") → RFC 7807 422. Cheaper but explicit-failure UX (the operator must drop the existing enrichment to re-run).
Recommendation: option 1 plus a route test that calls the endpoint twice and asserts both responses are 200.
Notes
Problem
POST /seeder/phase2-enrichment(app/features/seeder/service.pyphase2_enrichment(...); route atapp/features/seeder/routes.py:313) inserts intoreplenishment_event,exogenous_signal, andsales_returnswithout conflict handling. A second call against an already-enriched database raisesIntegrityErroronuq_exogenous_signal_per_store(constraint name confirmed during the PRP-38 dogfood), and the unhandled exception surfaces asHTTP 500instead of a clean RFC 7807 problem+json.Repro
Scope
The bug is reachable only on a manual second call. The showcase pipeline (
POST /demo/runwithscenario=showcase_rich) always seeds first inside the same run before callingphase2_enrichment, so the pipeline path never hits the duplicate state.Fix options (~10-20 LOC)
.on_conflict_do_nothing()to the three Phase-2 inserts (replenishment_event, exogenous_signal, sales_returns) so a repeat call is a no-op. Cleanest, matches theapp/features/ingest/service.pyupsert idiom.IntegrityErrorin the service, raiseUnprocessableEntityError("Phase 2 already enriched")→ RFC 7807 422. Cheaper but explicit-failure UX (the operator must drop the existing enrichment to re-run).Recommendation: option 1 plus a route test that calls the endpoint twice and asserts both responses are
200.Notes