113 lines
3.2 KiB
TypeScript
113 lines
3.2 KiB
TypeScript
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()],
|
|
};
|
|
}
|