diff --git a/src/card/card.module.ts b/src/card/card.module.ts index 896a435..329c2ae 100644 --- a/src/card/card.module.ts +++ b/src/card/card.module.ts @@ -27,7 +27,7 @@ import { TransactionService } from './services/transaction.service'; AccountService, AccountRepository, ], - exports: [CardService, TransactionService], + exports: [CardService, TransactionService, AccountService], controllers: [CardsController], }) export class CardModule {} diff --git a/src/card/services/card.service.ts b/src/card/services/card.service.ts index 1393fa1..2638d58 100644 --- a/src/card/services/card.service.ts +++ b/src/card/services/card.service.ts @@ -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(); } diff --git a/src/common/modules/neoleap/services/neoleap.service.ts b/src/common/modules/neoleap/services/neoleap.service.ts index 443c604..e4a74f0 100644 --- a/src/common/modules/neoleap/services/neoleap.service.ts +++ b/src/common/modules/neoleap/services/neoleap.service.ts @@ -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'); } } diff --git a/src/common/modules/notification/listeners/transaction-notification.listener.ts b/src/common/modules/notification/listeners/transaction-notification.listener.ts index dc1d4ea..ac13712 100644 --- a/src/common/modules/notification/listeners/transaction-notification.listener.ts +++ b/src/common/modules/notification/listeners/transaction-notification.listener.ts @@ -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', diff --git a/src/common/modules/notification/notification.module.ts b/src/common/modules/notification/notification.module.ts index 1ab1708..502e4d9 100644 --- a/src/common/modules/notification/notification.module.ts +++ b/src/common/modules/notification/notification.module.ts @@ -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, diff --git a/src/common/modules/notification/services/notifications.service.ts b/src/common/modules/notification/services/notifications.service.ts index 3f4fd07..1ec8590 100644 --- a/src/common/modules/notification/services/notifications.service.ts +++ b/src/common/modules/notification/services/notifications.service.ts @@ -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) { 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);