11 KiB
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:
- Injectable
FetchLike— every provider accepts an optionalfetchconfig field defaulting toglobalThis.fetch. Tests use a real recordingFetchLikerather thanvi.spyOn(globalThis, "fetch"), so the no-mocks rule (CLAUDE.md §2) is preserved without losing coverage. - Injectable
now/randomId—PayPalProvider's token-cache clock andSquareProvider/AdyenProvider's default idempotency-key generator are config fields. This removes module-scopeDate.now()andcrypto.randomUUID(), aligning with the deterministic universal-tier rule. - Injectable
base64Encode— PayPal's OAuth Basic header usesbtoaby default but accepts an injected encoder so the package runs on every runtime that names Base-64 differently. - Typed
PaymentProviderErrorextendingSuaveplanError<PaymentProviderErrorCode>replaces the upstreamthrow new Error(...)raised insidehttpRequest. ThePAYMENT_PROVIDER_CODESregistry is declared viadefineCodes("PAYMENT_PROVIDER", [...])(4 codes:HTTP_ERROR,TIMEOUT,PARSE_ERROR,OAUTH_TOKEN_FAILURE) and every formerErrorboundary upgrades to a typed factory carrying the HTTP status, response body excerpt, and provider name as discriminants. - Folder layout — the upstream flat
src/{stripe,paypal,...}.tsbecomessrc/providers/{stripe,paypal,square,mollie,adyen}/<name>.tsper 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-providersuffix).httpRequestmoves tosrc/http/http.tsand its config types tosrc/types/provider.tsso 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, injectablenowfor cache TTL, injectablebase64Encodefor the Basic auth header. - ADDED:
SquareProvider— Square Payments adapter, Bearer auth, manual + automatic capture,Square-Versionheader, injectablerandomIdfor 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-Keyheader, merchant-account context, injectablerandomId. - ADDED:
httpRequest— universalFetchLikewrapper with timeout viaAbortSignal, JSON serialisation, typed errors. - ADDED:
PAYMENT_PROVIDER_CODESregistry viadefineCodes("PAYMENT_PROVIDER", [...])— 4 codes (HTTP_ERROR,TIMEOUT,PARSE_ERROR,OAUTH_TOKEN_FAILURE). - ADDED:
PaymentProviderError extends SuaveplanError<PaymentProviderErrorCode>carrying optionalprovider,httpStatus,bodyExcerpt, andurldiscriminants; factorieshttpError,timeoutError,parseError,oauthTokenFailure. - ADDED:
FetchLiketype 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/idempotencyintegration. Providers acceptidempotencyKeyand forward it to the provider; auto-enforcement via the@suaveplan/idempotencymiddleware is a follow-up change once that package lands.- OTel spans / counters. The enterprise audit calls for per-provider
observability; the genesis
@suaveplan/paymentmigration 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/webhookHmacKeyconfig fields but does NOT implement verification — that lives in@suaveplan/payment'sWebhookVerifier. The config fields are preserved as opaque pass-throughs so future verifier integration has the secret material it needs. - Schema-registry integration. The
PaymentProviderinterface is validated at the type level; runtime Zod schemas around provider inputs / outputs are a@suaveplan/paymentconcern. - Adyen notification HMAC. The
webhookHmacKeyconfig field is preserved verbatim from upstream but the matching HMAC verifier ships with@suaveplan/payment'sWebhookVerifier, 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— covershttpRequest, timeout, error wrapping, typedPaymentProviderErrorsrc/error-codes/error-codes.test.ts— every code reachable, cause propagation, discriminantssrc/providers/stripe/stripe.test.ts—StripeProviderfull surface + status / refund-reason mappingsrc/providers/paypal/paypal.test.ts—PayPalProviderfull surface + OAuth token cache + refreshsrc/providers/square/square.test.ts—SquareProviderfull surface +randomIdinjectionsrc/providers/mollie/mollie.test.ts—MollieProviderfull surface + amount marshallingsrc/providers/adyen/adyen.test.ts—AdyenProviderfull surface + merchant-account contexttests/dep-graph.test.ts— universal-tier purity grep (nonode:*/bun:*/fs/pathimports)
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 |