Files
nick-doc/09 - Audits/Payment Provider Adapter Spec.md
2026-05-24 11:31:40 +04:00

6.5 KiB

title, tags, created, status, reviewers
title tags created status reviewers
Payment Provider Adapter Spec
adapters
payments
specification
architecture
2026-05-24 advisory
backend
security
product

Payment Provider Adapter Spec

This specification standardizes how payment providers are plugged in so platform logic does not depend on SHKeeper or any single webhook implementation.

The contract below replaces provider-specific branching in domain services and should be used by all pay-in, payout, release, and reconciliation logic.

Canonical implementation note: this is an advisory ADR for tasks in task 4.6. It maps to Funds Ledger and Escrow State Machine Specification and Webhook Security Spec.

1. Core provider contract

Implementations expose one typed adapter:

interface PaymentProviderAdapter {
  readonly provider: "shkeeper" | "request_network" | "manual_wallet" | "admin_wallet" | string;

  createPayInIntent(input: PayInIntentInput): Promise<PayInIntentResult>;
  getPayInStatus(input: PayInStatusInput): Promise<PayInStatusResult>;
  handleProviderWebhook(input: ProviderWebhookInput): Promise<ProviderWebhookResult>;
  createHostedPaymentLink(input: HostedLinkInput): Promise<HostedLinkResult>;
  createReleaseInstruction(input: ReleaseInstructionInput): Promise<ReleaseInstructionResult>;
  createRefundInstruction(input: RefundInstructionInput): Promise<RefundInstructionResult>;
  getPayoutStatus(input: PayoutStatusInput): Promise<PayoutStatusResult>;
  searchProviderPayments(input: ProviderSearchInput): Promise<ProviderPaymentRecord[]>;
}

All adapters must return a normalized result shape:

type NormalizedProviderStatus = "pending" | "processing" | "confirmed" | "completed" | "failed" | "cancelled" | "released" | "refunded";
type NormalizedProviderEvent = {
  providerPaymentId: string;
  purchaseRequestId?: string;
  requestId?: string;
  providerReference?: string;
  amount: string;      // decimal string
  currency: string;
  status: NormalizedProviderStatus;
  transactionHash?: string;
  providerEventType: string;
  receivedAt: string;  // ISO timestamp
  rawFingerprint: string; // provider payload hash
};

2. Method semantics

2.1 createPayInIntent

  • Create a provider-specific payment intent from a canonical request.
  • Must return:
    • providerPaymentId (source of truth for future reconciliation),
    • canonical status,
    • payInUrl when redirect/payment-page flow is used,
    • an expiry timestamp.
  • Must persist provider metadata under payment.providerData.<provider>.

2.2 getPayInStatus

  • Query provider status for an existing intent.
  • Must map provider statuses into NormalizedProviderStatus and include a provider-specific raw snapshot.
  • Must be idempotent and side-effect free.

2.3 handleProviderWebhook

  • Input must include raw body bytes, headers, provider identifier, and parsed envelope.
  • Must verify signatures before parsing business fields.
  • On success, emit canonical domain events and return an idempotency decision:
    • processed for first apply,
    • duplicate for replay,
    • ignored for unknown payment / no-op transitions.
  • Return the user-visible payment URL + optional redirect/callback endpoints.
  • Should support provider aliases (for migration aliasing, e.g., request-network vs request_network).

2.5 createReleaseInstruction and createRefundInstruction

  • Produce signed/payload instructions and pre-check:
    • account/release eligibility,
    • dispute hold not active,
    • sufficient releasable balance (ledger-derived),
    • admin approval requirements if configured.
  • Must never directly mutate release state.
  • Must be idempotent by (paymentId, actionType) where action type is release|refund.

2.6 getPayoutStatus

  • Return state of pending/processing payout tasks and chain/on-chain confirmation status.
  • Return normalized status to domain services:
    • processing for queued/broadcast not-finalized,
    • completed for finalized payment,
    • failed with provider error code when rejected.

2.7 searchProviderPayments

  • Used for reconciliation and manual verification.
  • Must support:
    • providerPaymentId/requestId lookup,
    • time-window pagination,
    • optional min/max amount filtering.
  • Must never be the primary source for state transitions without reconciliation checks.

3. Routing and selection

Provider selection follows environment-configured capability flags:

  • PAYMENT_ENABLED_PROVIDERS (comma-separated allowlist),
  • PAYMENT_DEFAULT_PROVIDER (read-first fallback),
  • PAYMENT_ROLLBACK_PROVIDER (read-only fallback target for cutbacks),
  • PAYMENT_MODE:
    • standard: normal provider routing,
    • dry_run: no writes, status-only,
    • read_only: no new pay-in/intent writes.

Selection rules:

  1. Validate provider support and provider license/credential validity.
  2. Route legacy requests to shkeeper when explicit migration window is active.
  3. For unknown provider, return a 400 Bad Request with explicit operator-visible error code.
  4. If requested provider is disabled, return 409 with migration explanation and owner-visible hint for operator override.

4. Canonical metadata contract

Payment documents keep provider-specific data namespaced under:

  • metadata.providers.<provider>.rawPayload
  • metadata.providers.<provider>.rawEvents[]
  • metadata.providers.<provider>.providerPaymentId
  • metadata.providers.<provider>.lastWebhookAt

Domain services must never read metadata.providers.* as mutable funds state. They must use ledger-derived balances and canonical status fields only.

5. Error contract

All adapter methods return standard failure modes:

  • retryable: true for transient provider errors (timeouts, 5xx, queue backpressure).
  • retryable: false for invalid payloads, invalid signatures, and authorization failures.
  • errorCode must be stable across retries for auditability.

6. Test coverage required

  • Contract tests per adapter:
    • createPayInIntent, status polling, webhook handling
    • invalid/absent signature behavior
    • duplicate webhook idempotency
    • unknown payment reference behavior
    • rollback selection and read-only mode behavior.
  • Reconciliation tests:
    • provider backfill for missing payment references,
    • status drift correction,
    • duplicate/missing event merge.