ADR-012 Accepted 2026-05-24

Dev Fixture Personas & Environment Verification

The dev seed produces two named fixture personas — minimal-advertiser and maximal-advertiser — via the real MPP 402 flow, including Stripe webhook confirmation. After seeding, task dev appends a dev-environment assertion test that verifies the known state against the live REST API. advertiser_slug is required on all bids.

Context

The original seed created a single demo-advertiser entry at $2.00 with a handful of profile fields. Several gaps emerged as the surface area grew:

Decision

1. advertiser_slug is required on all bids

POST /ads/bid requires advertiser_slug in the request body. Anonymous bids are rejected with 422. The OpenAPI spec, JSON schema (bid-request.schema.json), and place-bid Lambda all enforce this.

2. Two fixture personas replace demo-advertiser

The seed script produces exactly two named entries on every cold start. Their slugs and test URLs are stable so assertions can be written against them by name.

Property minimal-advertiser maximal-advertiser
Slug minimal-advertiser maximal-advertiser
URL https://minimal.madcatalog.example.com https://maximal.madcatalog.example.com
Bids 1 × $1.00 3 × ($10.00, $5.00, $5.00) — $20.00 total
Ad enrichment fields Required only (title, url, description) All optional fields: logo_url, brand_color, tagline, key_stat, cta_label, docs_url, hero_url
Advertiser PATCH None — profile auto-created with slug only All fields: name, tagline, logo_url, brand_color, key_stat, cta_label, docs_url
Expected rank 2 (lower spend) 1 (highest spend)
3. Seed waits for Stripe webhook settlement per bid

After each bid's 200 response, the seed polls GET /ads/{ad_id}/bids until the latest bid shows a settled status, or times out after 30 seconds. This ensures the Stripe CLI (task stripe:listen) has forwarded the payment_intent.succeeded event and the DynamoDB record reflects confirmed settlement before the next step runs.

4. task dev appends dev-environment assertions

After the seed completes, task dev runs task dev:env-test (node tests/dev-env.mjs). The test file makes assertions against the live API using the known fixture state as ground truth. On failure, the shell pane shows the assertion output and exits non-zero, making the breakage immediately visible.

Endpoints Exercised

The full seed-and-verify sequence touches the following endpoints:

StepEndpointPersonaPurpose
Probe GET /catalog both Idempotency check — skip if already seeded
1a POST /ads/bid (no auth) minimal Receive 402 challenge; required fields only
1b POST /payments/stripe/spt minimal Create SPT using challenge amount/currency
1c POST /ads/bid + Authorization: Payment minimal Claim slug, receive advertiser_api_key, place bid
1d GET /ads/{id}/bids (poll) minimal Wait for webhook-confirmed settlement
2a–2c Same as 1a–1c + all ad enrichment fields maximal (bid 1, $10.00) Claim slug, all optional ad fields in bid body
2d GET /ads/{id}/bids (poll) maximal Wait for settlement
2e PATCH /advertisers/maximal-advertiser maximal Enrich with all advertiser profile fields
3a–3d POST /ads/bid ×2 (with X-Advertiser-Api-Key) maximal (bids 2+3) Incremental spend top-ups; builds bid history
3e GET /ads/{id}/rank-status (MPP 402 flow) maximal Demonstrate MPP-gated rank polling; confirms rank 1
Assert 1 GET /catalog — Verify 2 ads, correct rank order
Assert 2 GET /advertisers — Verify 2 advertisers, correct spend totals
Assert 3 GET /advertisers/minimal-advertiser — Verify slug exists, no enrichment fields populated
Assert 4 GET /advertisers/maximal-advertiser — Verify all enrichment fields present and non-empty
Assert 5 GET /ads/{maximal_id}/bids — Verify 3 bids, all settled
Assert 6 GET /ads/{maximal_id}/rank-status — Verify rank 1, cents_to_rank_1 = 0

Consequences

Positive
  • Every ad in the catalog is attributable to an advertiser — the invariant is enforced at the API boundary
  • Catalog always shows minimal vs. maximal contrast; both enrichment extremes are visible
  • All advertiser enrichment fields are exercised on every seed, preventing silent field rot
  • Bid history is populated (3 bids on maximal) so bid history and timeline endpoints always have data
  • Webhook settlement confirmation means the catalog state reflects confirmed payment, not optimistic placement
  • Dev-env assertions catch drift between seed intent and API output automatically
Negative
  • Seed is slower — webhook wait adds latency per bid; full seed takes longer with 4 bids total
  • Stripe CLI (task stripe:listen) must be running or webhook wait times out
  • Existing anonymous-bid tests in api-smoke.mjs must be updated to include a slug
  • Idempotency detection must recognize both personas, not just one URL
  • Any test that placed anonymous bids (no advertiser_slug) is now invalid at the domain level

Alternatives Considered

OptionWhy not chosen
Keep single demo-advertiser Can't simultaneously represent minimal and maximal states; developers have to mentally reconstruct what a fully enriched entry would look like
Make advertiser_slug optional, validate in env tests only Pollutes the data model with logically orphaned records; makes the invariant a soft convention rather than an enforced contract
Skip webhook wait (accept optimistic settlement) Leaves the catalog in a state where spend is recorded but Stripe confirmation is pending; assertions on bid status would be non-deterministic
New tmux pane for env-test output Adds pane complexity; the dev shell pane already captures output from task dev sequence; failure is equally visible as a non-zero exit there