import { BankOperationKind } from './bank-operation-kind'; export function moscowToday(now = new Date()): string { return new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Moscow', year: 'numeric', month: '2-digit', day: '2-digit', }).format(now); } export function isCalendarDate(value: string): boolean { if (value.length !== 10 || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false; if (value.startsWith('0000')) return false; const date = new Date(`${value}T00:00:00.000Z`); return ( !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value ); } export function bankBalance( cardId: string, opening: bigint, operations: Array<{ kind: string; cardId: string; targetCardId: string | null; amountKopecks: bigint; }>, ): bigint { // ponytail: O(N) history fold; optimize only after measuring growing histories. let balance = opening; for (const operation of operations) { if (operation.cardId === cardId) { if ( operation.kind === BankOperationKind.Expense || operation.kind === BankOperationKind.Transfer ) balance -= operation.amountKopecks; else if ( operation.kind === BankOperationKind.Topup || operation.kind === BankOperationKind.Refund ) balance += operation.amountKopecks; } if ( operation.kind === BankOperationKind.Transfer && operation.targetCardId === cardId ) balance += operation.amountKopecks; } return balance; } export function bankSummary( period: { from: string; to: string }, operations: Array<{ kind: string; date: string; amountKopecks: bigint; categoryId: string | null; }>, ) { let expenses = 0n; let refunds = 0n; const categories = new Map< string, { categoryId: string; expenses: bigint; refunds: bigint; net: bigint } >(); // ponytail: O(N) history fold; optimize only after measuring growing histories. for (const operation of operations) { if (operation.date < period.from || operation.date > period.to) continue; if ( operation.kind !== BankOperationKind.Expense && operation.kind !== BankOperationKind.Refund ) continue; const expense = operation.kind === BankOperationKind.Expense ? operation.amountKopecks : 0n; const refund = operation.kind === BankOperationKind.Refund ? operation.amountKopecks : 0n; expenses += expense; refunds += refund; if (operation.categoryId !== null) { const category = categories.get(operation.categoryId) ?? { categoryId: operation.categoryId, expenses: 0n, refunds: 0n, net: 0n, }; category.expenses += expense; category.refunds += refund; category.net = category.expenses - category.refunds; categories.set(category.categoryId, category); } } return { expenses, refunds, net: expenses - refunds, categories: [...categories.values()], }; }