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