Files
suaveplan/openspec/archive/2026-05-17-slim-payment-providers/proposal.md
2026-05-27 23:29:53 +01:00

232 lines
13 KiB
Markdown

# 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<TName extends string>(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.