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:
Majdalkilany0
2026-01-14 13:27:56 +03:00
committed by GitHub
6 changed files with 72 additions and 19 deletions

View File

@ -27,7 +27,7 @@ import { TransactionService } from './services/transaction.service';
AccountService,
AccountRepository,
],
exports: [CardService, TransactionService],
exports: [CardService, TransactionService, AccountService],
controllers: [CardsController],
})
export class CardModule {}

View File

@ -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();
}

View File

@ -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');
}
}

View File

@ -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',

View File

@ -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,

View File

@ -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);