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

7.3 KiB

OpenSpec Change: enterprise-payment

  • Change ID: enterprise-payment
  • Package: @suaveplan/payment
  • Category: universal
  • Target path: universal/commerce/payment
  • Status: IN PROGRESS
  • Date: 2026-04-24
  • Reference spec: /mnt/s/web/suaveplan/enterprise/payment.md

Summary

Deliver @suaveplan/payment on the genesis branch from scratch at enterprise quality. The package provides payment intent lifecycle primitives: a state machine covering created → authorised → captured / refunded / failed transitions, webhook signature verification with bounded clock-skew (default 300s), idempotency-key enforcement via @suaveplan/idempotency (replay returns the recorded outcome, never re-executes), receipt generation (structured data, not HTML), amount arithmetic (integer cents, multi-currency), and a provider-agnostic PaymentAdapter interface. Concrete provider adapters live in @suaveplan/payment-providers. PCI scope is minimised: the package never stores or transports raw card data; only tokens, intent IDs, and last-four-digit fragments pass through.


Current-state audit (genesis)

The package does not exist on genesis yet — this is a fresh build.


Available workspace deps on genesis

Package Role
@suaveplan/error (workspace:^) PaymentError; PAYMENT.* error codes
@suaveplan/types (workspace:^) LoggerLike, Clock, EventBusLike
@suaveplan/events (workspace:^) Payment lifecycle event bus
@suaveplan/idempotency (workspace:^) Per-transition idempotency key guard
@suaveplan/crypto (workspace:^) Constant-time HMAC for webhook verification
@suaveplan/fsm (workspace:^) FSM interpreter for payment state machine
zod (catalog:forms) Schema validation
@opentelemetry/api (catalog:observability) Spans + counters

Dev deps: vitest, @vitest/coverage-istanbul, fast-check (all catalog:testing).


Public API surface

Core types

type PaymentStatus =
  | 'created' | 'authorised' | 'captured'
  | 'refunded' | 'partially_refunded' | 'failed' | 'cancelled'

interface PaymentIntent {
  id: string
  status: PaymentStatus
  amount: number         // integer cents
  currency: string       // ISO 4217 e.g. 'GBP'
  description?: string
  token?: string         // provider payment method token; no raw card data
  lastFour?: string
  metadata: Record<string, unknown>
  createdAt: Date
  updatedAt: Date
  history: PaymentTransition[]
}

interface PaymentTransition {
  from: PaymentStatus
  to: PaymentStatus
  idempotencyKey: string
  timestamp: Date
  actor?: string
}

interface Receipt {
  intentId: string
  amount: number
  currency: string
  formattedAmount: string   // e.g. '£12.50'
  status: PaymentStatus
  transitions: PaymentTransition[]
  issuedAt: Date
}

PaymentAdapter interface

interface PaymentAdapter {
  readonly provider: string
  create(options: CreatePaymentOptions): Promise<PaymentIntent>
  authorise(intentId: string, token: string, idempotencyKey: string): Promise<PaymentIntent>
  capture(intentId: string, amount: number, idempotencyKey: string): Promise<PaymentIntent>
  refund(intentId: string, amount: number, idempotencyKey: string): Promise<PaymentIntent>
  cancel(intentId: string, idempotencyKey: string): Promise<PaymentIntent>
  verifyWebhook(body: string, headers: Record<string, string>, secret: string): boolean
  normalizeWebhook(body: unknown): PaymentWebhookEvent[]
}

PaymentService

class PaymentService {
  constructor(options: PaymentServiceOptions)
  create(options: CreatePaymentOptions): Promise<PaymentIntent>
  authorise(intentId: string, token: string): Promise<PaymentIntent>
  capture(intentId: string, amount?: number): Promise<PaymentIntent>
  refund(intentId: string, amount?: number): Promise<PaymentIntent>
  cancel(intentId: string): Promise<PaymentIntent>
  getReceipt(intentId: string): Receipt
}

interface PaymentServiceOptions {
  adapter: PaymentAdapter
  store?: PaymentStore
  idempotency?: IdempotencyGuard
  logger?: LoggerLike
  clock?: Clock
  bus?: EventBusLike
}

Amount utilities

function formatAmount(amount: number, currency: string, locale?: string): string
function toMinorUnits(major: number, currency: string): number
function fromMinorUnits(minor: number, currency: string): number
function addAmounts(a: number, b: number): number
function subtractAmounts(a: number, b: number): number   // throws if negative

Error codes

type PaymentErrorCode =
  | 'PAYMENT_NOT_FOUND'
  | 'PAYMENT_INVALID_TRANSITION'
  | 'PAYMENT_SIGNATURE_FAILED'
  | 'PAYMENT_REPLAY_DETECTED'
  | 'PAYMENT_IDEMPOTENCY_CONFLICT'
  | 'PAYMENT_AMOUNT_INVALID'
  | 'PAYMENT_PROVIDER_ERROR'
  | 'PAYMENT_ABORTED'

Genesis additions

  1. PaymentError extends SuaveplanError with eight stable codes.
  2. Zod schemas for PaymentIntent, PaymentTransition, Receipt, CreatePaymentOptions, PaymentWebhookEvent.
  3. State machine — implemented via @suaveplan/fsm; illegal transitions surface PAYMENT_INVALID_TRANSITION with full history in the error context.
  4. Idempotency enforcement — every state-altering method requires an idempotency key passed through @suaveplan/idempotency; replay returns the cached PaymentIntent without re-executing the provider call.
  5. Webhook verification — constant-time HMAC via @suaveplan/crypto; bounded clock-skew rejects webhooks older than 300s (configurable); nonce dedup window prevents replay.
  6. PCI minimisation — token and lastFour fields only; no pan, cvv, expiry in any type.
  7. OTel instrumentation — span payment.transition with attributes payment.from_status, payment.to_status, payment.provider; counter payment.transition_total; histogram payment.transition_duration_ms.
  8. Property-based tests — state machine reachability: every valid transition chain from created eventually reaches a terminal state; amount arithmetic properties (commutativity of add, non-negativity of subtract).

File layout

universal/commerce/payment/
  package.json
  tsconfig.json
  vitest.config.ts
  src/
    index.ts
    error-codes.ts
    constants/
      index.ts              # ALLOWED_TRANSITIONS, TERMINAL_STATUSES, DEFAULT_SKEW_MS
    types/
      common.ts
      common.md
    core/
      payment-service/
        payment-service.ts
        payment-service.md
        payment-service.types.ts
        payment-service.test.ts
        index.ts
      payment-fsm/
        payment-fsm.ts
        payment-fsm.md
        payment-fsm.test.ts
        index.ts
    webhook/
      webhook-verifier/
        webhook-verifier.ts
        webhook-verifier.md
        webhook-verifier.test.ts
        index.ts
    receipt/
      receipt-generator/
        receipt-generator.ts
        receipt-generator.md
        receipt-generator.test.ts
        index.ts
    utils/
      amount.ts
      amount.md
      amount.test.ts
    __benchmarks__/
      payment.bench.ts
  tests/
    integration.test.ts
    property.test.ts

Impact

Downstream consumers: @suaveplan/payment-providers, @suaveplan/booking.

Workspace dep prerequisites: @suaveplan/error, @suaveplan/types, @suaveplan/events, @suaveplan/idempotency, @suaveplan/crypto, @suaveplan/fsm.