mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-11-26 00:24:54 +00:00
fix: validate card spending limit before transfering to child
This commit is contained in:
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
7
src/card/dtos/requests/fund-iban.request.dto.ts
Normal file
7
src/card/dtos/requests/fund-iban.request.dto.ts
Normal file
@ -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;
|
||||
}
|
||||
1
src/card/dtos/requests/index.ts
Normal file
1
src/card/dtos/requests/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './fund-iban.request.dto';
|
||||
@ -27,6 +27,13 @@ export class AccountRepository {
|
||||
});
|
||||
}
|
||||
|
||||
getAccountByIban(iban: string): Promise<Account | null> {
|
||||
return this.accountRepository.findOne({
|
||||
where: { iban },
|
||||
relations: ['cards'],
|
||||
});
|
||||
}
|
||||
|
||||
getAccountByAccountNumber(accountNumber: string): Promise<Account | null> {
|
||||
return this.accountRepository.findOne({
|
||||
where: { accountNumber },
|
||||
|
||||
@ -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<number> {
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,14 @@ export class AccountService {
|
||||
return account;
|
||||
}
|
||||
|
||||
async getAccountByIban(iban: string): Promise<Account> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<number> {
|
||||
const internalTransactionSum = await this.transactionRepository.findInternalTransactionTotal(account.id);
|
||||
return new Decimal(account.balance).minus(internalTransactionSum).toNumber();
|
||||
}
|
||||
|
||||
private async findExistingTransaction(transactionId: string, accountReference: string): Promise<Transaction | null> {
|
||||
const existingTransaction = await this.transactionRepository.findTransactionByReference(
|
||||
transactionId,
|
||||
|
||||
@ -103,5 +103,8 @@
|
||||
},
|
||||
"OTP": {
|
||||
"INVALID_OTP": "رمز التحقق الذي أدخلته غير صالح. يرجى المحاولة مرة أخرى."
|
||||
},
|
||||
"CARD": {
|
||||
"INSUFFICIENT_BALANCE": "البطاقة لا تحتوي على رصيد كافٍ لإكمال هذا التحويل."
|
||||
}
|
||||
}
|
||||
|
||||
@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user