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>
48 lines
1.7 KiB
TypeScript
48 lines
1.7 KiB
TypeScript
import {
|
|
IsBoolean,
|
|
IsIn,
|
|
IsOptional,
|
|
IsString,
|
|
MinLength,
|
|
} from "class-validator";
|
|
|
|
/** Mirrors the Prisma `Currency` enum; a chequera's is fixed at creation. */
|
|
export const BANK_CURRENCIES = ["MXN", "USD"] as const;
|
|
export type BankAccountCurrency = (typeof BANK_CURRENCIES)[number];
|
|
|
|
/** Mirrors `TransactionDomain`. A soft hint on the account, never enforced. */
|
|
export const BANK_BUSINESS_LINES = ["UTILITY", "INSURANCE", "TRUST"] as const;
|
|
export type BankBusinessLine = (typeof BANK_BUSINESS_LINES)[number];
|
|
|
|
export class CreateBankDto {
|
|
@IsString() @MinLength(1) name!: string;
|
|
/** "MX" | "US" — free text, informational only. */
|
|
@IsOptional() @IsString() country?: string;
|
|
}
|
|
|
|
export class UpdateBankDto {
|
|
@IsOptional() @IsString() @MinLength(1) name?: string;
|
|
@IsOptional() @IsString() country?: string;
|
|
}
|
|
|
|
export class CreateBankAccountDto {
|
|
@IsString() @MinLength(1) bankId!: string;
|
|
@IsString() @MinLength(1) label!: string;
|
|
/**
|
|
* Immutable after creation (no field for it on the update DTO): every
|
|
* movement already booked into the account is denominated in it, so
|
|
* changing it would silently re-denominate history.
|
|
*/
|
|
@IsIn(BANK_CURRENCIES) currency!: BankAccountCurrency;
|
|
@IsOptional() @IsIn(BANK_BUSINESS_LINES) businessLine?: BankBusinessLine;
|
|
@IsOptional() @IsBoolean() active?: boolean;
|
|
}
|
|
|
|
export class UpdateBankAccountDto {
|
|
@IsOptional() @IsString() @MinLength(1) bankId?: string;
|
|
@IsOptional() @IsString() @MinLength(1) label?: string;
|
|
@IsOptional() @IsIn(BANK_BUSINESS_LINES) businessLine?: BankBusinessLine;
|
|
/** Closing an account hides it from the picker; its movements stay readable. */
|
|
@IsOptional() @IsBoolean() active?: boolean;
|
|
}
|