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

11 KiB
Raw Permalink Blame History

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<PaymentProviderErrorCode> 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}/<name>.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 × {<name>.ts, <name>.md, <name>.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<PaymentProviderErrorCode> 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/<provider>.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/<feature>/<feature>.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