Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
# Add @suaveplan/api-plugin-payment — Hono plugin wrapping @suaveplan/payment + provider siblings
**Package**: `@suaveplan/api-plugin-payment` (NEW, server-tier)
**Status**: PROPOSED
**Type**: new-package
**Position**: builds on `@suaveplan/api`, `@suaveplan/api-plugin`, `@suaveplan/api-openapi`, `@suaveplan/api-plugin-auth`, `@suaveplan/payment`, optional `@suaveplan/idempotency`.
- **Spec delta**: ./specs/api-plugin-payment/spec.md
## Why
Genesis ships `@suaveplan/payment` (universal-tier `PaymentProvider` interface, `PaymentEngine`, `WebhookVerifier`, `Refund`, full lifecycle) and four concrete provider siblings (`@suaveplan/payment-provider-{stripe,adyen,paypal,square,mollie}`). What's missing is an HTTP layer that mounts the lifecycle as Zod-typed OpenAPI routes on top of `@suaveplan/api`, gates them behind `requireAuth()` from `@suaveplan/api-plugin-auth`, scopes intents per-user (anti-enumeration), enforces `Idempotency-Key` on mutation routes, verifies provider-specific webhook HMAC before deserializing payloads, and registers a typed `paymentService` slot every downstream consumer reads.
This package fills that gap as a `definePlugin` consumer. It is the canonical **payment** exemplar of the 3-way plug-and-play contract (Zod schemas at boundary + OpenAPI route registration + service-registry slot) every `@suaveplan/api-plugin-*` package follows. The plugin is provider-agnostic — the operator constructs the `PaymentProvider` (Stripe, Adyen, PayPal, Square, Mollie, or a custom impl) and injects it at factory time. No default provider is chosen by the plugin.
## What changes
Net-new package at `0.1.0`. ADDED:
### Plugin factory + descriptor surface
- `createPaymentPlugin(options: PaymentPluginOptions): PluginDescriptor<PaymentPluginConfig>` — Zod-validated factory returning a `PluginDescriptor` from `@suaveplan/api-plugin`. The descriptor implements the canonical 6-step `onInit`:
1. Resolve `app` + `openAPIRegistry` from `ServiceRegistry`.
2. Resolve the caller-supplied `PaymentProvider` from `options.provider` (no default — caller injects).
3. Construct a `PaymentService` wrapping `provider` + the `PaymentIntentStore` (in-memory default) so ownership scoping flows through every route.
4. Register `paymentService` in the host `ServiceRegistry` (typed slot) exposing the full lifecycle.
5. Register 6 OpenAPI routes via `definePluginRoutes` (mounted at `routePrefix`, default `/payments`) + 1 webhook receiver (`POST /payments/webhook`) mounted as a separate sub-router because webhook verification reads the raw request body BEFORE Zod parsing.
6. Log readiness via `ctx.logger.info`.
- `PaymentPluginOptions` (validated via `PaymentPluginOptionsSchema`): `{ provider: PaymentProvider; routePrefix?: string (default "/payments"); store?: PaymentIntentStore; webhookSecret: string; webhookToleranceMs?: number (default 300_000); webhookAlgorithm?: WebhookAlgorithm (default "SHA-256"); idempotencyStore?: IdempotencyStore; logger?: LoggerLike; clock?: Clock }`.
### `PaymentService` (typed ServiceRegistry slot)
- `createPaymentService({ provider, store, clock })` returns `PaymentService` exposing the full lifecycle: `createIntent`, `confirmIntent`, `captureIntent`, `cancelIntent`, `refund`, `getIntent`, `listIntents`. Every method is **owner-scoped** — it accepts a `Principal.id` (and optional `tenantId`) and rejects access to foreign intents with a 404 (anti-enumeration). The service owns the `PaymentIntentStore` write-side so provider-side IDs map back to owner ids.
### `PaymentIntentStore`
- `PaymentIntentStore` interface — record-side persistence mapping a provider-issued `PaymentIntent.id` back to its `ownerId` + optional `tenantId`. `MemoryPaymentIntentStore` is the in-process default; operators inject any other impl that satisfies the contract (SurrealDB, Postgres, Redis).
### Routes (Zod-typed OpenAPI surface)
All mutation routes are `requireAuth()`-gated and scope to `c.get("user").id`:
- `POST /payments/intents` — body: `CreatePaymentIntentBody` (Money via `MoneySchema`); response: `PaymentIntentResponse`. Honours `Idempotency-Key` header.
- `GET /payments/intents` — query: `ListPaymentIntentsQuery` (limit/cursor/status filter); response: `PaymentIntentListResponse` (only intents owned by the requester).
- `GET /payments/intents/{id}` — params: `PaymentIntentIdParam`; response: `PaymentIntentResponse`. Foreign intent → 404 anti-enumeration.
- `POST /payments/intents/{id}/confirm` — empty body; response: `PaymentIntentResponse`.
- `POST /payments/intents/{id}/capture` — body: `CaptureBody` (optional partial amount); response: `PaymentIntentResponse`.
- `POST /payments/intents/{id}/cancel` — empty body; response: `PaymentIntentResponse`.
- `POST /payments/intents/{id}/refund` — body: `RefundBody`; response: `RefundResponse`. Honours `Idempotency-Key` header.
Plus the unauthenticated webhook receiver:
- `POST /payments/webhook` — verifies the provider-specific HMAC (`X-Signature` / `X-Timestamp`) against `options.webhookSecret` BEFORE deserialising the body, replays through `EventBus` when `api-plugin-events` is mounted, otherwise applies updates via the `paymentService` directly.
### Zod schemas (boundary validation) — exposed at `./schemas/*`
- `./schemas/money` — `MoneySchema` (`{ amount: z.coerce.bigint(), currency: z.string().min(3).max(8) }`).
- `./schemas/options` — `PaymentPluginOptionsSchema`.
- `./schemas/routes` — request/response per route (`CreatePaymentIntentBodySchema`, `CaptureBodySchema`, `RefundBodySchema`, `PaymentIntentIdParamSchema`, `ListPaymentIntentsQuerySchema`, `PaymentIntentResponseSchema`, `RefundResponseSchema`, `PaymentIntentListResponseSchema`, `WebhookResponseSchema`).
### Errors + observability
- `PaymentPluginError extends SuaveplanError<PaymentPluginErrorCode>` via `defineCodes("PAYMENT_PLUGIN", [...])`. **12 codes**: `INVALID_INPUT`, `NOT_FOUND`, `FORBIDDEN`, `PROVIDER_ERROR`, `WEBHOOK_SIGNATURE_INVALID`, `WEBHOOK_TIMESTAMP_INVALID`, `IDEMPOTENCY_CONFLICT`, `INVALID_STATE`, `AMOUNT_MISMATCH`, `CURRENCY_MISMATCH`, `REFUND_EXCEEDS_CAPTURED`, `PROVIDER_NOT_CONFIGURED`, `INTENT_CREATION_FAILED`, `SERVICE_NOT_AVAILABLE`, `ROUTE_PREFIX_COLLISION`.
- OpenTelemetry: 5 counters and 1 histogram —
- `payments.intents_created_total{outcome,provider}`
- `payments.captures_total{outcome,provider}`
- `payments.refunds_total{outcome,provider}`
- `payments.webhook_received_total{outcome,provider}`
- `payments.webhook_signature_failures_total{provider}`
- `payments.intent_duration_ms{provider}` (histogram)
### Subpath exports
`.`, `./plugin/plugin`, `./middleware/idempotency-middleware`, `./services/payment-service`, `./store/memory-store`, `./store/payment-intent-store`, `./webhook/verify-webhook`, `./schemas/money`, `./schemas/options`, `./schemas/routes`, `./routes/build-routes`, `./otel/instruments`, `./error-codes/error-codes`, `./types/types`.
## Out of scope
- Front-end UI (checkout form, 3-D Secure challenge UI, dispute evidence upload). Backend only.
- Per-tenant provider routing (multi-provider `ProviderRouter`) — single injected provider for now; multi-provider routing is a follow-up `api-plugin-payment-router`.
- Saved payment methods / SetupIntents — covered by a follow-up `api-plugin-payment-methods`.
- Subscription billing — covered by `api-plugin-subscription`.
- Dispute workflow endpoints — covered by a follow-up `api-plugin-payment-disputes`.
- Reconciliation export endpoints — covered by `api-plugin-export` + `@suaveplan/payment`'s `PaymentReconciliation` directly.
## Source mapping
This package is **genesis-original** with design intent drawn from:
- The genesis `@suaveplan/api-plugin` `PluginContext` + `ServiceRegistry`.
- The genesis `@suaveplan/api-openapi` `definePluginRoutes` + `RouteBuilder`.
- The genesis `@suaveplan/api-plugin-auth` plug-and-play exemplar (3-step lifecycle + ContextVariableMap augmentation).
- The legacy `enterprise/api-plugin-payment.md` enterprise plan (canonical source) — section 1 (Purpose), section 3 (Required improvements), section 4 (Feature-rich enhancements 1, 9).
- `@suaveplan/payment` — `PaymentProvider` interface, `PaymentIntent`, `Refund`, `VALID_TRANSITIONS`, `WebhookVerifier`.
- `@suaveplan/idempotency` — `IdempotencyGuard`, `MemoryIdempotencyStore` for the `Idempotency-Key` middleware.
| Requirement | Source |
|---|---|
| `definePlugin` consumer shape | `@suaveplan/api-plugin` |
| `requireAuth()` gating, `c.get("user")` typed Principal | `@suaveplan/api-plugin-auth` |
| `definePluginRoutes` route registration | `@suaveplan/api-openapi` |
| `PaymentProvider` interface, `PaymentIntent`, `Refund` | `@suaveplan/payment/types/payment` |
| Webhook HMAC verification + timestamp replay protection | `@suaveplan/payment/webhook/webhook` |
| `Idempotency-Key` middleware | `@suaveplan/idempotency` |
| Owner-scoped lookups + 404 anti-enumeration | legacy `enterprise/api-plugin-payment.md` §1 + §3 ("PCI posture") + genesis principle |
| 12 `PAYMENT_PLUGIN_*` codes | legacy `enterprise/api-plugin-payment.md` §3 ("Typed errors") |
| 5 counters + 1 histogram | legacy `enterprise/api-plugin-payment.md` §3 ("Observability") |
| 3-way plug-and-play | genesis architectural principle (verbatim from `add-api-plugin-auth`) |
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/api-plugin-payment` to enumerate. Net-new package; no dependents today.
- No dependents (verified pre-shipment — net-new package; future consumers will be `api-plugin-payment-router` / `api-plugin-payment-methods` / `api-plugin-payment-disputes`).
### Catalog additions required
None — `hono`, `@hono/zod-openapi`, `zod`, `tslib`, `@opentelemetry/api`, `@vitest/coverage-istanbul`, `vite`, `vite-plugin-dts`, `vitest`, `fast-check` are all already pinned in the root catalog.