Files
2026-05-27 23:29:53 +01:00

117 lines
4.0 KiB
Markdown

# Banking plugin factory
## Purpose
`createBankingPlugin(options?)` is the package's primary entry point —
it Zod-validates the supplied options, builds the `BankingService`
inside the descriptor's `onInit`, bootstraps the default bank inside
`onReady`, mounts the OpenAPIHono route tree on the host `app` under
the configured `routePrefix`, and drops every cached reference inside
`onClose`. The descriptor is shaped against the canonical
`@suaveplan/api-plugin` `PluginDescriptor` so any host that consumes
that contract can register the banking plugin without bespoke wiring.
## Features
- Zod boundary at `createBankingPlugin(options)` — invalid input
rejects with `BankingError(BANKING.INVALID_OPTIONS)` carrying the
`ZodError` as `cause`.
- Declared dependency on the `database` plugin so the topological
loader runs the database plugin first and the `db` slot is
populated before `onInit` resolves it.
- Optional dependencies on `auth`, `audit`, `rate-limit` so a host
that registers those plugins gets them loaded before banking
(matching the enterprise audit's future-work additions); when
absent, the plugin still works (the optional dep contract).
- Idempotent `initializeDefaultBank` invoked from `onReady` — safe on
every host start.
- `decommissionBankingTenant(service, tenantId)` helper exported for
hosts that orchestrate tenant lifecycle externally (since the
current `PluginDescriptor` does not yet declare tenant hooks).
## Usage: Basic
```ts
import { createBankingPlugin } from "@suaveplan/api-plugin-banking";
const plugin = createBankingPlugin();
registry.register(plugin);
```
## Usage: Advanced
Customise the configuration:
```ts
import { createBankingPlugin } from "@suaveplan/api-plugin-banking";
const plugin = createBankingPlugin({
routePrefix: "/api/v1/banking",
maxAccountsPerUser: 25,
enableOverdraft: true,
enableTransfers: true,
defaultBankName: "Acme Trust",
defaultSortCode: 234567,
});
await registry.register(plugin);
await registry.initAll();
await registry.readyAll();
```
Tenant decommission:
```ts
import {
decommissionBankingTenant,
type BankingService,
} from "@suaveplan/api-plugin-banking";
const service = ctx.getService("bankingService") as BankingService;
const { banks, accounts } = await decommissionBankingTenant(
service,
tenantId
);
```
## API Reference
### `createBankingPlugin(options?: BankingPluginOptionsInput): PluginDescriptor<BankingPluginConfig>`
Returns a frozen descriptor. The descriptor exposes:
- `name: "banking"`
- `version: "0.1.0"`
- `dependencies: ["database"]`
- `optionalDependencies: ["auth", "audit", "rate-limit"]`
- `tags: ["banking", "accounts", "finance"]`
- `defaults`: the parsed `BankingPluginConfig`
- `onInit`, `onReady`, `onClose`
### `decommissionBankingTenant(service, tenantId): Promise<{ banks; accounts }>`
Convenience wrapper that delegates to
`BankingService.deleteTenantData`. Returns the count of soft-deleted
banks and hard-deleted accounts. The deletes are not atomic — if the
underlying SurQL throws between the two, `BANKING.TENANT_CLEANUP_FAILED`
surfaces with the partial state visible to the next call.
## Implementation Notes
The plugin uses `dependencies: ["database"]`, not
`optionalDependencies` — banking is meaningless without a database,
and surfacing a typed `BANKING.SERVICE_NOT_AVAILABLE` for the missing-slot
case at `onInit` time is the canonical genesis pattern (instead of
silently no-oping).
The `emit` helper carries a typed-payload signature constrained by
`BankingEventMap`. It runtime-casts to the loose `EventBusLike.emit`
shape because `@suaveplan/types`' `EventBusLike` is intentionally
untyped — strong typing lives on the per-plugin event map, not the
bus. The cast is at one place and narrow.
`onTenantCreate` is intentionally absent because the upstream
package's version was a no-op stub. Hosts that need per-tenant
default-bank provisioning call `service.createBank({ ... site_id })`
from their own tenant lifecycle hook.