fix(seed): allow 'seed' entitlement source so demo templates load (PMS-709) #438

Merged
longjacksonle merged 2 commits from fix/PMS-709-seed-entitlement-source into main 2026-08-02 21:28:01 +02:00

PMS-709: Demo MSP seed load does nothing on staging

Failure point (traced from the button)

The admin "Load demo-msp" / "Load minimal" buttons (bunyip-web /admin/seed) POST to /admin/seed/template -> API POST /v1/admin/seed/import?template=<name> -> crate::seed::load. The load ran fine through groups, applications and users, then failed at the entitlements step: EntitlementRepository::grant(pool, user.id, app.id, None, "seed") inserts source = 'seed', but the application_entitlements_source_check constraint (from migration 20260605000010) only permitted 'admin' | 'stripe' | 'backfill'. Every insert hit a check-constraint violation (SQLSTATE 23514).

Why it looked silent: the shared AppError From<sqlx::Error> logs the real error (constraint name and all) but collapses the API-facing message to the generic "A database error occurred", so the admin saw at most an unhelpful 500 and the specific cause was only in bunyip-api's logs. And because seed::load is not transactional, the 3 groups + 8 apps + 42 users written before the failing step persisted as a partial seed with no entitlements and no feedback.

Reproduced against a migrated throwaway Postgres: load() returned Err(Db(DatabaseError { message: "A database error occurred" })) with application_groups=3, applications=13, users=42, application_entitlements=0, feedback=0. The direct insert surfaced the true error: violates check constraint "application_entitlements_source_check".

Fix

  • New migration 20260802000010_allow_seed_entitlement_source.sql widens the constraint to ('admin','stripe','backfill','seed'). Non-destructive: every existing row already carries a now-permitted value, so re-adding the constraint validates cleanly; the constraint name is preserved.
  • entitlement_source::SEED constant added so the loader stops passing a bare literal, matching the other call sites (ADMIN/STRIPE).
  • Env-gated regression test (bunyip-api/tests/pms709_repro.rs) that migrates a throwaway DB and asserts the whole documented demo-msp dataset loads (42 users, 8 apps, 3 groups, 6 entitlements, 7 feedback) with the 6 entitlements at source='seed', plus idempotent re-load.

Verification

Against a fresh database (sqlx applies all migrations including the new one), seed::load(demo-msp) returns LoadSummary { groups: 3, applications: 8, users: 42, entitlements: 6, feedback: 7 }. just migration-immutability and migration-version checks, cargo fmt --check, and clippy -D warnings on the touched crates all pass. Not yet run against live staging; that verification is the remaining acceptance item once this deploys.

Notes / follow-ups (out of scope here)

  • The generic "A database error occurred" masking lives in the shared dunite-core AppError and is a deliberate posture (don't leak DB internals in API responses); the real error is logged. Making the admin-facing seed failure message specific would be a dunite-side change.
  • seed::load is not transactional, so a mid-load failure leaves a partial seed. Worth wrapping in a single transaction, but it touches many repository signatures and is left as a separate change.
## PMS-709: Demo MSP seed load does nothing on staging ### Failure point (traced from the button) The admin "Load demo-msp" / "Load minimal" buttons (bunyip-web `/admin/seed`) POST to `/admin/seed/template` -> API `POST /v1/admin/seed/import?template=<name>` -> `crate::seed::load`. The load ran fine through groups, applications and users, then failed at the entitlements step: `EntitlementRepository::grant(pool, user.id, app.id, None, "seed")` inserts `source = 'seed'`, but the `application_entitlements_source_check` constraint (from migration 20260605000010) only permitted `'admin' | 'stripe' | 'backfill'`. Every insert hit a check-constraint violation (SQLSTATE 23514). Why it looked silent: the shared `AppError` `From<sqlx::Error>` logs the real error (constraint name and all) but collapses the API-facing message to the generic "A database error occurred", so the admin saw at most an unhelpful 500 and the specific cause was only in bunyip-api's logs. And because `seed::load` is not transactional, the 3 groups + 8 apps + 42 users written before the failing step persisted as a partial seed with no entitlements and no feedback. Reproduced against a migrated throwaway Postgres: `load()` returned `Err(Db(DatabaseError { message: "A database error occurred" }))` with `application_groups=3, applications=13, users=42, application_entitlements=0, feedback=0`. The direct insert surfaced the true error: `violates check constraint "application_entitlements_source_check"`. ### Fix - New migration `20260802000010_allow_seed_entitlement_source.sql` widens the constraint to `('admin','stripe','backfill','seed')`. Non-destructive: every existing row already carries a now-permitted value, so re-adding the constraint validates cleanly; the constraint name is preserved. - `entitlement_source::SEED` constant added so the loader stops passing a bare literal, matching the other call sites (`ADMIN`/`STRIPE`). - Env-gated regression test (`bunyip-api/tests/pms709_repro.rs`) that migrates a throwaway DB and asserts the whole documented demo-msp dataset loads (42 users, 8 apps, 3 groups, 6 entitlements, 7 feedback) with the 6 entitlements at `source='seed'`, plus idempotent re-load. ### Verification Against a fresh database (sqlx applies all migrations including the new one), `seed::load(demo-msp)` returns `LoadSummary { groups: 3, applications: 8, users: 42, entitlements: 6, feedback: 7 }`. `just` migration-immutability and migration-version checks, `cargo fmt --check`, and `clippy -D warnings` on the touched crates all pass. Not yet run against live staging; that verification is the remaining acceptance item once this deploys. ### Notes / follow-ups (out of scope here) - The generic "A database error occurred" masking lives in the shared `dunite-core` `AppError` and is a deliberate posture (don't leak DB internals in API responses); the real error is logged. Making the admin-facing seed failure message specific would be a dunite-side change. - `seed::load` is not transactional, so a mid-load failure leaves a partial seed. Worth wrapping in a single transaction, but it touches many repository signatures and is left as a separate change.
The demo-seed loader grants each demo user their entitlements with source 'seed', but the application_entitlements_source_check constraint (from 20260605000010) only permitted 'admin' | 'stripe' | 'backfill'. Every demo-msp / minimal "Load template" admin action therefore failed at the entitlements step with a check-constraint violation (SQLSTATE 23514), which the shared AppError collapses to the generic "A database error occurred" - so the button appeared to do nothing (the real error was only in bunyip-api's logs). Because the loader is not transactional, groups, applications and users had already been written, leaving a partial seed.

Widen the constraint to include 'seed' (new migration 20260802000010, non-destructive: every existing row already carries a now-permitted value) and add an entitlement_source::SEED constant so the loader stops using a bare literal. This is PMS-709.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9DhtRyWubFuzzKohE3JBt
test(seed): regression that demo-msp loads its full dataset (PMS-709)
All checks were successful
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 21s
Check / fmt + clippy + build + tests (pull_request) Successful in 13m20s
Create release / Create release from merged PR (pull_request) Has been skipped
76eb48d2bc
Env-gated DB test (skips without BUNYIP_TEST_DATABASE_URL, like the other DB suites) that migrates a throwaway database and runs the demo-msp template through seed::load, asserting the full documented dataset lands (42 users, 8 apps, 3 groups, 6 entitlements, 7 feedback) with the 6 entitlements carrying source='seed', and that a second load is idempotent. Fails on the pre-fix check-constraint violation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9DhtRyWubFuzzKohE3JBt
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-08-02 21:17:56 +02:00
longjacksonle deleted branch fix/PMS-709-seed-entitlement-source 2026-08-02 21:28:02 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!438
No description provided.