# Architect — F-023 Payments: provider interface + Stripe + webhooks ## Feature F-023 introduces real payment processing without coupling domain to Stripe. ## Design ### Module boundaries Create `project/src/modules/payments/` with domain/application/infrastructure/api/tests. Payments owns `payments_transactions`; domain code never imports the Stripe SDK. ### Data model Add migration `017_payments.js`: - `payments_transactions`: id, provider text, provider_event_id text unique per provider, provider_payment_id text, order_id uuid, amount_cents integer, currency text, status text, raw jsonb, created_at timestamptz. - CHECK `status IN ('requires_payment','succeeded','failed','refunded','chargeback')`. - UNIQUE `(provider, provider_event_id)` for idempotency. ### Payment provider interface - `PaymentProvider.createIntent(input)` and `PaymentProvider.verifyWebhook(rawBody, signature)` returning a typed `PaymentEvent` or throwing. - Domain never touches Stripe SDK; only the Stripe adapter does. ### Webhook flow - `POST /payments/webhook` reads raw body + signature header. - Verifies signature with `STRIPE_WEBHOOK_SECRET` (or env-derived) — invalid -> 400. - Looks up `(provider, provider_event_id)` in transactions; if seen, return 200 without re-processing (idempotent). - Maps Stripe events to `PaymentEvent` (`PaymentSucceeded`, `PaymentFailed`, `PaymentRefunded`, `ChargebackCreated`) and processes them: persist transaction, call `OrderService.transition` and emit event through `OrderEventPublisher`. ### API - `POST /payments/webhook` public (signature-protected). - Admin/dev `GET /payments/transactions/:orderId` for inspection. ### Domain events - Emit `OrderPaid`/`OrderCancelled`/`OrderRefunded` via `OrderEventPublisher` already exposed by orders. ## Acceptance trace - Domain code contains zero Stripe SDK imports. - Bad signature -> 400, no DB row. - Replayed event -> single processed transaction. - PaymentSucceeded -> order state PAID, event emitted.