Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,280 @@
# Add @suaveplan/payment — universal-tier payment intent lifecycle
**Package**: `@suaveplan/payment`
**Status**: PROPOSED
**Type**: migration (upstream `../suaveplan/packages/universal/payment`)
- **Spec delta**: ./specs/payment/spec.md
## Why
`STATUS.md` line 132 lists `@suaveplan/payment` as Pending. The upstream
package owns the entire payment-intent lifecycle: the 9-state machine, the
typed event emissions for each transition, webhook signature verification
with replay protection, structured receipt generation, fraud scoring,
refund-balance tracking, jurisdiction-aware tax calculation, retry
orchestration with exponential backoff, gateway adapters for Stripe and
PayPal (plus an in-memory test gateway), PCI-conscious card tokenisation,
and gateway reconciliation. With `@suaveplan/money` shipping in the same
wave, this is the next domino — every commerce-tier downstream
(`@suaveplan/payment-providers`, `@suaveplan/api-plugin-payment`,
`@suaveplan/api-plugin-banking`, `@suaveplan/booking`) depends on the
`PaymentEngine`, `PaymentProvider` interface, `PaymentIntent` shape, and
the typed `PaymentError` registry.
The migration preserves the entire upstream public surface verbatim
(`PaymentEngine`, `WebhookVerifier`, `ReceiptGenerator`, `TestGateway`,
`StripeGateway`, `PayPalGateway`, `FraudDetector`, `RefundManager`,
`PaymentRetrier`, `TaxCalculator`, `CardTokenizer`,
`PaymentReconciliation`, `paymentHooks` / `onPaymentProcessing`, every type
and constant) while upgrading three foundational concerns to the genesis
quality bar:
1. **Typed `PaymentError`** extending `SuaveplanError<PaymentErrorCode>`
replaces the upstream `BaseError` and the side-effect-imported
`ErrorCodes.registerMany` mechanism. The `PAYMENT_CODES` registry is
declared via `defineCodes("PAYMENT", [...])` (13 codes), and every
former `throw new Error(...)` boundary upgrades to a typed factory.
2. **Universal-tier purity** — the upstream webhook verifier imported
`node:crypto`; we route HMAC through `@suaveplan/crypto-hash` and
constant-time compare through `@suaveplan/crypto-utils`. The card
tokenizer keeps using `SubtleCrypto` (already universal). The PayPal
adapter accepts injected `btoa` to keep the universal contract on
runtimes that name it differently.
3. **Injectable boundaries** — every `Date.now()` / `Math.random()` /
`fetch` / `setTimeout` use is hoisted to a config option so tests are
deterministic without `vi.mock`. This also enables the no-mocks rule
while keeping 100% coverage tractable.
The 18 upstream source files collapse to 16 (the upstream
`errors/error-codes.ts` + `errors/payment-errors.ts` pair merges into one
`error-codes/error-codes.ts`; the upstream `gateway/gateway.types.ts` +
`gateway/test-gateway.ts` / `stripe-gateway.ts` / `paypal-gateway.ts`
keep their separate files but live under `gateway/` with no `*-gateway`
suffix per the no-redundant-naming rule). One sibling `.md` per
implementation file lands at genesis quality (≥200 words, basic +
advanced examples, full API reference).
## What changes
- ADDED: `PaymentEngine` — orchestrator over a `PaymentProvider`,
`EventBus<PaymentEventMap>`-backed lifecycle events
(`intent.created`/`confirmed`/`captured`/`canceled`/`failed`,
`refund.created`/`succeeded`/`failed`, `dispute.opened`/`closed`),
`VALID_TRANSITIONS` enforcement, refund-balance guard, hook pipeline.
Explicit `markFailed` / `openDispute` / `closeDispute` emitters so the
closed event map is fully reachable.
- ADDED: `WebhookVerifier` — HMAC-SHA-{256,384,512} over
`timestamp.body`, constant-time signature compare via
`@suaveplan/crypto-utils`, replay window via configurable `toleranceMs`,
injected `now()` clock.
- ADDED: `ReceiptGenerator` + `resetReceiptCounter` — structured `Receipt`
records + plain-text renderer with injected `now()` and `nextId`.
- ADDED: `PaymentGateway` interface + `GatewayChargeStatus` /
`GatewayResult` / `GatewayCharge` / `ValidationResult` / `ChargeOptions`
types.
- ADDED: `TestGateway` + `resetChargeCounter` — in-memory adapter with
recorded operations log, configurable failure modes, injected `now()`.
- ADDED: `StripeGateway` — Payment Intents API via injected `FetchLike`;
defaults `apiVersion` to `2024-12-18.acacia`; constant-time-safe.
- ADDED: `PayPalGateway` — Orders API v2 via injected `FetchLike`; token
cache uses injected `now()`; injected `btoa` for universal Base-64.
- ADDED: `FraudDetector` — velocity / amount / customer / new-method
heuristics with configurable thresholds + injected `now()`.
- ADDED: `RefundManager` + `resetRefundCounter` — per-intent refund
ledger raising typed `PaymentError(PAYMENT.REFUND_INTENT_NOT_REGISTERED)`
or `PaymentError(PAYMENT.REFUND_EXCEEDS_CAPTURED)`.
- ADDED: `PaymentRetrier` — exponential backoff + jitter, non-retryable
code blocklist, injected `random()` + `sleep()`.
- ADDED: `TaxCalculator` — region → country → default fallback, inclusive
+ exclusive pricing, exempt items.
- ADDED: `CardTokenizer` — Luhn + brand detection + HMAC-SHA-256 over
injected `SubtleCrypto`; typed `PaymentError(PAYMENT.INVALID_CARD)` on
validation failure.
- ADDED: `PaymentReconciliation` — internal-vs-gateway matcher with
normalised status comparison, orphan detection on both sides,
injected `now()`.
- ADDED: `paymentHooks` singleton + `Hooks<TMap>` class + `onPaymentProcessing`
shortcut — package-local replacement for the upstream
`@suaveplan/events/hooks` import (the genesis events package does not
yet expose `Hooks`; reusing the contract avoids a follow-up rewrite
when it does).
- ADDED: `PAYMENT_CODES` registry via `defineCodes("PAYMENT", [...])` —
13 codes (`NOT_FOUND`, `INVALID_TRANSITION`, `ALREADY_CAPTURED`,
`ALREADY_REFUNDED`, `REFUND_EXCEEDS_CAPTURED`, `PROVIDER_ERROR`,
`WEBHOOK_INVALID_SIGNATURE`, `WEBHOOK_EXPIRED`, `WEBHOOK_PARSE_ERROR`,
`AMOUNT_MISMATCH`, `INVALID_CARD`, `GATEWAY_ERROR`,
`REFUND_INTENT_NOT_REGISTERED`).
- ADDED: `PaymentError extends SuaveplanError<PaymentErrorCode>` with
optional `intentId` / `currentStatus` / `targetStatus` / `ageMs`
discriminants and 12 factory functions matching the upstream surface
plus two genesis-only additions (`alreadyRefunded`, `invalidCard`,
`gatewayError`, `refundIntentNotRegistered`).
- ADDED: Subpath exports per public module — 16 entries covering every
feature; tree-shakeable.
## Out of scope
- **`@suaveplan/idempotency` integration.** The enterprise audit calls
for idempotency-key enforcement around every state transition; the
upstream library accepts and passes through `idempotencyKey` but does
not enforce dedup. The migration preserves the upstream contract;
a follow-up `add-payment-idempotency` change wires
`@suaveplan/idempotency` into the engine's mutating paths.
- **`@suaveplan/schema-registry` Zod boundaries.** The enterprise audit
asks for Zod schemas on every intent, webhook envelope, and receipt.
Adding them would change the public types' runtime behaviour (validate
at the boundary, throw on parse fail); deferred to a follow-up so the
migration baseline preserves behaviour bit-for-bit. The
`WebhookPayload<T>` generic remains unchanged.
- **`@suaveplan/feature-flags` rollouts for risky provider behaviour.**
Upstream does not use them; additive new feature deferred.
- **`@suaveplan/opentelemetry` instrumentation.** Upstream emits zero
OTel spans; a future `enterprise-payment` change adds spans, metrics,
and counters around every entry point.
- **`@suaveplan/retry` consumer migration.** The upstream `PaymentRetrier`
is a private re-implementation; replacing it with
`@suaveplan/retry` would change the public surface (different option
field names, different `RetryResult` shape). Deferred.
- **Property-based tests (fast-check).** The deterministic suite hits
100% — fast-check folds in once that baseline is green, mirroring the
money migration.
- **Side-effect error-code registration in `ErrorCodes.registerMany`.**
Upstream side-effect-imports the registration at `index.ts` load
time; the genesis `@suaveplan/error` package exposes
`defineCodes(...)` directly without a process-wide singleton, so the
registration ceremony goes away. The discrimination contract
(`err.code === PAYMENT_CODES.NOT_FOUND`) is preserved.
- **Consumer migration.** No genesis consumer exists yet; downstream
packages adopt the new contract when migrated.
## Source mapping
This change derives from `../suaveplan/enterprise/payment.md` and the
upstream source under `../suaveplan/packages/universal/payment/src/`.
| Requirement | Source lines | Notes |
|---|---|---|
| Universal-tier purity (zero browser/Node deps) | enterprise/payment.md L37 | Reinforced; upstream webhook used `node:crypto` — replaced with `@suaveplan/crypto-hash` |
| `PaymentIntent` shape + 9-state lifecycle | enterprise/payment.md L12, L40; upstream `src/types/payment.types.ts` L1-L166 | Verbatim |
| `VALID_TRANSITIONS` state-machine table | upstream `src/types/payment.types.ts` L151-L165 | Verbatim |
| `PaymentEngine` with `createIntent`/`confirmIntent`/`captureIntent`/`cancelIntent`/`refund`/`getIntent` | enterprise/payment.md L40; upstream `src/engine/payment-engine.ts` L1-L169 | Verbatim |
| State-transition enforcement → typed `PAYMENT.INVALID_TRANSITION` | enterprise/payment.md L40, L46; upstream `src/engine/payment-engine.ts` L154-L167 | Verbatim |
| Refund-balance guard → typed `PAYMENT.REFUND_EXCEEDS_CAPTURED` | enterprise/payment.md L46; upstream `src/engine/payment-engine.ts` L121-L127 | Verbatim |
| Lifecycle event emission | enterprise/payment.md L12; upstream `src/engine/payment-engine.ts` L53/L68/L86/L101/L130-L135 | Verbatim (with `EventBus.emitAsync`) |
| Webhook verification with HMAC + replay window | enterprise/payment.md L41, L43-L44; upstream `src/webhook/webhook-verifier.ts` L1-L88 | Verbatim; HMAC routed through `@suaveplan/crypto-hash` |
| Webhook constant-time signature compare | enterprise/payment.md L41, L43; upstream `src/webhook/webhook-verifier.ts` L74-L77 | Replaced with `@suaveplan/crypto-utils.constantTimeEqual` |
| Receipt generation | enterprise/payment.md L12; upstream `src/receipt/receipt-generator.ts` L1-L165 | Verbatim with injected `now`/`nextId` |
| `PaymentGateway` abstract contract | upstream `src/gateway/gateway.types.ts` L1-L70 | Verbatim |
| `TestGateway` in-memory adapter | upstream `src/gateway/test-gateway.ts` L1-L267 | Verbatim with injected `now()` |
| `StripeGateway` Payment Intents API | upstream `src/gateway/stripe-gateway.ts` L1-L303 | Verbatim with injected `FetchLike` |
| `PayPalGateway` Orders API v2 + token cache | upstream `src/gateway/paypal-gateway.ts` L1-L328 | Verbatim with injected `FetchLike`/`now`/`btoa` |
| `FraudDetector` velocity / amount / customer / new-method heuristics | upstream `src/fraud/fraud-detector.ts` L1-L185 | Verbatim with injected `now()` |
| `RefundManager` per-intent ledger | upstream `src/refund/refund-manager.ts` L1-L147 | Verbatim, throws typed `PaymentError`s instead of `Error` |
| `PaymentRetrier` exponential backoff | upstream `src/retry/payment-retry.ts` L1-L157 | Verbatim with injected `random()`/`sleep()` |
| `TaxCalculator` jurisdiction registry | upstream `src/tax/tax-calculator.ts` L1-L183 | Verbatim |
| `CardTokenizer` PCI-conscious hashing | enterprise/payment.md L42; upstream `src/tokenization/card-tokenizer.ts` L1-L156 | Verbatim, throws typed `PaymentError(INVALID_CARD)`, injected `SubtleCrypto` |
| `PaymentReconciliation` matcher | upstream `src/reconciliation/reconciliation.ts` L1-L158 | Verbatim |
| `paymentHooks` + `onPaymentProcessing` | upstream `src/hooks.ts` L1-L60 | Re-implemented locally — genesis `@suaveplan/events` does not expose `Hooks` yet; surface preserved verbatim |
| Stable error-code registry | enterprise/payment.md L46; upstream `src/errors/error-codes.ts` + `src/errors/payment-errors.ts` | Merged + upgraded to `defineCodes("PAYMENT", [...])`; expanded to 13 codes |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| 100% test coverage (istanbul, all four axes) | Genesis-wide policy (CLAUDE.md §7) — upstream was at ~99% |
| Sibling `.md` per implementation file (≥200 words, basic + advanced examples) | Genesis-wide policy (CLAUDE.md §4) — upstream had partial docs |
| Test files import runner primitives from `@suaveplan/testing/runner` | Genesis-wide policy (AGENTS.md §7) — upstream imported `vitest` directly |
| Explicit named re-exports in every feature-module barrel (no `export *`) | Genesis-wide policy (AGENTS.md §6a) |
| Package version starts at `0.1.0` | Genesis-wide policy (CLAUDE.md "Editing") — upstream was `1.2.6` |
| Injected `now()` / `random()` / `sleep()` / `fetch` / `btoa` / `SubtleCrypto` on every IO + clock boundary | Required to satisfy the no-mocks rule at 100% coverage without `vi.mock` |
| `markFailed` / `openDispute` / `closeDispute` explicit emitters | Required so the closed `PaymentEventMap` has a production code path for every key (engine alone never emits `intent.failed`/`dispute.*`); upstream relied on the open-ended `[key: string]: unknown[]` escape hatch which `EventBus<TMap>` does not allow |
| 13 error codes (vs upstream's 10) | Three genesis-only codes (`INVALID_CARD`, `GATEWAY_ERROR`, `REFUND_INTENT_NOT_REGISTERED`) replace upstream's raw `throw new Error(...)` at the tokeniser, gateway-adapter, and refund-manager boundaries respectively |
### Parity Checklist
| Capability | Upstream | Migrated | Notes |
|---|---|---|---|
| `PaymentEngine` full lifecycle surface | ✅ | ✅ | Every method preserved |
| `VALID_TRANSITIONS` state machine | ✅ | ✅ | Verbatim, now `Object.freeze`d |
| Typed lifecycle events | ✅ | ⬆️ Enhanced | Closed map (no `[key: string]` escape hatch); explicit emitters for failed/disputed states |
| `WebhookVerifier` HMAC + replay window | ✅ | ⬆️ Enhanced | Universal-tier (uses `@suaveplan/crypto-hash`); injectable `now()` |
| `ReceiptGenerator` + plain-text renderer | ✅ | ⬆️ Enhanced | Injectable `now()` + `nextId` |
| `PaymentGateway` contract | ✅ | ✅ | Verbatim |
| `TestGateway` | ✅ | ⬆️ Enhanced | Injectable `now()` |
| `StripeGateway` | ✅ | ⬆️ Enhanced | Injectable `FetchLike` for tests; typed errors |
| `PayPalGateway` | ✅ | ⬆️ Enhanced | Injectable `FetchLike`/`now`/`btoa`; typed errors |
| `FraudDetector` | ✅ | ⬆️ Enhanced | Injectable `now()` |
| `RefundManager` | ✅ | ⬆️ Enhanced | Throws typed `PaymentError`s; injectable `now()`/`nextId` |
| `PaymentRetrier` | ✅ | ⬆️ Enhanced | Injectable `random()`/`sleep()` |
| `TaxCalculator` | ✅ | ✅ | Verbatim |
| `CardTokenizer` | ✅ | ⬆️ Enhanced | Throws typed `PaymentError(INVALID_CARD)`; injectable `SubtleCrypto`/`now()` |
| `PaymentReconciliation` | ✅ | ⬆️ Enhanced | Injectable `now()` |
| `paymentHooks` / `onPaymentProcessing` | ✅ | ✅ | Re-implemented locally (genesis `@suaveplan/events` lacks `Hooks`); surface preserved |
| `PaymentError` factory functions | ✅ | ⬆️ Enhanced | Now extends `SuaveplanError<PaymentErrorCode>`; 13 codes (vs upstream 10) |
| Subpath exports | ✅ | ⬆️ Enhanced | 16 entries (vs upstream's 12) |
| Idempotency-key enforcement | 🚫 upstream (audit asks) | 🚫 deferred | `@suaveplan/idempotency` integration in follow-up |
| Zod schemas at boundaries | 🚫 upstream (audit asks) | 🚫 deferred | Behaviour-preserving baseline first |
| OTel spans / counters / histograms | 🚫 upstream (audit asks) | 🚫 deferred | `enterprise-payment` follow-up |
| Feature flags for provider rollouts | 🚫 upstream (audit asks) | 🚫 deferred | Additive new feature |
| Property-based tests (fast-check) | 🚫 upstream (audit asks) | 🚫 deferred | Deterministic suite hits 100% first |
| `@suaveplan/retry` consumer migration | 🚫 upstream (audit asks) | 🚫 deferred | Changes public retry surface |
### Reference docs consulted
- ✅ `../suaveplan/packages/universal/payment/src/` — all 18 implementation files read
- ✅ `../suaveplan/packages/universal/payment/README.md`
- ✅ `../suaveplan/packages/universal/payment/package.json`
- ✅ `../suaveplan/enterprise/payment.md` — full
- 🚫 `../suaveplan/openspec/specs/payment/spec.md` — does not exist
- 🚫 `../suaveplan/openspec/changes/*/proposal.md` touching `payment` — none beyond `enterprise-payment` itself
## Verification approach
Every requirement in the spec delta has a `**Verified by:**` line naming
the test files that pin its scenarios. The test layout mirrors the source
layout — one `<feature>.test.ts` per `<feature>.ts`:
- `src/error-codes/error-codes.test.ts` — registry, factories, discriminants
- `src/hooks/hooks.test.ts` — generic class + singleton + priority
- `src/engine/engine.test.ts` — every lifecycle path + transition guard +
provider-error wrap (Error + non-Error throws) + out-of-band emitters
- `src/webhook/webhook.test.ts` — signature determinism, tolerance, parse,
algorithm matrix, fallback clock
- `src/receipt/receipt.test.ts` — full render + minimal render + injected
`nextId` + `taxLabel` fallback
- `src/gateway/test.test.ts` — happy + failure mid-stream + validation
- `src/gateway/stripe.test.ts` — create + capture + refund + get +
validate via injected `FetchLike`; success / cancelled / unknown /
HTTP-error paths
- `src/gateway/paypal.test.ts` — auth + token cache + token refresh +
capture + refund (with capture id) + refund (no capture) + status map
- `src/fraud/fraud.test.ts` — every flag, threshold boundary, reset, cap
- `src/refund/refund.test.ts` — every error code reachable, summary,
by-reason filter, predicate, injected nextId
- `src/retry/retry.test.ts` — success / retry-success / non-retryable
result / non-retryable thrown / exhaustion / non-Error wrap /
calculateDelay matrix + default sleep
- `src/tax/tax.test.ts` — registry, region override, exclusive,
inclusive, exempt, empty input
- `src/tokenization/tokenization.test.ts` — tokenize / Luhn (positive +
length + checksum) / brand (every supported pattern) / typed-error paths
- `src/reconciliation/reconciliation.test.ts` — match / discrepancy /
unmatched both sides / status normalisation / clock injection
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/payment` to enumerate.
- No dependents — this is a new universal-tier package on genesis. Every
potential downstream (`@suaveplan/payment-providers`,
`@suaveplan/api-plugin-payment`, `@suaveplan/api-plugin-banking`,
`@suaveplan/booking`) is still in the `Pending` list and adopts this
package when migrated.
## Migration impact
None — additive only. No existing genesis package imports
`@suaveplan/payment`.