# 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` 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`-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` 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` 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` 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` 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`; 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 `.test.ts` per `.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`.