From 15a48e48840e6b2339fb12c1fa1a90dc9d747bd6 Mon Sep 17 00:00:00 2001 From: Abdalhameed Ahmad Date: Sun, 7 Sep 2025 21:18:49 +0300 Subject: [PATCH] fix: validate card spending limit before transfering to child --- src/card/controllers/cards.controller.ts | 14 ++++++++++++-- src/card/dtos/requests/fund-iban.request.dto.ts | 7 +++++++ src/card/dtos/requests/index.ts | 1 + src/card/repositories/account.repository.ts | 7 +++++++ src/card/repositories/transaction.repository.ts | 13 +++++++++++++ src/card/services/account.service.ts | 14 ++++++++++++++ src/card/services/card.service.ts | 9 +++++++++ src/card/services/transaction.service.ts | 6 ++++++ src/i18n/ar/app.json | 3 +++ src/i18n/en/app.json | 3 +++ 10 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 src/card/dtos/requests/fund-iban.request.dto.ts create mode 100644 src/card/dtos/requests/index.ts diff --git a/src/card/controllers/cards.controller.ts b/src/card/controllers/cards.controller.ts index b81fb0e..4b1a12a 100644 --- a/src/card/controllers/cards.controller.ts +++ b/src/card/controllers/cards.controller.ts @@ -1,5 +1,5 @@ -import { Controller, Get, Post, UseGuards } from '@nestjs/common'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { Body, Controller, Get, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { Roles } from '~/auth/enums'; import { IJwtPayload } from '~/auth/interfaces'; import { AllowedRoles, AuthenticatedUser } from '~/common/decorators'; @@ -7,6 +7,7 @@ import { AccessTokenGuard, RolesGuard } from '~/common/guards'; import { CardEmbossingDetailsResponseDto } from '~/common/modules/neoleap/dtos/response'; import { ApiDataResponse } from '~/core/decorators'; import { ResponseFactory } from '~/core/utils'; +import { FundIbanRequestDto } from '../dtos/requests'; import { AccountIbanResponseDto, CardResponseDto } from '../dtos/responses'; import { CardService } from '../services'; @@ -46,4 +47,13 @@ export class CardsController { const iban = await this.cardService.getIbanInformation(sub); return ResponseFactory.data(new AccountIbanResponseDto(iban)); } + + @Post('mock/fund-iban') + @ApiOperation({ summary: 'Mock endpoint to fund the IBAN - For testing purposes only' }) + @UseGuards(RolesGuard) + @AllowedRoles(Roles.GUARDIAN) + @HttpCode(HttpStatus.NO_CONTENT) + fundIban(@Body() { amount, iban }: FundIbanRequestDto) { + return this.cardService.fundIban(iban, amount); + } } diff --git a/src/card/dtos/requests/fund-iban.request.dto.ts b/src/card/dtos/requests/fund-iban.request.dto.ts new file mode 100644 index 0000000..d22a544 --- /dev/null +++ b/src/card/dtos/requests/fund-iban.request.dto.ts @@ -0,0 +1,7 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { TransferToJuniorRequestDto } from '~/junior/dtos/request'; + +export class FundIbanRequestDto extends TransferToJuniorRequestDto { + @ApiProperty({ example: 'DE89370400440532013000' }) + iban!: string; +} diff --git a/src/card/dtos/requests/index.ts b/src/card/dtos/requests/index.ts new file mode 100644 index 0000000..36ead34 --- /dev/null +++ b/src/card/dtos/requests/index.ts @@ -0,0 +1 @@ +export * from './fund-iban.request.dto'; diff --git a/src/card/repositories/account.repository.ts b/src/card/repositories/account.repository.ts index 1e4af12..ae1349f 100644 --- a/src/card/repositories/account.repository.ts +++ b/src/card/repositories/account.repository.ts @@ -27,6 +27,13 @@ export class AccountRepository { }); } + getAccountByIban(iban: string): Promise { + return this.accountRepository.findOne({ + where: { iban }, + relations: ['cards'], + }); + } + getAccountByAccountNumber(accountNumber: string): Promise { return this.accountRepository.findOne({ where: { accountNumber }, diff --git a/src/card/repositories/transaction.repository.ts b/src/card/repositories/transaction.repository.ts index b38a257..a61f55d 100644 --- a/src/card/repositories/transaction.repository.ts +++ b/src/card/repositories/transaction.repository.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { Decimal } from 'decimal.js'; import moment from 'moment'; import { Repository } from 'typeorm'; import { @@ -84,4 +85,16 @@ export class TransactionRepository { where: { transactionId, accountReference }, }); } + + findInternalTransactionTotal(accountId: string): Promise { + return this.transactionRepository + .find({ + where: { accountId, transactionType: TransactionType.INTERNAL }, + }) + .then((transactions) => { + return transactions + .reduce((total, tx) => new Decimal(total).plus(tx.transactionAmount), new Decimal(0)) + .toNumber(); + }); + } } diff --git a/src/card/services/account.service.ts b/src/card/services/account.service.ts index fb6e2f3..4c6af29 100644 --- a/src/card/services/account.service.ts +++ b/src/card/services/account.service.ts @@ -27,6 +27,14 @@ export class AccountService { return account; } + async getAccountByIban(iban: string): Promise { + const account = await this.accountRepository.getAccountByIban(iban); + if (!account) { + throw new UnprocessableEntityException('ACCOUNT.NOT_FOUND'); + } + return account; + } + creditAccountBalance(accountReference: string, amount: number) { return this.accountRepository.topUpAccountBalance(accountReference, amount); } @@ -52,4 +60,10 @@ export class AccountService { return this.accountRepository.decreaseAccountBalance(accountReference, amount); } + + //THIS IS A MOCK FUNCTION FOR TESTING PURPOSES ONLY + async fundIban(iban: string, amount: number) { + const account = await this.getAccountByIban(iban); + return this.accountRepository.topUpAccountBalance(account.accountReference, amount); + } } diff --git a/src/card/services/card.service.ts b/src/card/services/card.service.ts index 854160e..3aae9a2 100644 --- a/src/card/services/card.service.ts +++ b/src/card/services/card.service.ts @@ -119,6 +119,11 @@ export class CardService { @Transactional() async transferToChild(juniorId: string, amount: number) { const card = await this.getCardByCustomerId(juniorId); + const availableSpendingLimit = await this.transactionService.calculateAvailableSpendingLimitForParent(card.account); + + if (amount > availableSpendingLimit) { + throw new BadRequestException('CARD.INSUFFICIENT_BALANCE'); + } const finalAmount = Decimal(amount).plus(card.limit); await Promise.all([ @@ -129,4 +134,8 @@ export class CardService { return finalAmount.toNumber(); } + + 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 b48759c..839f370 100644 --- a/src/card/services/transaction.service.ts +++ b/src/card/services/transaction.service.ts @@ -5,6 +5,7 @@ import { AccountTransactionWebhookRequest, CardTransactionWebhookRequest, } from '~/common/modules/neoleap/dtos/requests'; +import { Account } from '../entities/account.entity'; import { Transaction } from '../entities/transaction.entity'; import { TransactionRepository } from '../repositories/transaction.repository'; import { AccountService } from './account.service'; @@ -57,6 +58,11 @@ export class TransactionService { return transaction; } + async calculateAvailableSpendingLimitForParent(account: Account): Promise { + const internalTransactionSum = await this.transactionRepository.findInternalTransactionTotal(account.id); + return new Decimal(account.balance).minus(internalTransactionSum).toNumber(); + } + private async findExistingTransaction(transactionId: string, accountReference: string): Promise { const existingTransaction = await this.transactionRepository.findTransactionByReference( transactionId, diff --git a/src/i18n/ar/app.json b/src/i18n/ar/app.json index 411141d..ea7f2f4 100644 --- a/src/i18n/ar/app.json +++ b/src/i18n/ar/app.json @@ -103,5 +103,8 @@ }, "OTP": { "INVALID_OTP": "رمز التحقق الذي أدخلته غير صالح. يرجى المحاولة مرة أخرى." + }, + "CARD": { + "INSUFFICIENT_BALANCE": "البطاقة لا تحتوي على رصيد كافٍ لإكمال هذا التحويل." } } diff --git a/src/i18n/en/app.json b/src/i18n/en/app.json index b6c4ccb..3373254 100644 --- a/src/i18n/en/app.json +++ b/src/i18n/en/app.json @@ -102,5 +102,8 @@ }, "OTP": { "INVALID_OTP": "The OTP you entered is invalid. Please try again." + }, + "CARD": { + "INSUFFICIENT_BALANCE": "The card does not have sufficient balance to complete this transfer." } }