Files
skills/docs/money/payment/payment-providers/src/registry/define.md
2026-05-29 08:20:56 +01:00

4.4 KiB

definePaymentProvider

Purpose

Factory helper for sibling @suaveplan/payment-provider-* packages that want to expose a PaymentProvider (from @suaveplan/payment) without authoring a full class. The factory returns a frozen object that satisfies the structural PaymentProvider contract and carries the caller-supplied method references verbatim — closures are preserved, no this rebinding happens. Sibling packages MAY use this factory or return a class instance from their new XProvider(...) constructor; both register identically into {@link ProviderRegistry}. The factory exists so providers without per-instance state (deterministic test providers, in-memory mocks, thin wrappers around an HTTP closure) can skip the class boilerplate.

Features

  • Literal-typed name — the result narrows to PaymentProvider & { readonly name: TName } so consumers can discriminate at the type level (provider.name === "stripe" narrows the rest of the object)
  • Frozen output — accidental mutation downstream throws in strict mode
  • Method-identity preservation — the function passed in is the function dispatched at call time (useful for spies / instrumentation that identify methods by reference)
  • Zero runtime cost beyond a single Object.freeze
  • Universal — no Node, browser, or React imports

Usage: Basic

import { definePaymentProvider, ProviderRegistry } from "@suaveplan/payment-providers";

const stub = definePaymentProvider({
    name: "stub",
    createIntent: async (input) => ({
        id: "intent_1",
        provider: "stub",
        amount: input.amount,
        currency: input.amount.currency,
        status: "created",
        captureMethod: input.captureMethod ?? "automatic",
        refunds: [],
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
    }),
    confirmIntent: async () => { throw new Error("not used"); },
    captureIntent: async () => { throw new Error("not used"); },
    cancelIntent: async () => { throw new Error("not used"); },
    refund: async () => { throw new Error("not used"); },
    getIntent: async () => { throw new Error("not used"); },
});

const registry = new ProviderRegistry();
registry.register(stub);

Usage: Advanced

import {
    definePaymentProvider,
    ProviderRegistry,
} from "@suaveplan/payment-providers";

// A deterministic test provider that records every call by reference
// — note that `recordedCreate` IS the function exposed on
// `provider.createIntent`, so spies / assertions can identify it by
// equality.
const calls: string[] = [];
const recordedCreate = async (input: never): Promise<never> => {
    calls.push("create");
    void input;
    throw new Error("test-only stub");
};

const provider = definePaymentProvider({
    name: "test",
    createIntent: recordedCreate,
    confirmIntent: recordedCreate,
    captureIntent: recordedCreate,
    cancelIntent: recordedCreate,
    refund: recordedCreate,
    getIntent: recordedCreate,
});

// Type narrowing on the literal name.
if (provider.name === "test") {
    // `provider` here is narrowed to `{ name: "test", ... }`
}

// The factory result is frozen.
expect(Object.isFrozen(provider)).toBe(true);
expect(provider.createIntent).toBe(recordedCreate);

const registry = new ProviderRegistry();
registry.register(provider);
expect(registry.get("test")).toBe(provider);

API Reference

Symbol Kind Signature
PaymentProviderSpec<TName> interface { name: TName; createIntent; confirmIntent; captureIntent; cancelIntent; refund; getIntent } — every method type matches the PaymentProvider interface from @suaveplan/payment
definePaymentProvider<TName>(spec) function returns PaymentProvider & { readonly name: TName }, frozen

Implementation Notes

definePaymentProvider is intentionally minimal — it does not register the result anywhere, does not wrap methods in instrumentation, and does not perform validation beyond TypeScript's structural checks. Validation belongs to the layer that decides what to do with a provider (the ProviderRegistry, the PaymentEngine, etc.); the factory's only job is to package a spec into a frozen, literal-typed PaymentProvider. The Object.freeze call freezes the top-level object only; the methods themselves remain mutable function objects so sibling packages can attach their own metadata if needed.