# Bank repository ## Purpose `BankRepository` owns the SurrealDB `bank` table. It extends `BaseRepository` and exposes the bank-specific read/write surface that the plugin's service layer and HTTP routes consume: typed `create` and `update`, lookups by name, site, sort code, status; `softDelete` (sets status = 1); `hasActiveAccounts` (used as the precondition for `softDelete`); a case-insensitive `searchByName`; and the idempotent `getOrCreateDefaultBank` that the plugin's `onReady` hook calls during bootstrap. ## Features - Typed `create(data: CreateBank)` / `update(id, data: UpdateBank)` with site-id normalisation and field-whitelist enforcement. - Sort-code lookup gates on the `XX-XX-XX` regex and throws `BankingError(INVALID_SORT_CODE)` for malformed input — never silently passes through to the SurQL planner. - `hasActiveAccounts` reads the `bank_account` table via parameterised SurQL so the soft-delete precondition is enforced inside the same request. - OTel `banking.banks_total{op}` counter emitted on every successful create/update/soft-delete so observability is on the hot path. - `deleteByTenant` returns the hard-deleted row count so the plugin's `onTenantDelete` hook can log the cleanup volume. ## Usage: Basic ```ts import { BankRepository } from "@suaveplan/api-plugin-banking"; const repo = new BankRepository(adapter); const bank = await repo.create({ name: "Genesis Bank", sort_code: 234567 }); await repo.softDelete(bank.id); ``` ## Usage: Advanced Bootstrap the configured default bank in a lifecycle hook: ```ts import { BankRepository } from "@suaveplan/api-plugin-banking"; async function bootstrap(repo: BankRepository) { const bank = await repo.getOrCreateDefaultBank("Test Bank", 123456); return bank; } ``` Sort-code-driven lookup: ```ts const found = await repo.getBySortCode("12-34-56"); // throws BankingError(INVALID_SORT_CODE) if format is wrong ``` Tenant cleanup (used by the plugin's `onTenantDelete` hook): ```ts const count = await repo.deleteByTenant("site:abcdef"); logger.info({ count }, "banks purged for tenant"); ``` ## API Reference ### `class BankRepository extends BaseRepository` Constructor: `new BankRepository(db: DatabaseAdapter)`. Inherits everything from `BaseRepository` and adds: - `create(data: CreateBank): Promise` - `update(id, data: UpdateBank): Promise` (typed override) - `existsByName(name, siteId?): Promise` - `getByName(name): Promise` - `getBySiteId(siteId): Promise` - `getBySortCode(sortCode: string): Promise` — formatted `XX-XX-XX` input - `getByStatus(status, limit?, offset?): Promise` - `getActiveBanks(limit?, offset?): Promise` - `getAvailableBanks(limit?, offset?): Promise` - `softDelete(id): Promise` - `hasActiveAccounts(bankId): Promise` - `searchByName(searchTerm, limit?): Promise` - `getOrCreateDefaultBank(name, sortCode): Promise` - `deleteByTenant(tenantId): Promise` — count of deleted rows Static helper: `BankRepository.extractBareId(id): string`. ## Implementation Notes The sort-code field is stored as `int` in SurrealDB (matching the upstream `surql/tables/bank-account.surql` schema), so the wire-format `XX-XX-XX` string is parsed to a number at every lookup site. The regex guard rejects partial / malformed strings up front; a stray `12-345-6` would otherwise `parseInt` to `12345` and silently return the wrong bank. `getOrCreateDefaultBank` is idempotent by design — the plugin's `onReady` runs it on every host start, and consumers that run multi-host deployments rely on the "look-up-first, then insert" pattern to avoid sort-code collisions. This is not a race-free guarantee (two hosts booting at exactly the same time can both observe missing → both insert), but the underlying SurQL `INSERT` will reject on the unique index in that case, falling back to the lookup-then-fetch path. `hasActiveAccounts` queries `bank_account` directly rather than going through `BankAccountRepository.findBy(...)` to avoid the constructor dependency between repositories — both can be instantiated independently with only the shared `DatabaseAdapter`.