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

4.0 KiB

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

import { createBankingPlugin } from "@suaveplan/api-plugin-banking";

const plugin = createBankingPlugin();
registry.register(plugin);

Usage: Advanced

Customise the configuration:

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:

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.