# Add @suaveplan/payment-providers — universal-tier provider plugins (Stripe / PayPal / Square / Mollie / Adyen) **Package**: `@suaveplan/payment-providers` **Status**: PROPOSED **Type**: migration (upstream `../suaveplan/packages/universal/payment-providers`) - **Spec delta**: ./specs/payment-providers/spec.md ## Why `STATUS.md` line 133 lists `@suaveplan/payment-providers` as Pending. The upstream package supplies the five reference adapters that implement the `@suaveplan/payment` `PaymentProvider` interface — Stripe, PayPal, Square, Mollie, and Adyen. With `@suaveplan/payment` shipped in the same wave, every commerce-tier downstream (`@suaveplan/api-plugin-payment`, `@suaveplan/booking`, `@suaveplan/api-plugin-banking`) needs at least one of these adapters to deliver an end-to-end payment lifecycle. The migration preserves the entire upstream public surface verbatim (`StripeProvider`, `PayPalProvider`, `SquareProvider`, `MollieProvider`, `AdyenProvider`, `httpRequest`, `ApiKeyConfig`, `OAuthConfig`, `MerchantKeyConfig`, `ProviderResponse`, `HttpOptions`) while upgrading five foundational concerns to the genesis quality bar: 1. **Injectable `FetchLike`** — every provider accepts an optional `fetch` config field defaulting to `globalThis.fetch`. Tests use a real recording `FetchLike` rather than `vi.spyOn(globalThis, "fetch")`, so the no-mocks rule (CLAUDE.md §2) is preserved without losing coverage. 2. **Injectable `now` / `randomId`** — `PayPalProvider`'s token-cache clock and `SquareProvider` / `AdyenProvider`'s default idempotency-key generator are config fields. This removes module-scope `Date.now()` and `crypto.randomUUID()`, aligning with the deterministic universal-tier rule. 3. **Injectable `base64Encode`** — PayPal's OAuth Basic header uses `btoa` by default but accepts an injected encoder so the package runs on every runtime that names Base-64 differently. 4. **Typed `PaymentProviderError`** extending `SuaveplanError` replaces the upstream `throw new Error(...)` raised inside `httpRequest`. The `PAYMENT_PROVIDER_CODES` registry is declared via `defineCodes("PAYMENT_PROVIDER", [...])` (4 codes: `HTTP_ERROR`, `TIMEOUT`, `PARSE_ERROR`, `OAUTH_TOKEN_FAILURE`) and every former `Error` boundary upgrades to a typed factory carrying the HTTP status, response body excerpt, and provider name as discriminants. 5. **Folder layout** — the upstream flat `src/{stripe,paypal,...}.ts` becomes `src/providers/{stripe,paypal,square,mollie,adyen}/.ts` per genesis convention (one feature module per folder, sibling `.md`, tests, types, barrel). The folder-naming rule §10 is respected by naming children only with the provider name (no `-provider` suffix). `httpRequest` moves to `src/http/http.ts` and its config types to `src/types/provider.ts` so consumers can tree-shake. The 8 upstream source files expand to 15 (5 provider impls × {`.ts`, `.md`, `.test.ts`, `index.ts`} per folder + flat `http/`, `types/`, `error-codes/` modules). One sibling `.md` per implementation file lands at genesis quality (≥200 words, basic + advanced examples, full API reference). ## What changes - ADDED: `StripeProvider` — Stripe REST API adapter, Bearer auth, manual - automatic capture, idempotency keys, status + refund-reason mapping. - ADDED: `PayPalProvider` — PayPal Orders v2 adapter, OAuth2 client- credentials flow with token cache, injectable `now` for cache TTL, injectable `base64Encode` for the Basic auth header. - ADDED: `SquareProvider` — Square Payments adapter, Bearer auth, manual + automatic capture, `Square-Version` header, injectable `randomId` for default idempotency keys. - ADDED: `MollieProvider` — Mollie Payments v2 adapter, Bearer auth, string-based amount marshalling (`"50.00"`), redirect URL handling. - ADDED: `AdyenProvider` — Adyen Checkout v71 adapter, `X-API-Key` header, merchant-account context, injectable `randomId`. - ADDED: `httpRequest` — universal `FetchLike` wrapper with timeout via `AbortSignal`, JSON serialisation, typed errors. - ADDED: `PAYMENT_PROVIDER_CODES` registry via `defineCodes("PAYMENT_PROVIDER", [...])` — 4 codes (`HTTP_ERROR`, `TIMEOUT`, `PARSE_ERROR`, `OAUTH_TOKEN_FAILURE`). - ADDED: `PaymentProviderError extends SuaveplanError` carrying optional `provider`, `httpStatus`, `bodyExcerpt`, and `url` discriminants; factories `httpError`, `timeoutError`, `parseError`, `oauthTokenFailure`. - ADDED: `FetchLike` type alias re-exported from `@suaveplan/payment-providers/http/http`. - ADDED: Subpath exports per public module — 9 entries covering every feature; tree-shakeable. ## Out of scope - **`@suaveplan/idempotency` integration.** Providers accept `idempotencyKey` and forward it to the provider; auto-enforcement via the `@suaveplan/idempotency` middleware is a follow-up change once that package lands. - **OTel spans / counters.** The enterprise audit calls for per-provider observability; the genesis `@suaveplan/payment` migration also deferred this. Wiring spans here without spans in the engine creates asymmetry. Defer until both packages land OTel together. - **Webhook signature verification.** Each provider supports webhook signing (Stripe HMAC, Adyen HMAC, PayPal cert chain, etc.). The upstream package accepts `webhookSecret` / `webhookHmacKey` config fields but does NOT implement verification — that lives in `@suaveplan/payment`'s `WebhookVerifier`. The config fields are preserved as opaque pass-throughs so future verifier integration has the secret material it needs. - **Schema-registry integration.** The `PaymentProvider` interface is validated at the type level; runtime Zod schemas around provider inputs / outputs are a `@suaveplan/payment` concern. - **Adyen notification HMAC.** The `webhookHmacKey` config field is preserved verbatim from upstream but the matching HMAC verifier ships with `@suaveplan/payment`'s `WebhookVerifier`, not here. ## Source mapping This change derives from `../suaveplan/enterprise/payment-providers.md`. | Requirement | Source lines | Notes | |---|---|---| | Universal-tier purity (no Node / browser / React) | L37 | verbatim | | Structured errors extending `@suaveplan/error` | L42, L61 | verbatim | | 100% test coverage / Istanbul | L50 | verbatim | | 100% docs coverage with sibling `.md` per symbol | L51 | verbatim | | File-size budget 250 soft / 300 hard / 500 test | L52 | verbatim | | No mocks — real services / injectable seams | L53 | verbatim | | No re-exports of external deps | L54 | verbatim | | `sideEffects: false` + tree-shake friendly | L55 | verbatim | | `workspace:^` internal / catalog external deps | L56 | verbatim | | Biome clean | L58 | verbatim | | TypeScript strict; no `any` / `ts-ignore` | L59 | verbatim | | Five providers implementing `PaymentProvider` (Stripe, PayPal, Square, Mollie, Adyen) | L10-L13 (Purpose), upstream `src/index.ts` L1-L7 | verbatim — preserve full public surface | | Cancellation via `AbortSignal` | L44 | verbatim | | Deterministic clock / RNG | L41 | enhancement above upstream — providers gain injectable `now` / `randomId` / `base64Encode` | ### Requirements with no source line | Requirement | Justification | |---|---| | Injectable `FetchLike` per provider | Required to satisfy `CLAUDE.md` rule 2 ("NEVER use mocks") while preserving 100% coverage. Upstream tests `vi.spyOn(globalThis, "fetch")`; genesis cannot. | | Folder-per-provider with sibling `.md` | Required by `CLAUDE.md` "Package Structure (library packages)" — five files per feature module. Upstream uses a flat `src/.ts` layout. | | `PAYMENT_PROVIDER_CODES` registry (4 codes) | Required by `CLAUDE.md` rule 8 + enterprise §3 "Structured errors". Upstream `httpRequest` throws raw `Error`; genesis upgrades to typed. | | Subpath exports per provider | Required by `openspec/AGENTS.md` §6 — every feature module owns an `exports` entry for tree-shaking. | ## Verification approach Every requirement in the spec delta is verified by tests in the package's `src//.test.ts` files plus a cross-cutting tier-purity grep test in `tests/dep-graph.test.ts`. Each provider's test file uses a real `FetchLike` (recording adapter) — never `vi.spyOn` / `vi.mock` — and walks every method (`createIntent`, `confirmIntent`, `captureIntent`, `cancelIntent`, `refund`, `getIntent`) plus the full status-mapping matrix. - `src/http/http.test.ts` — covers `httpRequest`, timeout, error wrapping, typed `PaymentProviderError` - `src/error-codes/error-codes.test.ts` — every code reachable, cause propagation, discriminants - `src/providers/stripe/stripe.test.ts` — `StripeProvider` full surface + status / refund-reason mapping - `src/providers/paypal/paypal.test.ts` — `PayPalProvider` full surface + OAuth token cache + refresh - `src/providers/square/square.test.ts` — `SquareProvider` full surface + `randomId` injection - `src/providers/mollie/mollie.test.ts` — `MollieProvider` full surface + amount marshalling - `src/providers/adyen/adyen.test.ts` — `AdyenProvider` full surface + merchant-account context - `tests/dep-graph.test.ts` — universal-tier purity grep (no `node:*` / `bun:*` / `fs` / `path` imports) ## Migration impact None — additive only. No genesis consumer imports `@suaveplan/payment-providers` yet. Future commerce-tier consumers (`@suaveplan/api-plugin-payment`, `@suaveplan/booking`) will import these providers by their public class names — every upstream export is preserved. ## Impact ### Affected dependents Run `bunx turbo ls --filter=...@suaveplan/payment-providers` to enumerate. No dependents on the genesis tree yet (only `@suaveplan/payment` is published; downstream consumers are still pending). The change is additive. - No dependents (verified by `bunx turbo ls --filter=...@suaveplan/payment-providers` — empty result). ## Parity Checklist | Capability | Upstream | Migrated | Notes | |---|---|---|---| | `StripeProvider` (createIntent, confirmIntent, captureIntent, cancelIntent, refund, getIntent) | yes | yes | full surface preserved | | `PayPalProvider` + OAuth token cache | yes | yes | now injectable `now()` for TTL + `base64Encode` for Basic header | | `SquareProvider` | yes | yes | now injectable `randomId` for default idempotency keys | | `MollieProvider` | yes | yes | full surface preserved | | `AdyenProvider` + merchant-account context | yes | yes | now injectable `randomId` | | `httpRequest` helper | yes | yes | now raises typed `PaymentProviderError` instead of raw `Error`; accepts injectable `FetchLike` | | `ApiKeyConfig` / `OAuthConfig` / `MerchantKeyConfig` / `ProviderResponse` / `HttpOptions` types | yes | yes | full surface preserved + added `fetch`, `now`, `randomId`, `base64Encode` optional fields | | `vi.spyOn(globalThis, "fetch")` in tests | yes | replaced | uses real `FetchLike` recorder per CLAUDE.md rule 2 | | Webhook HMAC verifier | scoped out | scoped out | upstream defers to `@suaveplan/payment` `WebhookVerifier`; genesis preserves the config fields verbatim | | OTel spans / counters | absent | absent | deferred per `## Out of scope` to align with `@suaveplan/payment` | | `@suaveplan/idempotency` middleware | absent | absent | deferred — pass-through preserved |