Compare commits

...

2 Commits

Author SHA1 Message Date
918b15c315 fix: add swagger 2025-09-23 09:00:41 +03:00
1830d92cbd feat: weekly stats for junior 2025-09-23 08:56:57 +03:00
8 changed files with 125 additions and 2 deletions

View File

@ -58,7 +58,9 @@ export class CardResponseDto {
this.status = card.status;
this.statusDescription = CardStatusDescriptionMapper[card.statusDescription][UserLocale.ENGLISH].description;
this.balance =
card.customerType === CustomerType.CHILD ? Math.min(card.limit, card.account.balance) : card.account.balance;
card.customerType === CustomerType.CHILD
? Math.min(card.limit, card.account.balance)
: card.account.balance - card.account.reservedBalance;
this.reservedBalance = card.customerType === CustomerType.PARENT ? card.account.reservedBalance : null;
}
}

View File

@ -32,7 +32,16 @@ export class Transaction {
@Column({ name: 'rrn', nullable: true, type: 'varchar' })
rrn!: string;
@Column({ type: 'decimal', precision: 12, scale: 2, name: 'transaction_amount' })
@Column({
type: 'decimal',
precision: 12,
scale: 2,
name: 'transaction_amount',
transformer: {
to: (value: number) => value,
from: (value: string) => parseFloat(value),
},
})
transactionAmount!: number;
@Column({ type: 'varchar', name: 'transaction_currency' })

View File

@ -84,4 +84,16 @@ export class TransactionRepository {
where: { transactionId, accountReference },
});
}
getTransactionsForCardWithinDateRange(juniorId: string, startDate: Date, endDate: Date): Promise<Transaction[]> {
return this.transactionRepository
.createQueryBuilder('transaction')
.innerJoinAndSelect('transaction.card', 'card')
.where('card.customerId = :juniorId', { juniorId })
.andWhere('transaction.transactionScope = :scope', { scope: TransactionScope.CARD })
.andWhere('transaction.transactionType = :type', { type: TransactionType.EXTERNAL })
.andWhere('transaction.transactionDate BETWEEN :startDate AND :endDate', { startDate, endDate })
.orderBy('transaction.transactionDate', 'DESC')
.getMany();
}
}

View File

@ -163,6 +163,10 @@ export class CardService {
return finalAmount.toNumber();
}
getWeeklySummary(juniorId: string) {
return this.transactionService.getWeeklySummary(juniorId);
}
fundIban(iban: string, amount: number) {
return this.accountService.fundIban(iban, amount);
}

View File

@ -1,5 +1,6 @@
import { forwardRef, Inject, Injectable, UnprocessableEntityException } from '@nestjs/common';
import Decimal from 'decimal.js';
import moment from 'moment';
import { Transactional } from 'typeorm-transactional';
import {
AccountTransactionWebhookRequest,
@ -73,4 +74,43 @@ export class TransactionService {
return existingTransaction;
}
async getWeeklySummary(juniorId: string) {
const startOfWeek = moment().startOf('week').toDate();
const endOfWeek = moment().endOf('week').toDate();
const transactions = await this.transactionRepository.getTransactionsForCardWithinDateRange(
juniorId,
startOfWeek,
endOfWeek,
);
const summary = {
startOfWeek: startOfWeek,
endOfWeek: endOfWeek,
total: 0,
monday: 0,
tuesday: 0,
wednesday: 0,
thursday: 0,
friday: 0,
saturday: 0,
sunday: 0,
};
transactions.forEach((transaction) => {
const day = moment(transaction.transactionDate).format('dddd').toLowerCase() as
| 'monday'
| 'tuesday'
| 'wednesday'
| 'thursday'
| 'friday'
| 'saturday'
| 'sunday';
summary[day] += transaction.transactionAmount;
});
summary.total = transactions.reduce((acc, curr) => acc + curr.transactionAmount, 0);
return summary;
}
}

View File

@ -32,6 +32,7 @@ import {
ThemeResponseDto,
TransferToJuniorResponseDto,
} from '../dtos/response';
import { WeeklySummaryResponseDto } from '../dtos/response/weekly-summary.response.dto';
import { JuniorService } from '../services';
@Controller('juniors')
@ -144,4 +145,16 @@ export class JuniorController {
return ResponseFactory.data(new TransferToJuniorResponseDto(newAmount));
}
@Get(':juniorId/weekly-summary')
@UseGuards(RolesGuard)
@AllowedRoles(Roles.GUARDIAN)
@ApiDataResponse(WeeklySummaryResponseDto)
async getWeeklySummary(
@Param('juniorId', CustomParseUUIDPipe) juniorId: string,
@AuthenticatedUser() user: IJwtPayload,
) {
const summary = await this.juniorService.getWeeklySummary(juniorId, user.sub);
return ResponseFactory.data(summary);
}
}

View File

@ -0,0 +1,31 @@
import { ApiProperty } from '@nestjs/swagger';
export class WeeklySummaryResponseDto {
@ApiProperty({ description: 'Start date of the week', example: '2023-10-01' })
startOfWeek!: Date;
@ApiProperty({ description: 'End date of the week', example: '2023-10-07' })
endOfWeek!: Date;
@ApiProperty({ description: 'Total amount spent in the week', example: 350 })
total!: number;
@ApiProperty({ description: 'Amount spent on Sunday', example: 50 })
sunday!: number;
@ApiProperty({ description: 'Amount spent on Monday', example: 30 })
monday!: number;
@ApiProperty({ description: 'Amount spent on Tuesday', example: 20 })
tuesday!: number;
@ApiProperty({ description: 'Amount spent on Wednesday', example: 40 })
wednesday!: number;
@ApiProperty({ description: 'Amount spent on Thursday', example: 60 })
thursday!: number;
@ApiProperty({ description: 'Amount spent on Friday', example: 70 })
friday!: number;
@ApiProperty({ description: 'Amount spent on Saturday', example: 80 })
saturday!: number;
}

View File

@ -209,6 +209,18 @@ export class JuniorService {
this.logger.log(`Junior ${juniorId} deleted successfully`);
}
getWeeklySummary(juniorId: string, guardianId: string) {
const doesBelong = this.doesJuniorBelongToGuardian(guardianId, juniorId);
if (!doesBelong) {
this.logger.error(`Junior ${juniorId} does not belong to guardian ${guardianId}`);
throw new BadRequestException('JUNIOR.NOT_BELONG_TO_GUARDIAN');
}
this.logger.log(`Getting weekly summary for junior ${juniorId}`);
return this.cardService.getWeeklySummary(juniorId);
}
private async prepareJuniorImages(juniors: Junior[]) {
this.logger.log(`Preparing junior images`);
await Promise.all(