import { ISO_DATE_LENGTH, ISO_DATE_START } from '../../../../common/time'; import { BankOperationKind } from './bank-operation-kind'; import { ZERO_KOPECKS } from './rub-money'; 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 !== ISO_DATE_LENGTH || !/^\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(ISO_DATE_START, ISO_DATE_LENGTH) === value ); } export function bankBalance( cardId: string, opening: bigint, operations: Array<{ amountKopecks: bigint; cardId: string; kind: string; targetCardId: null | string; }>, ): 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<{ amountKopecks: bigint; categoryId: null | string; date: string; kind: string; }>, ) { let expenses = ZERO_KOPECKS; let refunds = ZERO_KOPECKS; const categories = new Map< string, { categoryId: string; expenses: bigint; net: bigint; refunds: 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 : ZERO_KOPECKS; const refund = operation.kind === BankOperationKind.Refund ? operation.amountKopecks : ZERO_KOPECKS; expenses += expense; refunds += refund; if (operation.categoryId !== null) { const category = categories.get(operation.categoryId) ?? { categoryId: operation.categoryId, expenses: ZERO_KOPECKS, refunds: ZERO_KOPECKS, net: ZERO_KOPECKS, }; 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()], }; }