first commit
This commit is contained in:
119
docs/money/payment/payment-providers/src/registry/define.md
Normal file
119
docs/money/payment/payment-providers/src/registry/define.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# `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
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user