From 9b0e1791da2bdfb62bcedffddcf27145dbf86123 Mon Sep 17 00:00:00 2001 From: Abdalhameed Ahmad Date: Sun, 7 Sep 2025 20:14:28 +0300 Subject: [PATCH] feat: transfer money to child --- src/card/repositories/card.repository.ts | 6 ++++ .../repositories/transaction.repository.ts | 22 +++++++++++++++ src/card/services/card.service.ts | 25 +++++++++++++++++ src/card/services/transaction.service.ts | 10 +++++-- src/junior/controllers/junior.controller.ts | 28 +++++++++++++++++-- src/junior/dtos/request/index.ts | 1 + .../request/transfer-to-junior.request.dto.ts | 12 ++++++++ src/junior/dtos/response/index.ts | 1 + .../transfer-to-junior.response.dto.ts | 10 +++++++ src/junior/services/junior.service.ts | 18 +++++++++++- 10 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 src/junior/dtos/request/transfer-to-junior.request.dto.ts create mode 100644 src/junior/dtos/response/transfer-to-junior.response.dto.ts diff --git a/src/card/repositories/card.repository.ts b/src/card/repositories/card.repository.ts index 0fa9bd7..cecfdb0 100644 --- a/src/card/repositories/card.repository.ts +++ b/src/card/repositories/card.repository.ts @@ -60,4 +60,10 @@ export class CardRepository { statusDescription: statusDescription, }); } + + updateCardLimit(cardId: string, newLimit: number) { + return this.cardRepository.update(cardId, { + limit: newLimit, + }); + } } diff --git a/src/card/repositories/transaction.repository.ts b/src/card/repositories/transaction.repository.ts index 39d8665..b38a257 100644 --- a/src/card/repositories/transaction.repository.ts +++ b/src/card/repositories/transaction.repository.ts @@ -57,6 +57,28 @@ export class TransactionRepository { ); } + createInternalChildTransaction(card: Card, amount: number): Promise { + return this.transactionRepository.save( + this.transactionRepository.create({ + transactionId: `CHILD-${card.id}-${Date.now()}`, + transactionAmount: amount, + transactionCurrency: '682', + billingAmount: 0, + settlementAmount: 0, + transactionDate: new Date(), + fees: 0, + cardId: card.id, + cardReference: card.cardReference, + cardMaskedNumber: card.firstSixDigits + '******' + card.lastFourDigits, + accountId: card.account!.id, + transactionType: TransactionType.INTERNAL, + accountReference: card.account!.accountReference, + transactionScope: TransactionScope.CARD, + vatOnFees: 0, + }), + ); + } + findTransactionByReference(transactionId: string, accountReference: string): Promise { return this.transactionRepository.findOne({ where: { transactionId, accountReference }, diff --git a/src/card/services/card.service.ts b/src/card/services/card.service.ts index 2231fcb..c251a5a 100644 --- a/src/card/services/card.service.ts +++ b/src/card/services/card.service.ts @@ -1,4 +1,5 @@ import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common'; +import Decimal from 'decimal.js'; import { Transactional } from 'typeorm-transactional'; import { AccountCardStatusChangedWebhookRequest } from '~/common/modules/neoleap/dtos/requests'; import { NeoLeapService } from '~/common/modules/neoleap/services'; @@ -10,12 +11,14 @@ import { CardColors } from '../enums'; import { CardStatusMapper } from '../mappers/card-status.mapper'; import { CardRepository } from '../repositories'; import { AccountService } from './account.service'; +import { TransactionService } from './transaction.service'; @Injectable() export class CardService { constructor( private readonly cardRepository: CardRepository, private readonly accountService: AccountService, + @Inject(forwardRef(() => TransactionService)) private readonly transactionService: TransactionService, @Inject(forwardRef(() => NeoLeapService)) private readonly neoleapService: NeoLeapService, @Inject(forwardRef(() => CustomerService)) private readonly customerService: CustomerService, ) {} @@ -99,4 +102,26 @@ export class CardService { return this.neoleapService.getEmbossingInformation(card); } + + async updateCardLimit(cardId: string, newLimit: number) { + const { affected } = await this.cardRepository.updateCardLimit(cardId, newLimit); + + if (affected === 0) { + throw new BadRequestException('CARD.NOT_FOUND'); + } + } + + @Transactional() + async transferToChild(juniorId: string, amount: number) { + const card = await this.getCardByCustomerId(juniorId); + + const finalAmount = Decimal(amount).plus(card.limit); + await Promise.all([ + this.neoleapService.updateCardControl(card.cardReference, finalAmount.toNumber()), + this.updateCardLimit(card.id, finalAmount.toNumber()), + this.transactionService.createInternalChildTransaction(card.id, amount), + ]); + + return finalAmount.toNumber(); + } } diff --git a/src/card/services/transaction.service.ts b/src/card/services/transaction.service.ts index d195368..b48759c 100644 --- a/src/card/services/transaction.service.ts +++ b/src/card/services/transaction.service.ts @@ -1,4 +1,4 @@ -import { Injectable, UnprocessableEntityException } from '@nestjs/common'; +import { forwardRef, Inject, Injectable, UnprocessableEntityException } from '@nestjs/common'; import Decimal from 'decimal.js'; import { Transactional } from 'typeorm-transactional'; import { @@ -15,7 +15,7 @@ export class TransactionService { constructor( private readonly transactionRepository: TransactionRepository, private readonly accountService: AccountService, - private readonly cardService: CardService, + @Inject(forwardRef(() => CardService)) private readonly cardService: CardService, ) {} @Transactional() @@ -51,6 +51,12 @@ export class TransactionService { return transaction; } + async createInternalChildTransaction(cardId: string, amount: number) { + const card = await this.cardService.getCardById(cardId); + const transaction = await this.transactionRepository.createInternalChildTransaction(card, amount); + return transaction; + } + private async findExistingTransaction(transactionId: string, accountReference: string): Promise { const existingTransaction = await this.transactionRepository.findTransactionByReference( transactionId, diff --git a/src/junior/controllers/junior.controller.ts b/src/junior/controllers/junior.controller.ts index ebd9ff0..35d9480 100644 --- a/src/junior/controllers/junior.controller.ts +++ b/src/junior/controllers/junior.controller.ts @@ -8,8 +8,18 @@ import { ApiDataPageResponse, ApiDataResponse, ApiLangRequestHeader } from '~/co import { PageOptionsRequestDto } from '~/core/dtos'; import { CustomParseUUIDPipe } from '~/core/pipes'; import { ResponseFactory } from '~/core/utils'; -import { CreateJuniorRequestDto, SetThemeRequestDto, UpdateJuniorRequestDto } from '../dtos/request'; -import { JuniorResponseDto, QrCodeValidationResponseDto, ThemeResponseDto } from '../dtos/response'; +import { + CreateJuniorRequestDto, + SetThemeRequestDto, + TransferToJuniorRequestDto, + UpdateJuniorRequestDto, +} from '../dtos/request'; +import { + JuniorResponseDto, + QrCodeValidationResponseDto, + ThemeResponseDto, + TransferToJuniorResponseDto, +} from '../dtos/response'; import { JuniorService } from '../services'; @Controller('juniors') @@ -100,4 +110,18 @@ export class JuniorController { return ResponseFactory.data(new QrCodeValidationResponseDto(junior)); } + + @Post(':juniorId/transfer') + @UseGuards(RolesGuard) + @AllowedRoles(Roles.GUARDIAN) + @ApiDataResponse(TransferToJuniorResponseDto) + async doesJuniorBelongToGuardian( + @AuthenticatedUser() user: IJwtPayload, + @Param('juniorId', CustomParseUUIDPipe) juniorId: string, + @Body() body: TransferToJuniorRequestDto, + ) { + const newAmount = await this.juniorService.transferToJunior(juniorId, body, user.sub); + + return ResponseFactory.data(new TransferToJuniorResponseDto(newAmount)); + } } diff --git a/src/junior/dtos/request/index.ts b/src/junior/dtos/request/index.ts index 08e7612..1f3590b 100644 --- a/src/junior/dtos/request/index.ts +++ b/src/junior/dtos/request/index.ts @@ -1,3 +1,4 @@ export * from './create-junior.request.dto'; export * from './set-theme.request.dto'; +export * from './transfer-to-junior.request.dto'; export * from './update-junior.request.dto'; diff --git a/src/junior/dtos/request/transfer-to-junior.request.dto.ts b/src/junior/dtos/request/transfer-to-junior.request.dto.ts new file mode 100644 index 0000000..a62ca44 --- /dev/null +++ b/src/junior/dtos/request/transfer-to-junior.request.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber } from 'class-validator'; +import { i18nValidationMessage as i18n } from 'nestjs-i18n'; + +export class TransferToJuniorRequestDto { + @ApiProperty({ example: 300.42 }) + @IsNumber( + { maxDecimalPlaces: 3 }, + { message: i18n('validation.IsNumber', { path: 'general', property: 'transferToJunior.amount' }) }, + ) + amount!: number; +} diff --git a/src/junior/dtos/response/index.ts b/src/junior/dtos/response/index.ts index 12ce574..2564df2 100644 --- a/src/junior/dtos/response/index.ts +++ b/src/junior/dtos/response/index.ts @@ -2,3 +2,4 @@ export * from './junior.response.dto'; export * from './qr-code-validation-details.response.dto'; export * from './qr-code-validation.response.dto'; export * from './theme.response.dto'; +export * from './transfer-to-junior.response.dto'; diff --git a/src/junior/dtos/response/transfer-to-junior.response.dto.ts b/src/junior/dtos/response/transfer-to-junior.response.dto.ts new file mode 100644 index 0000000..73f06bc --- /dev/null +++ b/src/junior/dtos/response/transfer-to-junior.response.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class TransferToJuniorResponseDto { + @ApiProperty({ example: 300.42 }) + newAmount!: number; + + constructor(newAmount: number) { + this.newAmount = newAmount; + } +} diff --git a/src/junior/services/junior.service.ts b/src/junior/services/junior.service.ts index 63f5593..acb611e 100644 --- a/src/junior/services/junior.service.ts +++ b/src/junior/services/junior.service.ts @@ -10,7 +10,12 @@ import { DocumentService, OciService } from '~/document/services'; import { UserType } from '~/user/enums'; import { UserService } from '~/user/services'; import { UserTokenService } from '~/user/services/user-token.service'; -import { CreateJuniorRequestDto, SetThemeRequestDto, UpdateJuniorRequestDto } from '../dtos/request'; +import { + CreateJuniorRequestDto, + SetThemeRequestDto, + TransferToJuniorRequestDto, + UpdateJuniorRequestDto, +} from '../dtos/request'; import { Junior } from '../entities'; import { JuniorRepository } from '../repositories'; import { QrcodeService } from './qrcode.service'; @@ -167,6 +172,17 @@ export class JuniorService { return !!junior; } + async transferToJunior(juniorId: string, body: TransferToJuniorRequestDto, guardianId: string) { + const doesBelong = await 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'); + } + + return this.cardService.transferToChild(juniorId, body.amount); + } + private async prepareJuniorImages(juniors: Junior[]) { this.logger.log(`Preparing junior images`); await Promise.all(