From 1830d92cbdc6c032fc9c067ef841676afe8662a0 Mon Sep 17 00:00:00 2001 From: Abdalhameed Ahmad Date: Tue, 23 Sep 2025 08:56:57 +0300 Subject: [PATCH] feat: weekly stats for junior --- src/card/dtos/responses/card.response.dto.ts | 4 +- src/card/entities/transaction.entity.ts | 11 ++++- .../repositories/transaction.repository.ts | 12 ++++++ src/card/services/card.service.ts | 4 ++ src/card/services/transaction.service.ts | 40 +++++++++++++++++++ src/junior/controllers/junior.controller.ts | 12 ++++++ .../response/weekly-summary.response.dto.ts | 30 ++++++++++++++ src/junior/services/junior.service.ts | 12 ++++++ 8 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 src/junior/dtos/response/weekly-summary.response.dto.ts diff --git a/src/card/dtos/responses/card.response.dto.ts b/src/card/dtos/responses/card.response.dto.ts index 26f93c3..d69a478 100644 --- a/src/card/dtos/responses/card.response.dto.ts +++ b/src/card/dtos/responses/card.response.dto.ts @@ -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; } } diff --git a/src/card/entities/transaction.entity.ts b/src/card/entities/transaction.entity.ts index 029ef29..9db3c0d 100644 --- a/src/card/entities/transaction.entity.ts +++ b/src/card/entities/transaction.entity.ts @@ -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' }) diff --git a/src/card/repositories/transaction.repository.ts b/src/card/repositories/transaction.repository.ts index b38a257..b8e84b8 100644 --- a/src/card/repositories/transaction.repository.ts +++ b/src/card/repositories/transaction.repository.ts @@ -84,4 +84,16 @@ export class TransactionRepository { where: { transactionId, accountReference }, }); } + + getTransactionsForCardWithinDateRange(juniorId: string, startDate: Date, endDate: Date): Promise { + 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(); + } } diff --git a/src/card/services/card.service.ts b/src/card/services/card.service.ts index 9235aab..b9df3fd 100644 --- a/src/card/services/card.service.ts +++ b/src/card/services/card.service.ts @@ -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); } diff --git a/src/card/services/transaction.service.ts b/src/card/services/transaction.service.ts index f09d844..96237dd 100644 --- a/src/card/services/transaction.service.ts +++ b/src/card/services/transaction.service.ts @@ -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; + } } diff --git a/src/junior/controllers/junior.controller.ts b/src/junior/controllers/junior.controller.ts index 81f2118..b54d397 100644 --- a/src/junior/controllers/junior.controller.ts +++ b/src/junior/controllers/junior.controller.ts @@ -144,4 +144,16 @@ export class JuniorController { return ResponseFactory.data(new TransferToJuniorResponseDto(newAmount)); } + + @Get(':juniorId/weekly-summary') + @UseGuards(RolesGuard) + @AllowedRoles(Roles.GUARDIAN) + @ApiDataResponse('string') + async getWeeklySummary( + @Param('juniorId', CustomParseUUIDPipe) juniorId: string, + @AuthenticatedUser() user: IJwtPayload, + ) { + const summary = await this.juniorService.getWeeklySummary(juniorId, user.sub); + return ResponseFactory.data(summary); + } } diff --git a/src/junior/dtos/response/weekly-summary.response.dto.ts b/src/junior/dtos/response/weekly-summary.response.dto.ts new file mode 100644 index 0000000..310e0f6 --- /dev/null +++ b/src/junior/dtos/response/weekly-summary.response.dto.ts @@ -0,0 +1,30 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class WeeklySummaryResponseDto { + @ApiProperty({ description: 'Total amount spent in the week', example: 350 }) + total!: number; + + @ApiProperty({ description: 'range', example: '2023-10-01 - 2023-10-07' }) + range!: string; + + @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; +} diff --git a/src/junior/services/junior.service.ts b/src/junior/services/junior.service.ts index b864746..d2e9961 100644 --- a/src/junior/services/junior.service.ts +++ b/src/junior/services/junior.service.ts @@ -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(