136 lines
4.2 KiB
TypeScript
136 lines
4.2 KiB
TypeScript
import {
|
|
MAX_CARD_NAME_LENGTH,
|
|
MAX_OPERATION_DESCRIPTION_LENGTH,
|
|
} from '../domain/bank-card-limits';
|
|
import {
|
|
BadRequestException,
|
|
ConflictException,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import { createHash } from 'node:crypto';
|
|
import {
|
|
bankPaymentSystems,
|
|
BankPeriodQueryDto,
|
|
CreateBankCardDto,
|
|
CreateBankOperationDto,
|
|
} from '../presentation/bank-card.dto';
|
|
import { isCalendarDate, moscowToday } from '../domain/bank-card.calculations';
|
|
import { parseRubMoney } from '../domain/rub-money';
|
|
|
|
export const BankErrorCode = {
|
|
InvalidInput: 'BANK_INVALID_INPUT',
|
|
VersionConflict: 'BANK_VERSION_CONFLICT',
|
|
NotFound: 'BANK_NOT_FOUND',
|
|
InvalidMoney: 'BANK_INVALID_MONEY',
|
|
InvalidDate: 'BANK_INVALID_DATE',
|
|
ImmutableOperation: 'BANK_IMMUTABLE_OPERATION',
|
|
InvalidRefund: 'BANK_INVALID_REFUND',
|
|
RefundLimit: 'BANK_REFUND_LIMIT',
|
|
DependentRefunds: 'BANK_DEPENDENT_REFUNDS',
|
|
IntentConflict: 'BANK_INTENT_CONFLICT',
|
|
OperationDeleted: 'BANK_OPERATION_DELETED',
|
|
CardArchived: 'BANK_CARD_ARCHIVED',
|
|
OpeningDateConflict: 'BANK_OPENING_DATE_CONFLICT',
|
|
WriteConflict: 'BANK_WRITE_CONFLICT',
|
|
DuplicateCategory: 'BANK_DUPLICATE_CATEGORY',
|
|
} as const;
|
|
export type BankErrorCode = (typeof BankErrorCode)[keyof typeof BankErrorCode];
|
|
|
|
export function fail(code: BankErrorCode = BankErrorCode.InvalidInput): never {
|
|
throw new BadRequestException({ code });
|
|
}
|
|
export function periodBounds(query: BankPeriodQueryDto) {
|
|
if (query.month && !query.from && !query.to) {
|
|
const from = `${query.month}-01`;
|
|
if (!isCalendarDate(from)) return fail();
|
|
const end = new Date(`${from}T00:00:00Z`);
|
|
end.setUTCMonth(end.getUTCMonth() + 1, 0);
|
|
return { from, to: end.toISOString().slice(0, 10) };
|
|
}
|
|
if (
|
|
query.month ||
|
|
!query.from ||
|
|
!query.to ||
|
|
!isCalendarDate(query.from) ||
|
|
!isCalendarDate(query.to) ||
|
|
query.from > query.to
|
|
)
|
|
return fail();
|
|
return { from: query.from, to: query.to };
|
|
}
|
|
export function conflict(
|
|
code: BankErrorCode = BankErrorCode.VersionConflict,
|
|
): never {
|
|
throw new ConflictException({ code });
|
|
}
|
|
export function missing(): never {
|
|
throw new NotFoundException({ code: BankErrorCode.NotFound });
|
|
}
|
|
export const fingerprint = (value: unknown) =>
|
|
createHash('sha256')
|
|
.update(
|
|
JSON.stringify(value, (_key, item: unknown) =>
|
|
typeof item === 'bigint' ? item.toString() : item,
|
|
),
|
|
)
|
|
.digest('hex');
|
|
export function amount(
|
|
value: Parameters<typeof parseRubMoney>[0],
|
|
positive = false,
|
|
) {
|
|
try {
|
|
return parseRubMoney(value, positive);
|
|
} catch {
|
|
return fail(BankErrorCode.InvalidMoney);
|
|
}
|
|
}
|
|
export function calendar(value: string) {
|
|
if (!isCalendarDate(value)) fail(BankErrorCode.InvalidDate);
|
|
return value;
|
|
}
|
|
export function currentDate(value: string) {
|
|
calendar(value);
|
|
if (value > moscowToday()) fail(BankErrorCode.InvalidDate);
|
|
}
|
|
export function name(value: string, max: number) {
|
|
if (typeof value !== 'string' || !value.trim() || value.trim().length > max)
|
|
fail();
|
|
return value.trim();
|
|
}
|
|
export function cardInput(dto: CreateBankCardDto) {
|
|
const bankName = dto.bankName?.trim() || null;
|
|
if (bankName && bankName.length > MAX_CARD_NAME_LENGTH) fail();
|
|
if (dto.lastFour != null && !/^\d{4}$/.test(dto.lastFour)) fail();
|
|
if (
|
|
dto.paymentSystem != null &&
|
|
!bankPaymentSystems.some((system) => system === dto.paymentSystem)
|
|
)
|
|
fail();
|
|
return {
|
|
...(dto.paymentSystem == null ? {} : { paymentSystem: dto.paymentSystem }),
|
|
name: name(dto.name, MAX_CARD_NAME_LENGTH),
|
|
bankName,
|
|
lastFour: dto.lastFour || null,
|
|
openingBalanceKopecks: amount(dto.openingBalance),
|
|
openingDate: calendar(dto.openingDate),
|
|
};
|
|
}
|
|
export function operationInput(dto: CreateBankOperationDto) {
|
|
if (
|
|
dto.description != null &&
|
|
(typeof dto.description !== 'string' ||
|
|
dto.description.length > MAX_OPERATION_DESCRIPTION_LENGTH)
|
|
)
|
|
fail();
|
|
return {
|
|
kind: dto.kind,
|
|
cardId: dto.cardId,
|
|
targetCardId: dto.targetCardId ?? null,
|
|
categoryId: dto.categoryId ?? null,
|
|
originalExpenseId: dto.originalExpenseId ?? null,
|
|
amountKopecks: amount(dto.amount, true),
|
|
date: calendar(dto.date),
|
|
description: dto.description || null,
|
|
};
|
|
}
|