refactor: organize backend by business responsibility

This commit is contained in:
2026-09-13 13:52:57 +03:00
parent 7fde3ece6e
commit 8ba005346d
145 changed files with 3027 additions and 2346 deletions
+2 -2
View File
@@ -1,9 +1,9 @@
import { BadRequestException, ValidationPipe } from '@nestjs/common';
import { CredentialsDto } from '../auth/auth.dto';
import { CredentialsDto } from '../modules/auth/presentation/auth.dto';
import {
CreateBankCardDto,
UpdateBankCardDto,
} from '../bank-cards/bank-card.dto';
} from '../modules/banking/bank-cards/presentation/bank-card.dto';
describe('DTO validation', () => {
const pipe = new ValidationPipe({ transform: true, whitelist: true });
+2 -1
View File
@@ -1,6 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import type { Money } from './money.types';
export class MoneyDto {
export class MoneyDto implements Money {
@ApiProperty()
declare currency: string;
+1
View File
@@ -0,0 +1 @@
export type Money = { currency: string; value: string };
-57
View File
@@ -1,57 +0,0 @@
import { parseRubMoney, rubMoney } from './rub-money';
describe('RUB money', () => {
it.each([
['0', 0n],
['-100', -10000n],
['0.01', 1n],
['123.4', 12340n],
['90071992547409.93', 9007199254740993n],
['92233720368547758.07', 2n ** 63n - 1n],
['-92233720368547758.08', -(2n ** 63n)],
])('roundtrips %s exactly', (value, kopecks) => {
expect(parseRubMoney({ currency: 'RUB', value })).toBe(kopecks);
expect(rubMoney(kopecks)).toEqual({ currency: 'RUB', value });
});
it.each([
'92233720368547758.08',
'-92233720368547758.09',
'1.001',
'1.000',
'NaN',
'Infinity',
'1e2',
'1,23',
'',
' 1',
'1\n',
'01',
])('rejects invalid input %s without rounding', (value) => {
expect(() => parseRubMoney({ currency: 'RUB', value })).toThrow();
});
it('rejects other currencies and non-string values', () => {
expect(() => parseRubMoney({ currency: 'USD', value: '1' })).toThrow();
expect(() =>
parseRubMoney({ currency: 'RUB', value: 1 as unknown as string }),
).toThrow();
});
it('requires positive operation amounts, including at the maximum', () => {
for (const value of ['0', '-0.00', '-0.01'])
expect(() => parseRubMoney({ currency: 'RUB', value }, true)).toThrow();
expect(parseRubMoney({ currency: 'RUB', value: '0.01' }, true)).toBe(1n);
expect(
parseRubMoney({ currency: 'RUB', value: '92233720368547758.07' }, true),
).toBe(2n ** 63n - 1n);
});
it('formats computed sums beyond the storage range without losing precision', () => {
expect(rubMoney(2n * (2n ** 63n - 1n))).toEqual({
currency: 'RUB',
value: '184467440737095516.14',
});
expect(rubMoney(-1n)).toEqual({ currency: 'RUB', value: '-0.01' });
});
});
-27
View File
@@ -1,27 +0,0 @@
import { decimalFromString, formatDecimal } from './decimal';
import { MoneyDto } from './money.dto';
export function parseRubMoney(money: MoneyDto, positive = false): bigint {
if (
money?.currency !== 'RUB' ||
typeof money.value !== 'string' ||
money.value !== money.value.trim()
)
throw new TypeError('Expected RUB money with a decimal string');
const amount = decimalFromString(money.value);
if (amount.scale > 2)
throw new RangeError('RUB money supports at most two decimal places');
const kopecks = amount.value * 10n ** BigInt(2 - amount.scale);
if (kopecks < -(2n ** 63n) || kopecks > 2n ** 63n - 1n)
throw new RangeError('RUB money is outside the signed 64-bit range');
if (positive && kopecks <= 0n)
throw new RangeError('Operation amount must be positive');
return kopecks;
}
export function rubMoney(kopecks: bigint): MoneyDto {
return {
currency: 'RUB',
value: formatDecimal({ value: kopecks, scale: 2 }),
};
}