Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
# 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<BankingPluginConfig>` 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 |