feat: add manual bank card ledger
Store exact RUB balances, atomic transfers, linked refunds and versioned edits independently of T-Bank. Add migration, HTTP and durability verification.
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
import {
|
||||
ApiProperty,
|
||||
ApiPropertyOptional,
|
||||
OmitType,
|
||||
PartialType,
|
||||
} from '@nestjs/swagger';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDefined,
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Matches,
|
||||
Max,
|
||||
MaxLength,
|
||||
Min,
|
||||
MinLength,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { MoneyDto } from '../common/money.dto';
|
||||
|
||||
const trim = ({ value }: { value: unknown }) =>
|
||||
typeof value === 'string' ? value.trim() : value;
|
||||
export const bankOperationKinds = [
|
||||
'expense',
|
||||
'topup',
|
||||
'transfer',
|
||||
'refund',
|
||||
] as const;
|
||||
export type BankOperationKind = (typeof bankOperationKinds)[number];
|
||||
export class RubMoneyInputDto {
|
||||
@ApiProperty({ enum: ['RUB'] }) @IsIn(['RUB']) currency: string;
|
||||
@ApiProperty({ pattern: '^-?[0-9]+(\\.[0-9]{1,2})?$' })
|
||||
@IsString()
|
||||
@Matches(/^-?\d+(\.\d{1,2})?$/)
|
||||
value: string;
|
||||
}
|
||||
export class CreateBankCardDto {
|
||||
@ApiProperty({ format: 'uuid' }) @IsUUID() id: string;
|
||||
@ApiProperty({ minLength: 1, maxLength: 100 })
|
||||
@Transform(trim)
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(100)
|
||||
name: string;
|
||||
@ApiPropertyOptional({ type: String, nullable: true, maxLength: 100 })
|
||||
@IsOptional()
|
||||
@Transform(trim)
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
bankName?: string | null;
|
||||
@ApiPropertyOptional({ type: String, nullable: true, pattern: '^[0-9]{4}$' })
|
||||
@IsOptional()
|
||||
@Matches(/^[0-9]{4}$/)
|
||||
lastFour?: string | null;
|
||||
@ApiProperty({ type: RubMoneyInputDto })
|
||||
@IsDefined()
|
||||
@ValidateNested()
|
||||
@Type(() => RubMoneyInputDto)
|
||||
openingBalance: MoneyDto;
|
||||
@ApiProperty({ format: 'date' })
|
||||
@IsString()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
openingDate: string;
|
||||
}
|
||||
export class UpdateBankCardDto extends PartialType(
|
||||
OmitType(CreateBankCardDto, ['id'] as const),
|
||||
{ skipNullProperties: false },
|
||||
) {
|
||||
@ApiProperty({ minimum: 1 }) @IsInt() @Min(1) version: number;
|
||||
@ApiPropertyOptional()
|
||||
@ValidateIf((_object, value: unknown) => value !== undefined)
|
||||
@IsBoolean()
|
||||
archived?: boolean;
|
||||
}
|
||||
export class CategoryNameDto {
|
||||
@ApiProperty({ minLength: 1, maxLength: 80 })
|
||||
@Transform(trim)
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(80)
|
||||
name: string;
|
||||
}
|
||||
export class UpdateExpenseCategoryDto extends CategoryNameDto {
|
||||
@ApiProperty({ minimum: 1 }) @IsInt() @Min(1) version: number;
|
||||
}
|
||||
export class CreateBankOperationDto {
|
||||
@ApiProperty({ format: 'uuid' }) @IsUUID() id: string;
|
||||
@ApiProperty({ enum: bankOperationKinds })
|
||||
@IsIn(bankOperationKinds)
|
||||
kind: BankOperationKind;
|
||||
@ApiProperty({ format: 'uuid' }) @IsUUID() cardId: string;
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@ValidateIf((_object, value: unknown) => value !== undefined)
|
||||
@IsUUID()
|
||||
targetCardId?: string;
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@ValidateIf((_object, value: unknown) => value !== undefined)
|
||||
@IsUUID()
|
||||
categoryId?: string;
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@ValidateIf((_object, value: unknown) => value !== undefined)
|
||||
@IsUUID()
|
||||
originalExpenseId?: string;
|
||||
@ApiProperty({ type: RubMoneyInputDto })
|
||||
@IsDefined()
|
||||
@ValidateNested()
|
||||
@Type(() => RubMoneyInputDto)
|
||||
amount: MoneyDto;
|
||||
@ApiProperty({ format: 'date' })
|
||||
@IsString()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
date: string;
|
||||
@ApiPropertyOptional({ maxLength: 500 })
|
||||
@ValidateIf((_object, value: unknown) => value !== undefined)
|
||||
@IsString()
|
||||
@MaxLength(500)
|
||||
description?: string;
|
||||
}
|
||||
export class UpdateBankOperationDto extends PartialType(
|
||||
OmitType(CreateBankOperationDto, ['id'] as const),
|
||||
{ skipNullProperties: false },
|
||||
) {
|
||||
@ApiProperty({ minimum: 1 }) @IsInt() @Min(1) version: number;
|
||||
}
|
||||
export class BankCardsQueryDto {
|
||||
@ApiPropertyOptional({ enum: ['true', 'false'] })
|
||||
@IsOptional()
|
||||
@IsIn(['true', 'false'])
|
||||
includeArchived?: string;
|
||||
}
|
||||
export class BankPageQueryDto {
|
||||
@ApiPropertyOptional({ minimum: 1, default: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number;
|
||||
@ApiPropertyOptional({ minimum: 1, maximum: 100, default: 50 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
limit?: number;
|
||||
}
|
||||
export class BankOperationsQueryDto extends BankPageQueryDto {
|
||||
@ApiProperty({ pattern: '^[0-9]{4}-(0[1-9]|1[0-2])$' })
|
||||
@Matches(/^\d{4}-(0[1-9]|1[0-2])$/)
|
||||
month: string;
|
||||
}
|
||||
export class BankSummaryQueryDto {
|
||||
@ApiProperty({ pattern: '^[0-9]{4}-(0[1-9]|1[0-2])$' })
|
||||
@Matches(/^\d{4}-(0[1-9]|1[0-2])$/)
|
||||
month: string;
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
cardId?: string;
|
||||
}
|
||||
export class BankVersionQueryDto {
|
||||
@ApiProperty({ minimum: 1 })
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
version: number;
|
||||
}
|
||||
export class ExpenseCategoryDto {
|
||||
@ApiProperty({ type: String }) id: string;
|
||||
@ApiProperty({ type: String }) name: string;
|
||||
@ApiProperty({ type: Boolean }) isDefault: boolean;
|
||||
@ApiProperty({ type: Number }) version: number;
|
||||
}
|
||||
export class BankCardDto {
|
||||
@ApiProperty({ type: String }) id: string;
|
||||
@ApiProperty({ type: String }) name: string;
|
||||
@ApiProperty({ type: String, nullable: true }) bankName: string | null;
|
||||
@ApiProperty({ type: String, nullable: true }) lastFour: string | null;
|
||||
@ApiProperty({ type: MoneyDto }) openingBalance: MoneyDto;
|
||||
@ApiProperty({ type: String }) openingDate: string;
|
||||
@ApiProperty({ type: MoneyDto }) balance: MoneyDto;
|
||||
@ApiProperty({ type: String, nullable: true }) archivedAt: string | null;
|
||||
@ApiProperty({ type: Number }) version: number;
|
||||
}
|
||||
export class BankOperationDto {
|
||||
@ApiProperty({ type: String }) id: string;
|
||||
@ApiProperty({ enum: bankOperationKinds }) kind: BankOperationKind;
|
||||
@ApiProperty({ type: String }) cardId: string;
|
||||
@ApiProperty({ type: String }) cardName: string;
|
||||
@ApiProperty({ type: String, nullable: true }) targetCardId: string | null;
|
||||
@ApiProperty({ type: String, nullable: true }) targetCardName: string | null;
|
||||
@ApiProperty({ type: String, nullable: true }) categoryId: string | null;
|
||||
@ApiProperty({ type: String, nullable: true }) categoryName: string | null;
|
||||
@ApiProperty({ type: String, nullable: true }) originalExpenseId:
|
||||
string | null;
|
||||
@ApiProperty({ type: MoneyDto }) amount: MoneyDto;
|
||||
@ApiProperty({ type: MoneyDto, nullable: true })
|
||||
availableRefund: MoneyDto | null;
|
||||
@ApiProperty({ type: String }) date: string;
|
||||
@ApiProperty({ type: String, nullable: true }) description: string | null;
|
||||
@ApiProperty({ type: Number }) version: number;
|
||||
@ApiProperty({ type: String }) createdAt: string;
|
||||
}
|
||||
export class BankOperationsPageDto {
|
||||
@ApiProperty({ type: BankOperationDto, isArray: true })
|
||||
items: BankOperationDto[];
|
||||
@ApiProperty({ type: Number }) page: number;
|
||||
@ApiProperty({ type: Number }) total: number;
|
||||
@ApiProperty({ type: Boolean }) hasNext: boolean;
|
||||
}
|
||||
export class BankSummaryCategoryDto {
|
||||
@ApiProperty({ type: String }) categoryId: string;
|
||||
@ApiProperty({ type: String }) categoryName: string;
|
||||
@ApiProperty({ type: MoneyDto }) expenses: MoneyDto;
|
||||
@ApiProperty({ type: MoneyDto }) refunds: MoneyDto;
|
||||
@ApiProperty({ type: MoneyDto }) net: MoneyDto;
|
||||
}
|
||||
export class BankSummaryDto {
|
||||
@ApiProperty({ type: String }) month: string;
|
||||
@ApiProperty({ type: String, nullable: true }) cardId: string | null;
|
||||
@ApiProperty({ type: MoneyDto }) expenses: MoneyDto;
|
||||
@ApiProperty({ type: MoneyDto }) refunds: MoneyDto;
|
||||
@ApiProperty({ type: MoneyDto }) net: MoneyDto;
|
||||
@ApiProperty({ type: BankSummaryCategoryDto, isArray: true })
|
||||
categories: BankSummaryCategoryDto[];
|
||||
}
|
||||
Reference in New Issue
Block a user