- migration 037: offerings.price_amount/price_currency, offering_registrations.reservation_expires_at,
payment_transactions, payment_webhook_events (webhook_event_id as real PK — the idempotency mechanism)
- payment-provider.js: StripeProvider (real SDK, HMAC webhook verification) + TestProvider
(deterministic, in-process, no network) behind one interface, selected via PAYMENT_PROVIDER
- commerce.js: createCheckoutSession (server-side price, never caller-supplied), processWebhookEvent
(idempotent), handlePaymentSuccess/Failure/CheckoutAbandoned/Refund, reservation reaper support
- offerings.js: computeReservationExpiry, updateOffering (allowlisted PATCH), readiness gained
price/payment_provider checks, convertRegistrationToEnrollment now fires the activation email
every other enrollment path already triggers, and links the registration's guardian_id into
student_guardians on conversion (real bug: a converted minor had no guardian link, so the new
activation email had nowhere to resolve a recipient to)
- server.js: raw-body middleware scoped to /api/webhooks/payment, reservation-reaper poller
Was generated locally during Phase 3A work but never committed. Verified it corresponds
to the current package.json via a clean npm ci (118 packages, no resolution changes).