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 toPaymentProvider & { 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.