mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2026-03-10 18:41:46 +00:00
- Added functionality to credit child account balance and decrease parent account balance in CardService. - Updated TransactionService to reload card details for accurate balance after transactions. - Improved TransactionNotificationListener to fetch updated balances for both child and parent accounts, ensuring accurate notifications.
281 lines
11 KiB
TypeScript
281 lines
11 KiB
TypeScript
import { BadRequestException, forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
|
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
import Decimal from 'decimal.js';
|
|
import { Transactional } from 'typeorm-transactional';
|
|
import { NOTIFICATION_EVENTS } from '~/common/modules/notification/constants/event-names.constant';
|
|
import { ICardBlockedEvent, ICardCreatedEvent } from '~/common/modules/notification/interfaces/notification-events.interface';
|
|
import { AccountCardStatusChangedWebhookRequest } from '~/common/modules/neoleap/dtos/requests';
|
|
import { NeoLeapService } from '~/common/modules/neoleap/services';
|
|
import { Customer } from '~/customer/entities';
|
|
import { KycStatus } from '~/customer/enums';
|
|
import { CustomerService } from '~/customer/services';
|
|
import { OciService } from '~/document/services';
|
|
import { Card } from '../entities';
|
|
import { CardColors, CardStatus } 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 {
|
|
private readonly logger = new Logger(CardService.name);
|
|
constructor(
|
|
private readonly cardRepository: CardRepository,
|
|
private readonly accountService: AccountService,
|
|
private readonly ociService: OciService,
|
|
@Inject(forwardRef(() => TransactionService)) private readonly transactionService: TransactionService,
|
|
@Inject(forwardRef(() => NeoLeapService)) private readonly neoleapService: NeoLeapService,
|
|
@Inject(forwardRef(() => CustomerService)) private readonly customerService: CustomerService,
|
|
private readonly eventEmitter: EventEmitter2,
|
|
) {}
|
|
|
|
@Transactional()
|
|
async createCard(customerId: string): Promise<Card> {
|
|
const customer = await this.customerService.findCustomerById(customerId);
|
|
|
|
if (customer.kycStatus !== KycStatus.APPROVED) {
|
|
throw new BadRequestException('CUSTOMER.KYC_NOT_APPROVED');
|
|
}
|
|
|
|
if (!customer.neoleapExternalCustomerId) {
|
|
throw new BadRequestException('CUSTOMER.KYC_NOT_COMPLETED');
|
|
}
|
|
|
|
if (customer.cards.length > 0) {
|
|
throw new BadRequestException('CUSTOMER.ALREADY_HAS_CARD');
|
|
}
|
|
|
|
// Validate required fields for card creation
|
|
const missingFields = [];
|
|
if (!customer.nationalId) missingFields.push('nationalId');
|
|
if (!customer.dateOfBirth) missingFields.push('dateOfBirth');
|
|
if (!customer.nationalIdExpiry) missingFields.push('nationalIdExpiry');
|
|
|
|
if (missingFields.length > 0) {
|
|
throw new BadRequestException(
|
|
`CUSTOMER.MISSING_REQUIRED_FIELDS: ${missingFields.join(', ')}. Please complete your profile.`
|
|
);
|
|
}
|
|
|
|
const data = await this.neoleapService.createApplication(customer);
|
|
const account = await this.accountService.createAccount(data);
|
|
const createdCard = await this.cardRepository.createCard(customerId, account.id, data);
|
|
|
|
const cardWithRelations = await this.getCardById(createdCard.id);
|
|
|
|
const event: ICardCreatedEvent = {
|
|
card: cardWithRelations,
|
|
timestamp: new Date(),
|
|
};
|
|
this.eventEmitter.emit(NOTIFICATION_EVENTS.CARD_CREATED, event);
|
|
this.logger.log(`Emitted CARD_CREATED event for card ${cardWithRelations.id}`);
|
|
|
|
return cardWithRelations;
|
|
}
|
|
|
|
async getChildCards(guardianId: string): Promise<Card[]> {
|
|
const cards = await this.cardRepository.findChildCardsForGuardian(guardianId);
|
|
await this.prepareJuniorImages(cards);
|
|
return cards;
|
|
}
|
|
|
|
async createCardForChild(parentCustomer: Customer, childCustomer: Customer, cardColor: CardColors, cardPin: string) {
|
|
const data = await this.neoleapService.createChildCard(parentCustomer, childCustomer, cardPin);
|
|
const createdCard = await this.cardRepository.createCard(
|
|
childCustomer.id,
|
|
parentCustomer.cards[0].account.id,
|
|
data,
|
|
cardColor,
|
|
parentCustomer.id,
|
|
);
|
|
|
|
const cardWithRelations = await this.getCardById(createdCard.id);
|
|
|
|
const event: ICardCreatedEvent = {
|
|
card: cardWithRelations,
|
|
timestamp: new Date(),
|
|
};
|
|
this.eventEmitter.emit(NOTIFICATION_EVENTS.CARD_CREATED, event);
|
|
this.logger.log(`Emitted CARD_CREATED event for child card ${cardWithRelations.id}`);
|
|
|
|
return cardWithRelations;
|
|
}
|
|
|
|
async getCardByChildId(guardianId: string, childId: string): Promise<Card> {
|
|
const card = await this.cardRepository.findCardByChildId(guardianId, childId);
|
|
if (!card) {
|
|
throw new BadRequestException('CARD.NOT_FOUND');
|
|
}
|
|
await this.prepareJuniorImages([card]);
|
|
return card;
|
|
}
|
|
async getCardById(id: string): Promise<Card> {
|
|
const card = await this.cardRepository.getCardById(id);
|
|
|
|
if (!card) {
|
|
throw new BadRequestException('CARD.NOT_FOUND');
|
|
}
|
|
|
|
return card;
|
|
}
|
|
|
|
async getCardByReferenceNumber(referenceNumber: string): Promise<Card> {
|
|
const card = await this.cardRepository.getCardByReferenceNumber(referenceNumber);
|
|
|
|
if (!card) {
|
|
throw new BadRequestException('CARD.NOT_FOUND');
|
|
}
|
|
|
|
return card;
|
|
}
|
|
|
|
async getCardByVpan(vpan: string): Promise<Card> {
|
|
const card = await this.cardRepository.getCardByVpan(vpan);
|
|
|
|
if (!card) {
|
|
throw new BadRequestException('CARD.NOT_FOUND');
|
|
}
|
|
return card;
|
|
}
|
|
|
|
async getCardByCustomerId(customerId: string): Promise<Card> {
|
|
const card = await this.cardRepository.getCardByCustomerId(customerId);
|
|
if (!card) {
|
|
throw new BadRequestException('CARD.NOT_FOUND');
|
|
}
|
|
|
|
return card;
|
|
}
|
|
|
|
async updateCardStatus(body: AccountCardStatusChangedWebhookRequest) {
|
|
const card = await this.getCardByVpan(body.cardId);
|
|
const previousStatus = card.status;
|
|
const { description, status } = CardStatusMapper[body.newStatus] || CardStatusMapper['99'];
|
|
|
|
await this.cardRepository.updateCardStatus(card.id, status, description);
|
|
|
|
if (status === CardStatus.BLOCKED) {
|
|
const updatedCard = await this.getCardById(card.id);
|
|
const event: ICardBlockedEvent = {
|
|
card: updatedCard,
|
|
previousStatus,
|
|
blockReason: description,
|
|
timestamp: new Date(),
|
|
};
|
|
this.eventEmitter.emit(NOTIFICATION_EVENTS.CARD_BLOCKED, event);
|
|
this.logger.log(`Emitted CARD_BLOCKED event for card ${updatedCard.id}`);
|
|
}
|
|
|
|
return { id: card.id, status, description };
|
|
}
|
|
|
|
async getEmbossingInformation(customerId: string) {
|
|
const card = await this.getCardByCustomerId(customerId);
|
|
|
|
return this.neoleapService.getEmbossingInformation(card);
|
|
}
|
|
|
|
async getChildCardEmbossingInformation(cardId: string, guardianId: string) {
|
|
const card = await this.getCardById(cardId);
|
|
if (card.parentId !== guardianId) {
|
|
throw new BadRequestException('CARD.DOES_NOT_BELONG_TO_GUARDIAN');
|
|
}
|
|
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');
|
|
}
|
|
}
|
|
|
|
async getIbanInformation(customerId: string) {
|
|
const account = await this.accountService.getAccountByCustomerId(customerId);
|
|
return account.iban;
|
|
}
|
|
|
|
@Transactional()
|
|
async transferToChild(juniorId: string, amount: number) {
|
|
const card = await this.getCardByCustomerId(juniorId);
|
|
|
|
this.logger.debug(`Transfer to child - juniorId: ${juniorId}, parentId: ${card.parentId}, cardId: ${card.id}`);
|
|
this.logger.debug(`Card account - balance: ${card.account.balance}, reserved: ${card.account.reservedBalance}`);
|
|
|
|
const fundingAccount = card.parentId
|
|
? await this.accountService.getAccountByCustomerId(card.parentId)
|
|
: card.account;
|
|
|
|
this.logger.debug(`Funding account - balance: ${fundingAccount.balance}, reserved: ${fundingAccount.reservedBalance}, available: ${fundingAccount.balance - fundingAccount.reservedBalance}`);
|
|
this.logger.debug(`Amount requested: ${amount}`);
|
|
|
|
if (amount > fundingAccount.balance - fundingAccount.reservedBalance) {
|
|
this.logger.error(`Insufficient balance - requested: ${amount}, available: ${fundingAccount.balance - fundingAccount.reservedBalance}`);
|
|
throw new BadRequestException('CARD.INSUFFICIENT_BALANCE');
|
|
}
|
|
|
|
// Validate card reference exists
|
|
if (!card.cardReference) {
|
|
this.logger.error(`Card ${card.id} does not have a cardReference`);
|
|
throw new BadRequestException('CARD.INVALID_CARD_REFERENCE');
|
|
}
|
|
|
|
// Validate card limit is a valid number
|
|
const cardLimit = card.limit || 0;
|
|
if (isNaN(cardLimit) || cardLimit < 0) {
|
|
this.logger.error(`Card ${card.id} has invalid limit: ${cardLimit}`);
|
|
throw new BadRequestException('CARD.INVALID_CARD_LIMIT');
|
|
}
|
|
|
|
const finalAmount = Decimal(amount).plus(cardLimit);
|
|
const finalAmountNumber = finalAmount.toNumber();
|
|
|
|
// Validate final amount is positive
|
|
if (finalAmountNumber <= 0 || !isFinite(finalAmountNumber)) {
|
|
this.logger.error(`Invalid final amount calculated: ${finalAmountNumber} (amount: ${amount}, limit: ${cardLimit})`);
|
|
throw new BadRequestException('CARD.INVALID_AMOUNT');
|
|
}
|
|
|
|
this.logger.debug(`Updating card control - cardReference: ${card.cardReference}, finalAmount: ${finalAmountNumber}`);
|
|
|
|
// First, ensure all external operations succeed before creating transaction
|
|
await Promise.all([
|
|
this.neoleapService.updateCardControl(card.cardReference, finalAmountNumber),
|
|
this.updateCardLimit(card.id, finalAmountNumber),
|
|
this.accountService.increaseReservedBalance(fundingAccount, amount),
|
|
// Increase child account balance
|
|
this.accountService.creditAccountBalance(card.account.accountReference, amount),
|
|
// Decrease parent account balance (only if parent is funding)
|
|
card.parentId ? this.accountService.decreaseAccountBalance(fundingAccount.accountReference, amount) : Promise.resolve(),
|
|
]);
|
|
|
|
// Only create transaction and emit event after all operations succeed
|
|
await this.transactionService.createInternalChildTransaction(card.id, amount);
|
|
|
|
return finalAmount.toNumber();
|
|
}
|
|
|
|
getWeeklySummary(juniorId: string, startDate?: Date, endDate?: Date) {
|
|
return this.transactionService.getWeeklySummary(juniorId, startDate, endDate);
|
|
}
|
|
|
|
fundIban(iban: string, amount: number) {
|
|
return this.accountService.fundIban(iban, amount);
|
|
}
|
|
|
|
private async prepareJuniorImages(cards: Card[]) {
|
|
this.logger.log(`Preparing junior images`);
|
|
await Promise.all(
|
|
cards.map(async (card) => {
|
|
const profilePicture = card.customer?.user?.profilePicture;
|
|
|
|
if (profilePicture) {
|
|
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
}
|