Files
2026-05-27 23:29:53 +01:00

13 KiB

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:

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:

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.