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:111instantiated the route tree against a fake{} as PluginContextcast at module-load time so routes never actually received the live context.- The
BankingServiceand repositories carriedas anycasts on the database adapter because the upstream@suaveplan/apiDatabaseAdaptershape was untyped. - Routes were defined with the upstream
GET/POST/successResponsebuilder which the genesis@suaveplan/apidoes not export, so a direct port is impossible — the migration retypes against the canonical@hono/zod-openapicreateRoute+OpenAPIHonosurface 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 typedPluginDescriptor<BankingPluginConfig>withname: "banking",dependencies: ["database"],optionalDependencies: ["auth", "audit", "rate-limit"], three lifecycle hooks (onInit/onReady/onClose), and the configuredroutePrefixmount. - ADDED:
BankingPluginOptionsSchema/BankingPluginOptionsInput/BankingPluginOptionsResolved/BankingPluginConfig— seven defaulted options (enableDefaultBank,defaultBankName,defaultSortCode,maxAccountsPerUser,enableTransfers,enableOverdraft,routePrefix). - ADDED:
BankingServicecoordinator with typedcreateBank,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 matchingz.infer<>aliases. - ADDED: 14
BANKING.*error codes viadefineCodes("BANKING", ...)— every code thrown by ≥1 production code path. - ADDED: 6 OTel instruments under the
@suaveplan/api-plugin-bankingmeter (banks_total,accounts_total,transfers_total,transfer_amounthistogram,fund_operations_total,query_duration_mshistogram). - ADDED:
createBankingRoutes(deps)— 19 OpenAPIHono routes (/banks/*+/accounts/*) includingPOST /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
BankingEventMapand event emission on every state transition (created / updated / soft-deleted / status-changed / transfer-succeeded / transfer-failed). - ADDED:
bankingService,app,dbslots on the canonicalServiceRegistryvia module augmentation. - ADDED:
decommissionBankingTenant(service, tenantId)helper for hosts orchestrating tenant lifecycle externally. - ADDED:
surql/001-bank-and-account.surqlmigration file carrying the upstream schema, sort-code function library, account-number arithmetic functions, and theuser_accountrelation.
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 inenterprise/api-plugin-banking.md §4but 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
PluginDescriptoritself — the current@suaveplan/api-pluginPluginDescriptorshape does not declareonTenantDelete. The migration exposes adecommissionBankingTenanthelper 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 |