feat(bank): multi-bank chequera — required bankAccountId, per-account scoping
The office keeps more than one operating account (Utilities banks in MXN, Seguros in USD), but bank_transactions was a single implicit MXN register by design. Adds Bank/BankAccount and makes every read and write in the module scoped to exactly one account. Schema: - Bank / BankAccount. Currency is fixed per account and BankTransaction has no currency column of its own — a movement inherits its account's, the way a real bank account doesn't mix currencies. - BankTransaction.bankAccountId, required. A movement with no known account isn't reconcilable against a statement. - @@index([bankAccountId, transactionDate]): every read now filters by account and orders/groups by date. Migration: - backfill_bank_accounts.py seeds Scotiabank + "Utilities — Scotiabank (MXN)" and backfills all 22,669 existing rows onto it, then promotes the column to NOT NULL and attaches the FK. Standalone because prisma db push cannot add a required column to a populated table. Idempotent; re-running once a second account exists does not re-point rows. - run_all.py runs it (both modes) before transform_bank.py, which now resolves the account by label and fails fast if it is missing. API: - ?bankAccountId= required on list/stats/facets/summary — not optional with an "all accounts" default, since summing an MXN and a USD register repeats the currency-collapsing mistake the billing module exists to prevent. Missing is 400, unknown is 404. - facets() had no account clause at all and summary() has two raw-SQL rollups; all three are now parameterised. Scoping only one of summary's queries would leave the year list and its drill-down describing different books. - New bank/accounts + bank/banks sub-resource under a MANAGER bank:manage-accounts ability. currency is absent from the update DTO: booked movements are denominated in it, so editing would re-denominate history. Capture into a closed account is rejected. Web: - /banco gains an account picker (remembered per browser) and reads every figure in the selected account's currency; the "single currency (MXN)" doc-comment and the hardcoded MXN formatting are gone. - New /banco/cuentas for banks and accounts. Accounts are closed, never deleted — the FK is required, so deleting one would destroy its register. - /inicio's chequera card names the account it is reading instead of implying a single register. Verified against dev + browser: a second USD account showed full read/write isolation from the MXN register, whose totals were unchanged (22,669 movements, net 1,014,266.97). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -401,6 +401,40 @@ document-understanding problem. Recommend:
|
||||
|
||||
## 3. Multi-bank chequera
|
||||
|
||||
> **BUILT — 2026-07-27.** Everything below is implemented and verified against
|
||||
> the dev database and browser. `Bank` / `BankAccount` exist, every
|
||||
> `BankTransaction` carries a required `bankAccountId`, and all 22,669 migrated
|
||||
> rows were backfilled onto the Utilities/Scotiabank MXN account by
|
||||
> `migration/backfill_bank_accounts.py` (now wired into `run_all.py`, both
|
||||
> modes, ahead of `transform_bank.py`). Every read path in `bank.service.ts` is
|
||||
> account-scoped — including both raw-SQL rollups in `summary()` and the
|
||||
> previously-unfiltered `facets()`. `/banco` gained an account picker,
|
||||
> `/banco/cuentas` manages banks and accounts under the new MANAGER
|
||||
> `bank:manage-accounts` ability, and `/inicio`'s chequera card now names the
|
||||
> account it is reading rather than implying a single register.
|
||||
>
|
||||
> **Verified end to end:** a second account (USD) was created through the API,
|
||||
> a movement captured into it, and the MXN register's totals confirmed
|
||||
> unchanged (22,669 movements, net 1,014,266.97) with zero cross-account leak
|
||||
> in list/stats/facets/summary. Missing `bankAccountId` returns 400, unknown
|
||||
> returns 404, capture into a closed account returns 400, and an attempt to
|
||||
> PATCH an account's `currency` is rejected by DTO whitelisting. The test
|
||||
> account was then deleted — the real Seguros bank is still the open question
|
||||
> below, so nothing was left behind guessing at it.
|
||||
>
|
||||
> **Two deviations from the design below**, both tightening it:
|
||||
> - `bank_transactions` also gained an `@@index([bankAccountId, transactionDate])`.
|
||||
> Every read is now filtered by account and ordered/grouped by date; without
|
||||
> it each of them is a full scan of the 22k-row table.
|
||||
> - `UpdateBankAccountDto` deliberately has **no `currency` field**. The
|
||||
> movements already booked in an account are denominated in it, so editing it
|
||||
> would silently re-denominate history instead of converting it. Currency is
|
||||
> set once, at creation.
|
||||
>
|
||||
> Still open: which bank the Seguros USD account is actually at (see Open
|
||||
> questions). Until that answer arrives the office has exactly one chequera and
|
||||
> the UI behaves as it always did, just scoped explicitly.
|
||||
|
||||
### Motivation
|
||||
|
||||
Seguros uses a US bank account; Utilities uses a Mexican bank account. The
|
||||
|
||||
Reference in New Issue
Block a user