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:
-
Anonymous bids were allowed but don't make domain sense. An ad
without an advertiser is orphaned data — there is no entity to own the
X-Api-Key, no profile to enrich, no spend to aggregate. Allowing anonymous bids means the invariant “every ad belongs to exactly one advertiser” isn't enforced at the API boundary. - A single fixture can't show contrast. A developer opening the catalog sees one entry. It can't simultaneously demonstrate the minimal viable bid and the full enrichment surface. Both are important: the minimal fixture confirms the floor; the maximal fixture confirms the ceiling.
-
Webhook settlement was skipped. The seed placed a bid and moved
on regardless of whether the Stripe webhook (
POST /webhooks/stripe) had confirmed the payment. This left a window where the catalog showed a bid whose DynamoDB status was still pending. -
No assertion that the environment reached expected state. After
task devseeded data, there was no check that the API actually returns the expected catalog shape. Drift between seed intent and observed API output was only discovered manually.
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:
| Step | Endpoint | Persona | Purpose |
|---|---|---|---|
| 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.mjsmust 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
| Option | Why 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 |