first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View 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.