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:
@@ -510,10 +510,46 @@ model BusinessLineCategory {
|
||||
@@map("business_line_categories")
|
||||
}
|
||||
|
||||
/// The institution a chequera is held at. Purely a grouping label for the
|
||||
/// accounts under it — no money hangs off a Bank directly.
|
||||
model Bank {
|
||||
id String @id @default(uuid())
|
||||
name String @unique
|
||||
/// "MX" | "US" — informational, used only to label the account picker.
|
||||
country String?
|
||||
accounts BankAccount[]
|
||||
|
||||
@@map("banks")
|
||||
}
|
||||
|
||||
/// One physical chequera. Currency is fixed per account, because a real bank
|
||||
/// account is: there is deliberately NO currency column on BankTransaction, a
|
||||
/// movement inherits its account's. This is what keeps the MXN (Utilities /
|
||||
/// Scotiabank) and USD (Seguros) registers from ever being summed together,
|
||||
/// the same rule the customer ledger follows per currency.
|
||||
model BankAccount {
|
||||
id String @id @default(uuid())
|
||||
bankId String
|
||||
bank Bank @relation(fields: [bankId], references: [id])
|
||||
/// Staff-facing name, e.g. "Utilities — Scotiabank (MXN)".
|
||||
label String
|
||||
currency Currency
|
||||
/// Hint only, never enforced — one chequera can pay for more than one line.
|
||||
businessLine TransactionDomain?
|
||||
active Boolean @default(true)
|
||||
movements BankTransaction[]
|
||||
|
||||
@@map("bank_accounts")
|
||||
}
|
||||
|
||||
/// Unifies SCOTHIA's DATOS E (egresos) / DATOS I (ingresos) into one
|
||||
/// signed-amount table: income positive, expense negative.
|
||||
model BankTransaction {
|
||||
id String @id @default(uuid())
|
||||
// Required: a movement with no known account isn't reconcilable against a
|
||||
// statement. Every migrated row is SCOTHIA = the Utilities MXN account.
|
||||
bankAccountId String
|
||||
bankAccount BankAccount @relation(fields: [bankAccountId], references: [id])
|
||||
transactionDate DateTime
|
||||
transactionType String?
|
||||
reference String?
|
||||
@@ -531,7 +567,10 @@ model BankTransaction {
|
||||
legacySourceTable String?
|
||||
legacyId String?
|
||||
|
||||
// Provenance stays globally unique: every legacy row belongs to the one
|
||||
// Scotiabank account, so adding accounts never collides here.
|
||||
@@unique([legacySourceTable, legacyId])
|
||||
@@index([bankAccountId, transactionDate])
|
||||
@@map("bank_transactions")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user