mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2026-03-10 17:11:44 +00:00
Merge pull request #81 from Zod-Alkhair/feature/notification-system-fcm-registration
feat: enhance card service validation and notification integration
This commit is contained in:
@ -27,7 +27,7 @@ import { TransactionService } from './services/transaction.service';
|
||||
AccountService,
|
||||
AccountRepository,
|
||||
],
|
||||
exports: [CardService, TransactionService],
|
||||
exports: [CardService, TransactionService, AccountService],
|
||||
controllers: [CardsController],
|
||||
})
|
||||
export class CardModule {}
|
||||
|
||||
@ -216,14 +216,40 @@ export class CardService {
|
||||
throw new BadRequestException('CARD.INSUFFICIENT_BALANCE');
|
||||
}
|
||||
|
||||
const finalAmount = Decimal(amount).plus(card.limit);
|
||||
// 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, finalAmount.toNumber()),
|
||||
this.updateCardLimit(card.id, finalAmount.toNumber()),
|
||||
this.neoleapService.updateCardControl(card.cardReference, finalAmountNumber),
|
||||
this.updateCardLimit(card.id, finalAmountNumber),
|
||||
this.accountService.increaseReservedBalance(fundingAccount, amount),
|
||||
this.transactionService.createInternalChildTransaction(card.id, amount),
|
||||
]);
|
||||
|
||||
// Only create transaction and emit event after all operations succeed
|
||||
await this.transactionService.createInternalChildTransaction(card.id, amount);
|
||||
|
||||
return finalAmount.toNumber();
|
||||
}
|
||||
|
||||
|
||||
@ -427,10 +427,18 @@ export class NeoLeapService {
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.status === 400) {
|
||||
console.error('Error sending request to NeoLeap:', error);
|
||||
throw new BadRequestException(error.response?.data?.ResponseHeader?.ResponseDescription || error.message);
|
||||
const errorMessage = error.response?.data?.ResponseHeader?.ResponseDescription ||
|
||||
error.response?.data?.message ||
|
||||
error.message;
|
||||
const errorCode = error.response?.data?.ResponseHeader?.ResponseCode || 'UNKNOWN';
|
||||
this.logger.error(
|
||||
`NeoLeap API returned 400 error for endpoint ${endpoint}. ` +
|
||||
`Error Code: ${errorCode}, Message: ${errorMessage}. ` +
|
||||
`Payload: ${JSON.stringify(payload)}`
|
||||
);
|
||||
throw new BadRequestException(errorMessage || 'Request failed with status code 400');
|
||||
}
|
||||
console.error('Error sending request to NeoLeap:', error);
|
||||
this.logger.error(`Error sending request to NeoLeap endpoint ${endpoint}:`, error);
|
||||
throw new InternalServerErrorException('Error communicating with NeoLeap service');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import { NotificationFactory, NotificationPreferences } from '../services/notification-factory.service';
|
||||
import { UserService } from '~/user/services/user.service';
|
||||
import { AccountService } from '~/card/services/account.service';
|
||||
import { NOTIFICATION_EVENTS } from '../constants/event-names.constant';
|
||||
import { ITransactionCreatedEvent } from '../interfaces/notification-events.interface';
|
||||
import { NotificationScope } from '../enums/notification-scope.enum';
|
||||
@ -33,6 +34,8 @@ export class TransactionNotificationListener {
|
||||
private readonly notificationFactory: NotificationFactory,
|
||||
private readonly userService: UserService,
|
||||
private readonly i18n: I18nService,
|
||||
@Inject(forwardRef(() => AccountService))
|
||||
private readonly accountService: AccountService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -283,17 +286,28 @@ export class TransactionNotificationListener {
|
||||
const defaultChildName = this.i18n.t('app.NOTIFICATION.YOUR_CHILD', { lang: locale });
|
||||
const childName = childUser?.firstName || defaultChildName;
|
||||
const amount = transaction.transactionAmount;
|
||||
const balance = card.account?.balance || 0;
|
||||
|
||||
const parentCustomer = customer?.junior?.guardian?.customer;
|
||||
let parentAccount = parentCustomer?.cards?.[0]?.account;
|
||||
|
||||
if (!parentAccount && card.parentId) {
|
||||
try {
|
||||
parentAccount = await this.accountService.getAccountByCustomerId(card.parentId);
|
||||
} catch (error) {
|
||||
this.logger.warn(`Could not fetch parent account for customer ${card.parentId}, using child account balance`);
|
||||
}
|
||||
}
|
||||
|
||||
const balance = parentAccount?.balance || card.account?.balance || 0;
|
||||
const currency = getCurrency(
|
||||
card.account?.currency,
|
||||
parentAccount?.currency || card.account?.currency,
|
||||
transaction.transactionCurrency,
|
||||
'SAR'
|
||||
);
|
||||
const formattedAmount = formatCurrencyAmount(amount, currency);
|
||||
const formattedBalance = formatCurrencyAmount(balance, currency);
|
||||
|
||||
this.logger.debug(
|
||||
`Notifying parent (user ${parentUser.id}): Transferred ${formattedAmount} ${currency} to ${childName}`
|
||||
`Notifying parent (user ${parentUser.id}): Transferred ${formattedAmount} ${currency} to ${childName}, parent balance: ${formattedBalance} ${currency}`
|
||||
);
|
||||
|
||||
let title: string;
|
||||
@ -304,10 +318,10 @@ export class TransactionNotificationListener {
|
||||
message = this.i18n.t('app.NOTIFICATION.PARENT_TOP_UP_MESSAGE', {
|
||||
lang: locale,
|
||||
args: {
|
||||
amount: amount.toString(),
|
||||
amount: formattedAmount,
|
||||
currency: currency,
|
||||
childName: childName,
|
||||
balance: balance.toString(),
|
||||
balance: formattedBalance,
|
||||
},
|
||||
});
|
||||
} catch (i18nError: any) {
|
||||
@ -327,9 +341,9 @@ export class TransactionNotificationListener {
|
||||
transactionId: transaction.id,
|
||||
childId: childUser.id,
|
||||
childName: childName,
|
||||
amount: amount.toString(),
|
||||
amount: formattedAmount,
|
||||
currency: currency,
|
||||
balance: balance.toString(),
|
||||
balance: formattedBalance,
|
||||
timestamp: transaction.transactionDate.toISOString(),
|
||||
type: 'TOP_UP',
|
||||
action: 'OPEN_TRANSACTION',
|
||||
|
||||
@ -3,6 +3,7 @@ import { forwardRef, Module } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { TwilioModule } from 'nestjs-twilio';
|
||||
import { CardModule } from '~/card/card.module';
|
||||
import { RedisModule } from '~/common/redis/redis.module';
|
||||
import { buildMailerOptions, buildTwilioOptions } from '~/core/module-options';
|
||||
import { UserModule } from '~/user/user.module';
|
||||
@ -25,6 +26,7 @@ import { MessagingSystemFactory, RedisPubSubMessagingService } from './services/
|
||||
imports: [
|
||||
forwardRef(() => RedisModule.register()),
|
||||
forwardRef(() => UserModule),
|
||||
forwardRef(() => CardModule),
|
||||
TypeOrmModule.forFeature([Notification]),
|
||||
TwilioModule.forRootAsync({
|
||||
useFactory: buildTwilioOptions,
|
||||
|
||||
@ -6,7 +6,7 @@ import { OtpType } from '../../otp/enums';
|
||||
import { ISendOtp } from '../../otp/interfaces';
|
||||
import { SendEmailRequestDto } from '../dtos/request';
|
||||
import { Notification } from '../entities';
|
||||
import { EventType, NotificationChannel, NotificationScope } from '../enums';
|
||||
import { EventType, NotificationChannel, NotificationScope, NotificationStatus } from '../enums';
|
||||
import { NotificationsRepository } from '../repositories';
|
||||
import { MessagingSystemFactory } from './messaging/messaging-system-factory.service';
|
||||
|
||||
@ -35,7 +35,10 @@ export class NotificationsService {
|
||||
|
||||
async createNotification(notification: Partial<Notification>) {
|
||||
this.logger.log(`Creating notification for user ${notification.userId}`);
|
||||
const savedNotification = await this.notificationRepository.createNotification(notification);
|
||||
const savedNotification = await this.notificationRepository.createNotification({
|
||||
...notification,
|
||||
status: notification.status || NotificationStatus.UNREAD,
|
||||
});
|
||||
|
||||
const scope = notification.scope || NotificationScope.USER_REGISTERED;
|
||||
const messagingSystem = this.messagingSystemFactory.getMessagingSystem(scope);
|
||||
|
||||
Reference in New Issue
Block a user