# 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.
