# Slim @suaveplan/payment-providers — extract concrete provider modules into sibling packages **Package**: `@suaveplan/payment-providers` **Status**: PROPOSED **Type**: refactor (modification of the just-shipped migration `2026-05-17-add-payment-providers`) - **Design**: ./design.md - **Spec delta**: ./specs/payment-providers/spec.md ## Why The just-shipped migration (`openspec/archive/2026-05-17-add-payment-providers/`) folded five concrete provider adapters (Stripe, PayPal, Square, Mollie, Adyen) into a single `@suaveplan/payment-providers` package. That bundle wired the shared shape (`PaymentProviderError`, `httpRequest`, `FetchLike`, `ApiKeyConfig`, `OAuthConfig`, `MerchantKeyConfig`) and the five adapter classes into one publish unit. Consumers that only need Stripe pay for PayPal's OAuth machinery, Adyen's merchant-context wiring, and three more provider trees they never call. `@suaveplan/payment` already depends on this package transitively for its `PaymentProvider` interface; growing it with every new provider compounds the problem. The genesis pattern for foundation-with-sibling-plugins (see `@suaveplan/network` + `network-plugin-*`, `@suaveplan/drive` + `drive-plugin-*`) is to ship the core abstraction + registry in the foundation package and the concrete plugins in sibling packages. This change retrofits `@suaveplan/payment-providers` to match that pattern: 1. **Slim the core** — `@suaveplan/payment-providers` keeps only the shared types (`ApiKeyConfig`, `OAuthConfig`, `MerchantKeyConfig`, `HttpOptions`, `ProviderResponse`, `ProviderCommonOptions`, `NowFn`, `RandomIdFn`, `Base64EncodeFn`, `FetchLike`), the `httpRequest` helper, the `PaymentProviderError` + `PAYMENT_PROVIDER_CODES` registry, plus the new `ProviderRegistry` + `definePaymentProvider` factory. 2. **Add `ProviderRegistry`** — a thin in-memory registry that stores `PaymentProvider` instances by name with `register`/`get`/`has`/`list`/ `unregister`, raising `PaymentProviderError(PROVIDER_NOT_REGISTERED)` on unknown lookups. 3. **Add `definePaymentProvider`** — a factory helper that returns a `PaymentProvider`-shaped object with a stable identity so sibling provider packages can build adapters without duplicating boilerplate. 4. **Remove the five concrete provider trees** (Stripe / PayPal / Square / Mollie / Adyen). They re-land as 5 sibling packages (`@suaveplan/payment-provider-stripe`, `-paypal`, `-square`, `-mollie`, `-adyen`) in a follow-up wave, each depending on this slimmed core. ## What changes ### REMOVED - `StripeProvider` (entire `src/providers/stripe/` tree, including tests + `.md`). Re-lands as `@suaveplan/payment-provider-stripe.StripeProvider`. - `PayPalProvider` (entire `src/providers/paypal/` tree). Re-lands as `@suaveplan/payment-provider-paypal.PayPalProvider`. - `SquareProvider` (entire `src/providers/square/` tree). Re-lands as `@suaveplan/payment-provider-square.SquareProvider`. - `MollieProvider` (entire `src/providers/mollie/` tree). Re-lands as `@suaveplan/payment-provider-mollie.MollieProvider`. - `AdyenProvider` (entire `src/providers/adyen/` tree). Re-lands as `@suaveplan/payment-provider-adyen.AdyenProvider`. - Subpath exports `./providers/{stripe,paypal,square,mollie,adyen}/*` from `package.json#exports`. - `SquareConfig` type alias (Square-specific extension of `ApiKeyConfig` with `randomId`). The sibling package re-declares it locally — the core ships only provider-agnostic config shapes. ### ADDED - `ProviderRegistry` class with `register(provider)`, `get(name)`, `has(name)`, `list()`, `unregister(name)`, and `size`. Lookup miss throws `PaymentProviderError(PROVIDER_NOT_REGISTERED)` from `get`; duplicate registration throws `PROVIDER_ALREADY_REGISTERED`. - `definePaymentProvider(spec)` factory returning a `PaymentProvider & { readonly name: TName }`. Preserves caller-supplied method identities; freezes the resulting object so sibling packages can hand the value straight to `registry.register`. - `PROVIDER_NOT_REGISTERED` and `PROVIDER_ALREADY_REGISTERED` codes added to `PAYMENT_PROVIDER_CODES`. - `composeAbortSignal(internal, external)` extracted from `httpRequest` and re-exported as a public surface so sibling provider packages can compose their own signals without re-implementing the helper. ### MODIFIED - `package.json#exports` shrinks from 9 entries (5 providers + 4 core) to 5 entries (`.`, `./types/provider`, `./error-codes/error-codes`, `./http/http`, `./registry/registry`). - `README.md` rewritten to describe the slim core + sibling package ecosystem instead of the 5-adapter bundle. - `package.json#description` rewritten to reflect the slim shape. - `vite.config.ts#rollupOptions.external` stays unchanged (`@suaveplan/error`, `@suaveplan/payment`, `tslib`); no new externals are introduced. ## Migration Consumers that imported a concrete provider from `@suaveplan/payment-providers` MUST switch to the matching sibling package. The provider class names, constructors, and `PaymentProvider` interface contract are preserved verbatim — only the package name changes. Search command: ```bash grep -rln "from ['\"]@suaveplan/payment-providers['\"]" packages/ \ | xargs grep -l "StripeProvider\|PayPalProvider\|SquareProvider\|MollieProvider\|AdyenProvider" ``` Replacement table: | Old (pre-slim) | New (post-slim) | |---|---| | `import { StripeProvider } from "@suaveplan/payment-providers"` | `import { StripeProvider } from "@suaveplan/payment-provider-stripe"` | | `import { PayPalProvider } from "@suaveplan/payment-providers"` | `import { PayPalProvider } from "@suaveplan/payment-provider-paypal"` | | `import { SquareProvider } from "@suaveplan/payment-providers"` | `import { SquareProvider } from "@suaveplan/payment-provider-square"` | | `import { MollieProvider } from "@suaveplan/payment-providers"` | `import { MollieProvider } from "@suaveplan/payment-provider-mollie"` | | `import { AdyenProvider } from "@suaveplan/payment-providers"` | `import { AdyenProvider } from "@suaveplan/payment-provider-adyen"` | | `import type { SquareConfig } from "@suaveplan/payment-providers"` | `import type { SquareConfig } from "@suaveplan/payment-provider-square"` | `ApiKeyConfig`, `OAuthConfig`, `MerchantKeyConfig`, `HttpOptions`, `ProviderResponse`, `ProviderCommonOptions`, `NowFn`, `RandomIdFn`, `Base64EncodeFn`, `FetchLike`, `httpRequest`, `composeAbortSignal`, `PaymentProviderError`, `PAYMENT_PROVIDER_CODES`, `httpError`, `timeoutError`, `parseError`, `oauthTokenFailure` continue to be exported from `@suaveplan/payment-providers` unchanged. Consumers that want a name-keyed lookup of multiple providers SHOULD now build a `ProviderRegistry`: ```ts import { ProviderRegistry } from "@suaveplan/payment-providers"; import { StripeProvider } from "@suaveplan/payment-provider-stripe"; import { PayPalProvider } from "@suaveplan/payment-provider-paypal"; const registry = new ProviderRegistry(); registry.register(new StripeProvider({ apiKey: STRIPE_KEY })); registry.register(new PayPalProvider({ clientId, clientSecret })); const stripe = registry.get("stripe"); // PaymentProvider ``` ## Source mapping Kept-core requirements derive from `../suaveplan/enterprise/payment-providers.md` and the just-shipped `openspec/archive/2026-05-17-add-payment-providers/` delta. Removed-provider requirements derive from the same archived delta: | Surface | Source | Status | |---|---|---| | Universal-tier purity | `enterprise/payment-providers.md` L37 | kept | | `PAYMENT_PROVIDER_CODES` registry + `PaymentProviderError` | `enterprise/payment-providers.md` L42, L61 | kept; codes extended with `PROVIDER_NOT_REGISTERED`, `PROVIDER_ALREADY_REGISTERED` | | `httpRequest` + `FetchLike` + timeout/abort | `archive/2026-05-17-add-payment-providers/specs/payment-providers/spec.md` Req "httpRequest helper..." | kept; `composeAbortSignal` now publicly exported | | `ApiKeyConfig` / `OAuthConfig` / `MerchantKeyConfig` / `HttpOptions` / `ProviderResponse` / `NowFn` / `RandomIdFn` / `Base64EncodeFn` | `archive/...` spec L62-L96 | kept | | `StripeProvider` full lifecycle | `archive/...` spec Req L100-L138 | REMOVED — re-lands in `@suaveplan/payment-provider-stripe` | | `PayPalProvider` OAuth + lifecycle | `archive/...` spec Req L142-L187 | REMOVED — re-lands in `@suaveplan/payment-provider-paypal` | | `SquareProvider` + injectable `randomId` | `archive/...` spec Req L191-L224 | REMOVED — re-lands in `@suaveplan/payment-provider-square` | | `MollieProvider` + string-amount marshalling | `archive/...` spec Req L228-L258 | REMOVED — re-lands in `@suaveplan/payment-provider-mollie` | | `AdyenProvider` + merchant context | `archive/...` spec Req L262-L293 | REMOVED — re-lands in `@suaveplan/payment-provider-adyen` | | `ProviderRegistry` | new genesis-originated | ADDED | | `definePaymentProvider` factory | new genesis-originated | ADDED | | `composeAbortSignal` public export | extracted from internal `http.ts` | ADDED | ## Out of scope - The 5 sibling provider packages themselves (`payment-provider-stripe`, `-paypal`, `-square`, `-mollie`, `-adyen`) — those are a follow-up wave, one OpenSpec change per package, each depending on this slimmed core. - Auto-discovery / dynamic loading of provider packages. Consumers explicitly install + register the providers they need. - `OpenTelemetry` spans / counters on the registry — same deferral as the parent migration (align with `@suaveplan/payment`). ## Impact ### Affected dependents Run `bunx turbo ls --filter=...@suaveplan/payment-providers` to enumerate. - `@suaveplan/payment` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: imports only the `PaymentProvider` interface from itself; does NOT import any concrete adapter or shared type from `@suaveplan/payment-providers`. Verified by `grep -rn "@suaveplan/payment-providers" packages/universal/payment/` → empty result). Future dependents (the 5 sibling provider packages) will each carry their own OpenSpec change naming this slimmed core as a peer/runtime dep. | Future sibling package | Will depend on this core for | |---|---| | `@suaveplan/payment-provider-stripe` | `httpRequest`, `composeAbortSignal`, `ApiKeyConfig`, `PaymentProviderError`, `PAYMENT_PROVIDER_CODES`, `definePaymentProvider`, optionally `ProviderRegistry` | | `@suaveplan/payment-provider-paypal` | `httpRequest`, `composeAbortSignal`, `OAuthConfig`, `NowFn`, `Base64EncodeFn`, `PaymentProviderError`, `oauthTokenFailure`, `definePaymentProvider` | | `@suaveplan/payment-provider-square` | `httpRequest`, `ApiKeyConfig`, `RandomIdFn`, `PaymentProviderError`, `definePaymentProvider` | | `@suaveplan/payment-provider-mollie` | `httpRequest`, `ApiKeyConfig`, `PaymentProviderError`, `definePaymentProvider` | | `@suaveplan/payment-provider-adyen` | `httpRequest`, `MerchantKeyConfig`, `RandomIdFn`, `PaymentProviderError`, `definePaymentProvider` | ## Parity Checklist This is a slim-down refactor, not a fresh migration, so the parity bar is against the just-shipped `2026-05-17-add-payment-providers` delta — not an upstream library: | Capability | Prior shape | Slimmed shape | Notes | |---|---|---|---| | `httpRequest` + `FetchLike` + timeout/abort | yes | yes | unchanged behaviour; `composeAbortSignal` extracted as public export | | `PaymentProviderError` + 4-code registry | yes | yes (+ 2 codes) | adds `PROVIDER_NOT_REGISTERED`, `PROVIDER_ALREADY_REGISTERED` | | `ApiKeyConfig` / `OAuthConfig` / `MerchantKeyConfig` / `HttpOptions` / `ProviderResponse` / `ProviderCommonOptions` / `NowFn` / `RandomIdFn` / `Base64EncodeFn` | yes | yes | verbatim | | `SquareConfig` | yes | 🚫 removed | Square-specific; sibling package re-declares locally so the core has no provider-specific shapes | | 5 concrete provider classes | yes | 🚫 removed | re-land in 5 sibling packages (`payment-provider-{stripe,paypal,square,mollie,adyen}`) per the follow-up wave | | `ProviderRegistry` + `definePaymentProvider` | 🚫 absent | ✅ added | new foundation API for the sibling-plugin pattern | ## Verification approach - `src/registry/registry.test.ts` — exercises every `ProviderRegistry` method (`register`, `get`, `has`, `list`, `unregister`, `size`), duplicate-registration error, unknown-name error, both new error codes reachable from real call sites. - `src/registry/define.test.ts` — exercises `definePaymentProvider`, confirms it returns a frozen `PaymentProvider`, preserves method identities, and the result registers cleanly into `ProviderRegistry`. - `src/error-codes/error-codes.test.ts` — extended to cover both new codes and the two new factories (`providerNotRegistered`, `providerAlreadyRegistered`). - `src/http/http.test.ts` — extended to cover the now-public `composeAbortSignal` helper end-to-end. - `tests/dep-graph.test.ts` — unchanged; still asserts the package has no Node / browser / runtime-specific imports. - `src/index.ts` barrel + `package.json#exports` — manually verified to contain only the slim core symbols / paths.