1.9 KiB
1.9 KiB
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)andPaymentProvider.verifyWebhook(rawBody, signature)returning a typedPaymentEventor throwing.- Domain never touches Stripe SDK; only the Stripe adapter does.
Webhook flow
POST /payments/webhookreads 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, callOrderService.transitionand emit event throughOrderEventPublisher.
API
POST /payments/webhookpublic (signature-protected).- Admin/dev
GET /payments/transactions/:orderIdfor inspection.
Domain events
- Emit
OrderPaid/OrderCancelled/OrderRefundedviaOrderEventPublisheralready 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.