Task: Production-Shaped Inbound Subscription Billing Webhook Stub (Hard)
Overview
Subscriptions are manually toggled. Build a Stripe-style inbound webhook pipeline stub: signature verification, idempotent event store, transactional state transitions, dead-letter/retry metadata, and audit — not a single controller if-statement.
Scope (must all ship)
Endpoint
POST /api/v1/webhooks/billing — raw body preserved for HMAC (BILLING_WEBHOOK_SECRET)
- Dev mode: secret optional with loud warning log; prod: secret required or boot fails / reject all
Event store
- Entity
BillingWebhookEvent: eventId (unique), type, payload, status (processed|ignored|failed), error, receivedAt, processedAt
- Idempotency: duplicate
eventId → 200 no-op with same outcome recorded
Supported events (minimum)
subscription.activated — create or reactivate local Subscription; map external customer/sub IDs
subscription.cancelled — cancel with cancelledAt; retain history
payment.failed — set past_due (or equivalent) without deleting
- Unknown type → store as
ignored, 200 (do not 500)
Processing
- Verify signature before parse side effects
- DB transaction: insert event + apply subscription mutation atomically where possible; on conflict of eventId short-circuit
- Map external IDs via columns or side table
BillingCustomerMap (document choice)
- Structured audit log entries for each transition
Ops
- Admin read-only list stub optional:
GET /api/v1/admin/billing/webhook-events (RBAC)
- Metrics/log fields: processing latency, failure reason codes
Security
- Reject body > max bytes
- Do not echo secret; do not store full signature header unnecessarily
- Rate-limit webhook route separately if global auth rate limit would block provider retries
Testing (≥12)
- Valid activate / cancel / payment.failed
- Replay idempotency
- Invalid signature →
401 when secret set
- Unknown event ignored
- Concurrent duplicate delivery (unique constraint)
- Mapping missing external id →
failed with actionable error, not silent success
- Transaction rollback on subscription update failure leaves consistent event status
Deliverables
- Webhook module, entities/migrations, signature helper, admin list (if included)
.env.example + docs/billing-webhooks.md
- ≥12 tests
Acceptance Criteria
Out of scope
- Real Stripe SDK / live money movement
- Dunning emails (may emit domain event only)
Task: Production-Shaped Inbound Subscription Billing Webhook Stub (Hard)
Overview
Subscriptions are manually toggled. Build a Stripe-style inbound webhook pipeline stub: signature verification, idempotent event store, transactional state transitions, dead-letter/retry metadata, and audit — not a single controller if-statement.
Scope (must all ship)
Endpoint
POST /api/v1/webhooks/billing— raw body preserved for HMAC (BILLING_WEBHOOK_SECRET)Event store
BillingWebhookEvent:eventId(unique),type,payload,status(processed|ignored|failed),error,receivedAt,processedAteventId→200no-op with same outcome recordedSupported events (minimum)
subscription.activated— create or reactivate localSubscription; map external customer/sub IDssubscription.cancelled— cancel withcancelledAt; retain historypayment.failed— setpast_due(or equivalent) without deletingignored,200(do not 500)Processing
BillingCustomerMap(document choice)Ops
GET /api/v1/admin/billing/webhook-events(RBAC)Security
Testing (≥12)
401when secret setfailedwith actionable error, not silent successDeliverables
.env.example+docs/billing-webhooks.mdAcceptance Criteria
past_duedoes not delete subscriptionOut of scope