# Add `@suaveplan/api-plugin-banking` **Package**: `@suaveplan/api-plugin-banking` **Status**: PROPOSED **Type**: new-package **Spec delta**: ./specs/api-plugin-banking/spec.md ## Why The genesis api-plugin chain needs a banking-foundation plugin that owns the canonical `bank` (institution) and `bank_account` SurrealDB tables, exposes a coordinator service for the typed read / write surface, and ships the HTTP route tree consumers depend on. The downstream `@suaveplan/api-plugin-bank` (game-wallet specialisation) is being migrated by a peer agent in parallel and depends on this package — ship as fast as quality allows. The upstream package at `/mnt/s/web/suaveplan/packages/server/api-plugin-banking/` shipped working code but failed the genesis quality bar in three ways that the audit document `enterprise/api-plugin-banking.md` calls out: - `src/index.ts:111` instantiated the route tree against a fake `{} as PluginContext` cast at module-load time so routes never actually received the live context. - The `BankingService` and repositories carried `as any` casts on the database adapter because the upstream `@suaveplan/api` `DatabaseAdapter` shape was untyped. - Routes were defined with the upstream `GET/POST/successResponse` builder which the genesis `@suaveplan/api` does not export, so a direct port is impossible — the migration retypes against the canonical `@hono/zod-openapi` `createRoute` + `OpenAPIHono` surface used by every other genesis api-plugin (search, audit, webhook, ...). The migration brings the full functional surface across, retypes against the genesis `DatabaseAdapter` from `@suaveplan/db`, adds the fourteen `BANKING.*` error codes, six OTel instruments, a typed event map, a Zod options boundary, and 100% istanbul coverage. ## What changes - ADDED: `createBankingPlugin(options?)` — Zod-validated factory returning a typed `PluginDescriptor` with `name: "banking"`, `dependencies: ["database"]`, `optionalDependencies: ["auth", "audit", "rate-limit"]`, three lifecycle hooks (`onInit`/`onReady`/`onClose`), and the configured `routePrefix` mount. - ADDED: `BankingPluginOptionsSchema` / `BankingPluginOptionsInput` / `BankingPluginOptionsResolved` / `BankingPluginConfig` — seven defaulted options (`enableDefaultBank`, `defaultBankName`, `defaultSortCode`, `maxAccountsPerUser`, `enableTransfers`, `enableOverdraft`, `routePrefix`). - ADDED: `BankingService` coordinator with typed `createBank`, `updateBank`, `softDeleteBank`, `createAccount`, `updateAccount`, `updateAccountStatus`, `transferFunds`, `addFunds`, `subtractFunds`, `initializeDefaultBank`, `deleteTenantData`, and repository accessors. - ADDED: `BaseRepository`, `BankRepository`, `BankAccountRepository` — SurrealDB-backed CRUD with field-whitelist enforcement, RecordId flattening, and tenant-cleanup helpers. - ADDED: 11 Zod schemas (`BankSchema`, `BankAccountSchema`, `CreateBank{,Account}Schema`, `UpdateBank{,Account}Schema`, `TransferFundsSchema`, `FundsOperationSchema`, `UpdateAccountStatusSchema`, `SortCodeSchema`, literal-union status / type enums) plus matching `z.infer<>` aliases. - ADDED: 14 `BANKING.*` error codes via `defineCodes("BANKING", ...)` — every code thrown by ≥1 production code path. - ADDED: 6 OTel instruments under the `@suaveplan/api-plugin-banking` meter (`banks_total`, `accounts_total`, `transfers_total`, `transfer_amount` histogram, `fund_operations_total`, `query_duration_ms` histogram). - ADDED: `createBankingRoutes(deps)` — 19 OpenAPIHono routes (`/banks/*` + `/accounts/*`) including `POST /accounts/transfer`, `POST /accounts/:id/{add,subtract}-funds`, `PATCH /accounts/:id/status`, `GET /banks/sort-code/:sortCode`, `GET /banks/search/:query`, `GET /accounts/by-number/:n`, `GET /accounts/user/:userId`, `GET /accounts/:id/owner`. - ADDED: Typed `BankingEventMap` and event emission on every state transition (created / updated / soft-deleted / status-changed / transfer-succeeded / transfer-failed). - ADDED: `bankingService`, `app`, `db` slots on the canonical `ServiceRegistry` via module augmentation. - ADDED: `decommissionBankingTenant(service, tenantId)` helper for hosts orchestrating tenant lifecycle externally. - ADDED: `surql/001-bank-and-account.surql` migration file carrying the upstream schema, sort-code function library, account-number arithmetic functions, and the `user_account` relation. ## Out of scope - Auth / rate-limit / audit-log integration — the upstream plugin did not gate routes, and the enterprise audit document flags these as future-work additions. The optional-dependencies list signals the topo-sort order so a follow-up change can wire them without re-ordering. - Multi-currency accounts via `@suaveplan/money` / `@suaveplan/currency` — flagged as feature-rich enhancement in `enterprise/api-plugin-banking.md §4` but not present in the upstream source. Deferred to a dedicated change. - Double-entry ledger model — the upstream stores raw balance; the enhancement flag is preserved for a follow-up. - Scheduled / standing transfers, reconciliation endpoint, statement-PDF generation — listed in §4 enhancements only. - Decommissioned tenant hook on the `PluginDescriptor` itself — the current `@suaveplan/api-plugin` `PluginDescriptor` shape does not declare `onTenantDelete`. The migration exposes a `decommissionBankingTenant` helper that hosts invoke from their own tenant orchestrator. ## Source mapping This change derives from `../suaveplan/enterprise/api-plugin-banking.md` and the upstream source at `../suaveplan/packages/server/api-plugin-banking/src/`. | Requirement | Source lines | Notes | |---|---|---| | Plugin factory with `name: "banking"`, `version`, `description`, `dependencies: ["database"]` | `src/index.ts` L86-L96 (`createBankingPlugin`) | retyped against genesis `PluginDescriptor` | | `BankingConfigSchema` (`enableDefaultBank`, `defaultBankName`, `defaultSortCode`, `maxAccountsPerUser`, `enableTransfers`, `enableOverdraft`) | `src/index.ts` L19-L28 | renamed to `BankingPluginOptionsSchema`; added `routePrefix` (genesis convention) | | `BankingService` coordinator | `src/index.ts` L34-L64 | moved into `src/service/service.ts` per `enterprise §3` "move out of `src/index.ts`" | | `initializeDefaultBank` idempotent bootstrap | `src/index.ts` L54-L58 | preserved + made config-driven via name + sort code | | `validateUserAccountLimit` → `countByUserId` | `src/index.ts` L60-L63 | renamed + moved into repository; service enforces via `ACCOUNT_LIMIT_REACHED` | | `onInit` registers `bankingService`, `bankRepository`, `bankAccountRepository` | `src/index.ts` L114-L135 | trimmed to single `bankingService` slot; repositories are accessible via `service.getBankRepository()` | | `onReady` bootstraps default bank | `src/index.ts` L137-L143 | preserved; emits `banking:ready` typed event | | `onClose` drops service | `src/index.ts` L145-L147 | preserved | | `onTenantDelete` purges banks + accounts | `src/index.ts` L154-L168 | preserved as `service.deleteTenantData(tenantId)` + `decommissionBankingTenant` helper (descriptor hook not yet exposed in genesis `PluginDescriptor`) | | Re-export of `@suaveplan/api-schemas/banking` | `src/index.ts` L178 | REMOVED per `enterprise §3` ("violates NEVER re-export external deps") and CLAUDE.md rule 4; banking schemas now owned locally | | `BankRepository` full surface (create, existsByName, getByName, getBySiteId, getBySortCode, getByStatus, getActiveBanks, getAvailableBanks, softDelete, hasActiveAccounts, searchByName, getOrCreateDefaultBank) | `src/repositories/bank.repository/index.ts` L23-L209 | preserved with field-whitelist tightened; default-bank takes config args | | `BankAccountRepository` full surface | `src/repositories/bank-account.repository/index.ts` L47-L364 | preserved; fund-movement primitives retyped against `DatabaseAdapter`; overdraft tolerance threaded through | | `BaseRepository` shared scaffold + field whitelist | `src/repositories/base.repository.ts` L40-L266 | retyped against genesis `@suaveplan/db` `DatabaseAdapter`; parameter style switched to `$p0,$p1` positional via `db.query(sql, [...])` | | `RecordWithId`, `RecordId`, `RepositoryResponse`, `ListResponse` | `src/repositories/base.repository.types.ts` L1-L52 | folded into `src/types/types.ts` and `src/schemas/schemas.ts`; `ListResponse` dropped as unused on the genesis surface | | Bank routes (`GET /`, `POST /`, `GET /:id`, `PATCH /:id`, `DELETE /:id`, `GET /sort-code/:sortCode`, `GET /search/:query`) | `src/routes/banks/index.ts` L25-L336 | retyped against `@hono/zod-openapi` `createRoute`; every status declared in `responses` | | Account routes (list, get, by-user, by-number, create, update, delete, transfer, add-funds, subtract-funds, status, owner) | `src/routes/accounts/index.ts` L29-L537 | preserved verbatim semantics; routes consume `BankingService` instead of constructing repos per-request | | `banking:initialized`, `banking:ready` event extension | `src/types.ts` L20-L25 | extended to full `BankingEventMap` with per-state-transition payloads | | `surql/tables/bank-account.surql` (bank + bank_account schemas, function library, account-number arithmetic) | upstream `surql/tables/bank-account.surql` L1-L345 | copied verbatim to `surql/001-bank-and-account.surql` | | Fix `{} as PluginContext` cast at route mount | `enterprise §3` first bullet | routes now mount inside `onReady(ctx)` with real `ctx` | | Remove `as any` casts on `db` | `enterprise §3` second bullet | replaced with typed `DatabaseAdapter` from `@suaveplan/db` | | Move `BankingService` out of `src/index.ts` | `enterprise §3` third bullet | service lives in `src/service/service.ts` | | Drop re-export of `@suaveplan/api-schemas/banking` | `enterprise §3` fourth bullet | schemas owned locally per CLAUDE.md rule 4 | | Add `events.ts` for `EventMap` declaration | `enterprise §3` fifth bullet | replaced by `src/types/types.ts` `BankingEventMap` + typed `emitEvent` in routes | ### Requirements with no source line | Requirement | Justification | |---|---| | 100% istanbul test coverage gate | Genesis-wide policy, not package-specific | | `defineCodes("BANKING", [...])` error registry of 14 codes | Genesis-wide policy — upstream threw raw `new Error()` | | 6 OTel instruments emitted from production paths | Genesis-wide policy — upstream had no observability | | Module-augmentation of `ServiceRegistry` | Genesis convention — upstream had untyped `getService("db")` | | Co-located sibling `.md` files (≥200 words) | Genesis CLAUDE.md doc-coverage rule | | Test files import only from `@suaveplan/testing/runner` | CLAUDE.md rule 11 | | `enableOverdraft` + `enableTransfers` + `routePrefix` | `routePrefix` is genesis convention; `enable*` flags were upstream but `routePrefix` was hardcoded `/banking` upstream — moved to schema for multi-version deploys | | Account-number arithmetic (`parseAccountNumber`, `splitAccountNumber`) raises `INVALID_ACCOUNT_NUMBER` on malformed input | Upstream returned `null` silently; genesis throws so callers cannot ignore malformed input |