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,112 @@
# Bank repository
## Purpose
`BankRepository` owns the SurrealDB `bank` table. It extends
`BaseRepository<BankRecord>` 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<BankRecord>`
Constructor: `new BankRepository(db: DatabaseAdapter)`.
Inherits everything from `BaseRepository` and adds:
- `create(data: CreateBank): Promise<BankRecord>`
- `update(id, data: UpdateBank): Promise<BankRecord | null>` (typed
override)
- `existsByName(name, siteId?): Promise<boolean>`
- `getByName(name): Promise<BankRecord | null>`
- `getBySiteId(siteId): Promise<BankRecord[]>`
- `getBySortCode(sortCode: string): Promise<BankRecord | null>` —
formatted `XX-XX-XX` input
- `getByStatus(status, limit?, offset?): Promise<BankRecord[]>`
- `getActiveBanks(limit?, offset?): Promise<BankRecord[]>`
- `getAvailableBanks(limit?, offset?): Promise<BankRecord[]>`
- `softDelete(id): Promise<BankRecord | null>`
- `hasActiveAccounts(bankId): Promise<boolean>`
- `searchByName(searchTerm, limit?): Promise<BankRecord[]>`
- `getOrCreateDefaultBank(name, sortCode): Promise<BankRecord>`
- `deleteByTenant(tenantId): Promise<number>` — 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`.