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

5.1 KiB
Raw Permalink Blame History

Tasks: enterprise-payment

Change ID: enterprise-payment Package: @suaveplan/payment Target path: universal/commerce/payment Prerequisite packages on Verdaccio: @suaveplan/error, @suaveplan/types, @suaveplan/events, @suaveplan/idempotency, @suaveplan/crypto, @suaveplan/fsm


0. Pre-flight (non-negotiable)

  • 0.1 Confirm clean working tree (git status reports nothing) and no in-flight worktrees with uncommitted work (git worktree list then git status in each)
  • 0.2 If any worktrees exist under .claude/worktrees/, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (git worktree remove) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
  • 0.3 Confirm the package set in MANIFEST.md matches find packages -maxdepth 3 -name package.json count. Drift here means a prior change archived without regenerating the manifest.

1. Scaffold

  • Create universal/commerce/payment/ with src/, tests/, src/__benchmarks__/ and all feature subdirectories per the layout in proposal.md.
  • Write package.json at version 0.1.0 with "type": "module", "sideEffects": false, correct exports map, dependencies, and devDependencies.
  • Write tsconfig.json extending @suaveplan/typescript-config/library.json.
  • Write vitest.config.ts using createTestConfig() from @suaveplan/testing/config.
  • Add lint, typecheck, test, test:coverage, build, docs:check scripts.

2. Types + errors

  • src/types/common.ts — PaymentStatus, PaymentIntent, PaymentTransition, Receipt, CreatePaymentOptions, PaymentWebhookEvent, PaymentAdapter (interface), PaymentStore (interface), PaymentServiceOptions.
  • src/types/common.md (≥200 words, both examples, API reference).
  • src/error-codes.ts — PaymentErrorCode (8 codes), metadata, register, eager call.
  • src/constants/index.ts — ALLOWED_TRANSITIONS map, TERMINAL_STATUSES set, DEFAULT_CLOCK_SKEW_MS = 300_000.
  • src/types/schemas.ts + schemas.md — Zod schemas. Note: PaymentIntent schema must NOT include pan, cvv, or expiry fields.

3. Core modules

  • src/core/payment-fsm/ — PaymentFsm: wraps @suaveplan/fsm; defines all valid transitions per ALLOWED_TRANSITIONS; transition(intent, to, key) checks legality, throws PAYMENT_INVALID_TRANSITION if illegal, returns new PaymentIntent with updated status and appended PaymentTransition. Tests: all legal transitions, two illegal transitions, transition adds history entry.
  • src/webhook/webhook-verifier/ — WebhookVerifier.verify(body, secret, timestamp, signature): constant-time HMAC via @suaveplan/crypto; checks |now - timestamp| <= skewMs; throws PAYMENT_SIGNATURE_FAILED on mismatch, PAYMENT_SIGNATURE_FAILED on expired. Tests: valid, tampered, expired, missing timestamp.
  • src/receipt/receipt-generator/ — generateReceipt(intent): Receipt: maps intent fields; formattedAmount uses Intl.NumberFormat with currency. Tests: GBP formatting, USD formatting, partial refund status, all terminal statuses.
  • src/utils/amount.ts — formatAmount, toMinorUnits, fromMinorUnits, addAmounts, subtractAmounts. subtractAmounts throws PAYMENT_AMOUNT_INVALID if result < 0. Tests: all functions, negative subtraction throws.
  • src/core/payment-service/ — PaymentService: create, authorise, capture, refund, cancel — each wraps state transition with idempotency guard (from options.idempotency); calls options.adapter for provider interaction; stores result in options.store; emits event on options.bus. getReceipt(intentId) fetches from store and calls generateReceipt. Tests: happy path create→authorise→capture, duplicate idempotency key returns cached result, refund > captured amount throws, cancel from terminal state throws.

4. OTel instrumentation

  • Span payment.transition with attributes payment.from_status, payment.to_status, payment.provider; counter payment.transition_total; histogram payment.transition_duration_ms.

5. Property-based tests (tests/property.test.ts)

  • State machine reachability: every valid create→terminal path through ALLOWED_TRANSITIONS terminates in captured, refunded, failed, or cancelled.
  • Amount arithmetic: addAmounts(a, b) === addAmounts(b, a) for non-negative integers; fromMinorUnits(toMinorUnits(x, 'GBP'), 'GBP') === x for 0–1 000 000.

6. Benchmarks (src/__benchmarks__/payment.bench.ts)

  • PaymentFsm.transition 100 000 times.
  • generateReceipt 100 000 times.

7. Docs + verify

  • One .md per implementation file (≥200 words, both examples, API reference). Root README.md.
  • bunx turbo lint typecheck test --filter=@suaveplan/payment — green.
  • bunx turbo test:coverage --filter=@suaveplan/payment — 100%.
  • bun scripts/publish.ts --package @suaveplan/payment.
  • Archive: openspec/archive/2026-04-24-enterprise-payment/.