feat(cards): support inclusive date range queries
This commit is contained in:
@@ -42,7 +42,7 @@ export function bankBalance(
|
||||
}
|
||||
|
||||
export function bankSummary(
|
||||
month: string,
|
||||
period: string | { from: string; to: string },
|
||||
operations: Array<{
|
||||
kind: string;
|
||||
date: string;
|
||||
@@ -58,7 +58,12 @@ export function bankSummary(
|
||||
>();
|
||||
// ponytail: O(N) history fold; optimize only after measuring growing histories.
|
||||
for (const operation of operations) {
|
||||
if (operation.date.slice(0, 7) !== month) continue;
|
||||
if (
|
||||
typeof period === 'string'
|
||||
? operation.date.slice(0, 7) !== period
|
||||
: operation.date < period.from || operation.date > period.to
|
||||
)
|
||||
continue;
|
||||
if (operation.kind !== 'expense' && operation.kind !== 'refund') continue;
|
||||
const expense = operation.kind === 'expense' ? operation.amountKopecks : 0n;
|
||||
const refund = operation.kind === 'refund' ? operation.amountKopecks : 0n;
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
ApiProperty,
|
||||
ApiPropertyOptional,
|
||||
OmitType,
|
||||
IntersectionType,
|
||||
PartialType,
|
||||
} from '@nestjs/swagger';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
@@ -153,15 +154,34 @@ export class BankPageQueryDto {
|
||||
@Max(100)
|
||||
limit?: number;
|
||||
}
|
||||
export class BankOperationsQueryDto extends BankPageQueryDto {
|
||||
@ApiProperty({ pattern: '^[0-9]{4}-(0[1-9]|1[0-2])$' })
|
||||
export class BankPeriodQueryDto {
|
||||
@ApiPropertyOptional({
|
||||
pattern: '^[0-9]{4}-(0[1-9]|1[0-2])$',
|
||||
description: 'Use month or both from and to, exclusively.',
|
||||
})
|
||||
@IsOptional()
|
||||
@Matches(/^\d{4}-(0[1-9]|1[0-2])$/)
|
||||
month: string;
|
||||
month?: string;
|
||||
@ApiPropertyOptional({
|
||||
format: 'date',
|
||||
description: 'Inclusive start date; requires to.',
|
||||
})
|
||||
@IsOptional()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
from?: string;
|
||||
@ApiPropertyOptional({
|
||||
format: 'date',
|
||||
description: 'Inclusive end date; requires from.',
|
||||
})
|
||||
@IsOptional()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
to?: 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;
|
||||
export class BankOperationsQueryDto extends IntersectionType(
|
||||
BankPageQueryDto,
|
||||
BankPeriodQueryDto,
|
||||
) {}
|
||||
export class BankSummaryQueryDto extends BankPeriodQueryDto {
|
||||
@ApiPropertyOptional({ format: 'uuid' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@@ -227,7 +247,9 @@ export class BankSummaryCategoryDto {
|
||||
@ApiProperty({ type: MoneyDto }) net: MoneyDto;
|
||||
}
|
||||
export class BankSummaryDto {
|
||||
@ApiProperty({ type: String }) month: string;
|
||||
@ApiPropertyOptional({ type: String }) month?: string;
|
||||
@ApiPropertyOptional({ format: 'date' }) from?: string;
|
||||
@ApiPropertyOptional({ format: 'date' }) to?: string;
|
||||
@ApiProperty({ type: String, nullable: true }) cardId: string | null;
|
||||
@ApiProperty({ type: MoneyDto }) expenses: MoneyDto;
|
||||
@ApiProperty({ type: MoneyDto }) refunds: MoneyDto;
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
BankOperationDto,
|
||||
BankOperationsQueryDto,
|
||||
BankPageQueryDto,
|
||||
BankPeriodQueryDto,
|
||||
BankSummaryQueryDto,
|
||||
CreateBankCardDto,
|
||||
CreateBankOperationDto,
|
||||
@@ -47,6 +48,25 @@ const seeds = [
|
||||
const fail = (code = 'BANK_INVALID_INPUT'): never => {
|
||||
throw new BadRequestException({ code });
|
||||
};
|
||||
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 };
|
||||
}
|
||||
const conflict = (code = 'BANK_VERSION_CONFLICT'): never => {
|
||||
throw new ConflictException({ code });
|
||||
};
|
||||
@@ -612,6 +632,9 @@ export class BankCardsService {
|
||||
query: BankOperationsQueryDto | BankPageQueryDto,
|
||||
expenses = false,
|
||||
) {
|
||||
const period = expenses
|
||||
? undefined
|
||||
: periodBounds(query as BankOperationsQueryDto);
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
await this.card(tx, userId, id);
|
||||
const page = query.page ?? 1,
|
||||
@@ -622,7 +645,7 @@ export class BankCardsService {
|
||||
OR: [{ cardId: id }, { targetCardId: id }],
|
||||
...(expenses
|
||||
? { kind: 'expense' }
|
||||
: { date: { startsWith: (query as BankOperationsQueryDto).month } }),
|
||||
: { date: { gte: period!.from, lte: period!.to } }),
|
||||
};
|
||||
const total = await tx.bankCardOperation.count({ where });
|
||||
const rows = await tx.bankCardOperation.findMany({
|
||||
@@ -642,20 +665,21 @@ export class BankCardsService {
|
||||
});
|
||||
}
|
||||
summary(userId: string, query: BankSummaryQueryDto) {
|
||||
const period = periodBounds(query);
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
if (query.cardId) await this.card(tx, userId, query.cardId);
|
||||
const rows = await tx.bankCardOperation.findMany({
|
||||
where: {
|
||||
userId,
|
||||
deletedAt: null,
|
||||
date: { startsWith: query.month },
|
||||
date: { gte: period.from, lte: period.to },
|
||||
kind: { in: ['expense', 'refund'] },
|
||||
...(query.cardId ? { cardId: query.cardId } : {}),
|
||||
},
|
||||
include: { originalExpense: { select: { categoryId: true } } },
|
||||
});
|
||||
const totals = bankSummary(
|
||||
query.month,
|
||||
period,
|
||||
rows.map((row) => ({
|
||||
...row,
|
||||
categoryId: row.originalExpense?.categoryId ?? row.categoryId,
|
||||
@@ -666,6 +690,7 @@ export class BankCardsService {
|
||||
});
|
||||
return {
|
||||
month: query.month,
|
||||
...period,
|
||||
cardId: query.cardId ?? null,
|
||||
expenses: rubMoney(totals.expenses),
|
||||
refunds: rubMoney(totals.refunds),
|
||||
|
||||
Reference in New Issue
Block a user