Files
backend/src/modules/banking/bank-cards/domain/bank-card.calculations.ts
T

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()],
};
}