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

180 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |