mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-11-26 08:34:55 +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 { Body, Controller, Get, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common';
|
||||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { Roles } from '~/auth/enums';
|
import { Roles } from '~/auth/enums';
|
||||||
import { IJwtPayload } from '~/auth/interfaces';
|
import { IJwtPayload } from '~/auth/interfaces';
|
||||||
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
|
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 { CardEmbossingDetailsResponseDto } from '~/common/modules/neoleap/dtos/response';
|
||||||
import { ApiDataResponse } from '~/core/decorators';
|
import { ApiDataResponse } from '~/core/decorators';
|
||||||
import { ResponseFactory } from '~/core/utils';
|
import { ResponseFactory } from '~/core/utils';
|
||||||
|
import { FundIbanRequestDto } from '../dtos/requests';
|
||||||
import { AccountIbanResponseDto, CardResponseDto } from '../dtos/responses';
|
import { AccountIbanResponseDto, CardResponseDto } from '../dtos/responses';
|
||||||
import { CardService } from '../services';
|
import { CardService } from '../services';
|
||||||
|
|
||||||
@ -46,4 +47,13 @@ export class CardsController {
|
|||||||
const iban = await this.cardService.getIbanInformation(sub);
|
const iban = await this.cardService.getIbanInformation(sub);
|
||||||
return ResponseFactory.data(new AccountIbanResponseDto(iban));
|
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> {
|
getAccountByAccountNumber(accountNumber: string): Promise<Account | null> {
|
||||||
return this.accountRepository.findOne({
|
return this.accountRepository.findOne({
|
||||||
where: { accountNumber },
|
where: { accountNumber },
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Decimal } from 'decimal.js';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import {
|
import {
|
||||||
@ -84,4 +85,16 @@ export class TransactionRepository {
|
|||||||
where: { transactionId, accountReference },
|
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;
|
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) {
|
creditAccountBalance(accountReference: string, amount: number) {
|
||||||
return this.accountRepository.topUpAccountBalance(accountReference, amount);
|
return this.accountRepository.topUpAccountBalance(accountReference, amount);
|
||||||
}
|
}
|
||||||
@ -52,4 +60,10 @@ export class AccountService {
|
|||||||
|
|
||||||
return this.accountRepository.decreaseAccountBalance(accountReference, amount);
|
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()
|
@Transactional()
|
||||||
async transferToChild(juniorId: string, amount: number) {
|
async transferToChild(juniorId: string, amount: number) {
|
||||||
const card = await this.getCardByCustomerId(juniorId);
|
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);
|
const finalAmount = Decimal(amount).plus(card.limit);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@ -129,4 +134,8 @@ export class CardService {
|
|||||||
|
|
||||||
return finalAmount.toNumber();
|
return finalAmount.toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fundIban(iban: string, amount: number) {
|
||||||
|
return this.accountService.fundIban(iban, amount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
AccountTransactionWebhookRequest,
|
AccountTransactionWebhookRequest,
|
||||||
CardTransactionWebhookRequest,
|
CardTransactionWebhookRequest,
|
||||||
} from '~/common/modules/neoleap/dtos/requests';
|
} from '~/common/modules/neoleap/dtos/requests';
|
||||||
|
import { Account } from '../entities/account.entity';
|
||||||
import { Transaction } from '../entities/transaction.entity';
|
import { Transaction } from '../entities/transaction.entity';
|
||||||
import { TransactionRepository } from '../repositories/transaction.repository';
|
import { TransactionRepository } from '../repositories/transaction.repository';
|
||||||
import { AccountService } from './account.service';
|
import { AccountService } from './account.service';
|
||||||
@ -57,6 +58,11 @@ export class TransactionService {
|
|||||||
return transaction;
|
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> {
|
private async findExistingTransaction(transactionId: string, accountReference: string): Promise<Transaction | null> {
|
||||||
const existingTransaction = await this.transactionRepository.findTransactionByReference(
|
const existingTransaction = await this.transactionRepository.findTransactionByReference(
|
||||||
transactionId,
|
transactionId,
|
||||||
|
|||||||
@ -103,5 +103,8 @@
|
|||||||
},
|
},
|
||||||
"OTP": {
|
"OTP": {
|
||||||
"INVALID_OTP": "رمز التحقق الذي أدخلته غير صالح. يرجى المحاولة مرة أخرى."
|
"INVALID_OTP": "رمز التحقق الذي أدخلته غير صالح. يرجى المحاولة مرة أخرى."
|
||||||
|
},
|
||||||
|
"CARD": {
|
||||||
|
"INSUFFICIENT_BALANCE": "البطاقة لا تحتوي على رصيد كافٍ لإكمال هذا التحويل."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,5 +102,8 @@
|
|||||||
},
|
},
|
||||||
"OTP": {
|
"OTP": {
|
||||||
"INVALID_OTP": "The OTP you entered is invalid. Please try again."
|
"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