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

11 KiB

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