diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts index 53bb6bd..341b372 100644 --- a/src/auth/services/auth.service.ts +++ b/src/auth/services/auth.service.ts @@ -314,33 +314,54 @@ export class AuthService { /** * Register or update device with FCM token - * This method handles both new device registration and existing device updates + * This method handles: + * 1. Device already exists for this user → Update FCM token + * 2. Device exists for different user → Transfer device to new user + * 3. Device doesn't exist → Create new device */ private async registerDeviceToken(userId: string, deviceId: string, fcmToken: string): Promise { try { this.logger.log(`Registering/updating device ${deviceId} with FCM token for user ${userId}`); - // Check if device already exists for this user - const existingDevice = await this.deviceService.findUserDeviceById(deviceId, userId); + // Step 1: Check if device already exists for this user + const existingDeviceForUser = await this.deviceService.findUserDeviceById(deviceId, userId); - if (existingDevice) { - // Update existing device with new FCM token and last access time + if (existingDeviceForUser) { + // Device exists for this user → Update FCM token and last access time await this.deviceService.updateDevice(deviceId, { fcmToken, userId, lastAccessOn: new Date(), }); this.logger.log(`Device ${deviceId} updated with new FCM token for user ${userId}`); - } else { - // Create new device - await this.deviceService.createDevice({ - deviceId, + return; + } + + // Step 2: Check if device exists for any user (different user scenario) + const existingDevice = await this.deviceService.findByDeviceId(deviceId); + + if (existingDevice) { + // Device exists for different user → Transfer device to new user + this.logger.log( + `Device ${deviceId} exists for user ${existingDevice.userId}, transferring to user ${userId}` + ); + await this.deviceService.updateDevice(deviceId, { userId, fcmToken, lastAccessOn: new Date(), }); - this.logger.log(`New device ${deviceId} registered with FCM token for user ${userId}`); + this.logger.log(`Device ${deviceId} transferred from user ${existingDevice.userId} to user ${userId}`); + return; } + + // Step 3: Device doesn't exist → Create new device + await this.deviceService.createDevice({ + deviceId, + userId, + fcmToken, + lastAccessOn: new Date(), + }); + this.logger.log(`New device ${deviceId} registered with FCM token for user ${userId}`); } catch (error) { // Log error but don't fail the login/signup process const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/src/common/modules/notification/constants/event-names.constant.ts b/src/common/modules/notification/constants/event-names.constant.ts index 681763d..9854c6e 100644 --- a/src/common/modules/notification/constants/event-names.constant.ts +++ b/src/common/modules/notification/constants/event-names.constant.ts @@ -5,6 +5,11 @@ export const NOTIFICATION_EVENTS = { // Transaction events TRANSACTION_CREATED: 'notification.transaction.created', + + // Money Request events + MONEY_REQUEST_CREATED: 'notification.money-request.created', + MONEY_REQUEST_APPROVED: 'notification.money-request.approved', + MONEY_REQUEST_DECLINED: 'notification.money-request.declined', } as const; export type NotificationEventName = diff --git a/src/common/modules/notification/enums/notification-scope.enum.ts b/src/common/modules/notification/enums/notification-scope.enum.ts index c023127..dc9bee0 100644 --- a/src/common/modules/notification/enums/notification-scope.enum.ts +++ b/src/common/modules/notification/enums/notification-scope.enum.ts @@ -13,6 +13,11 @@ export enum NotificationScope { // Transaction notifications - Spending CHILD_SPENDING = 'CHILD_SPENDING', PARENT_SPENDING_ALERT = 'PARENT_SPENDING_ALERT', + + // Money Request notifications + MONEY_REQUEST_CREATED = 'MONEY_REQUEST_CREATED', + MONEY_REQUEST_APPROVED = 'MONEY_REQUEST_APPROVED', + MONEY_REQUEST_DECLINED = 'MONEY_REQUEST_DECLINED', } /** diff --git a/src/common/modules/notification/interfaces/notification-events.interface.ts b/src/common/modules/notification/interfaces/notification-events.interface.ts index 953da85..d1e675d 100644 --- a/src/common/modules/notification/interfaces/notification-events.interface.ts +++ b/src/common/modules/notification/interfaces/notification-events.interface.ts @@ -1,5 +1,6 @@ import { Transaction } from '~/card/entities/transaction.entity'; import { Card } from '~/card/entities/card.entity'; +import { MoneyRequest } from '~/money-request/entities/money-request.entity'; /** * Event payload for when a transaction is created @@ -22,3 +23,41 @@ export interface ITransactionCreatedEvent { timestamp: Date; } +/** + * Event payload for when a money request is created + * Used to notify parents when their child requests money + */ +export interface IMoneyRequestCreatedEvent { + /** The money request that was created */ + moneyRequest: MoneyRequest; + + /** When the event occurred */ + timestamp: Date; +} + +/** + * Event payload for when a money request is approved + * Used to notify children when their money request is approved + */ +export interface IMoneyRequestApprovedEvent { + /** The money request that was approved */ + moneyRequest: MoneyRequest; + + /** When the event occurred */ + timestamp: Date; +} + +/** + * Event payload for when a money request is declined + * Used to notify children when their money request is declined + */ +export interface IMoneyRequestDeclinedEvent { + /** The money request that was declined */ + moneyRequest: MoneyRequest; + + /** Rejection reason provided by parent */ + rejectionReason?: string; + + /** When the event occurred */ + timestamp: Date; +} diff --git a/src/common/modules/notification/listeners/index.ts b/src/common/modules/notification/listeners/index.ts index a582647..ff054b8 100644 --- a/src/common/modules/notification/listeners/index.ts +++ b/src/common/modules/notification/listeners/index.ts @@ -1,2 +1,3 @@ export * from './notification-created.listener'; export * from './transaction-notification.listener'; +export * from './money-request-notification.listener'; diff --git a/src/common/modules/notification/listeners/money-request-notification.listener.ts b/src/common/modules/notification/listeners/money-request-notification.listener.ts new file mode 100644 index 0000000..20587b9 --- /dev/null +++ b/src/common/modules/notification/listeners/money-request-notification.listener.ts @@ -0,0 +1,264 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { NotificationFactory, NotificationPreferences } from '../services/notification-factory.service'; +import { UserService } from '~/user/services/user.service'; +import { NOTIFICATION_EVENTS } from '../constants/event-names.constant'; +import { + IMoneyRequestApprovedEvent, + IMoneyRequestCreatedEvent, + IMoneyRequestDeclinedEvent, +} from '../interfaces/notification-events.interface'; +import { NotificationScope } from '../enums/notification-scope.enum'; +import { User } from '~/user/entities'; + +/** + * MoneyRequestNotificationListener + * + * Handles notifications for money request events. + * Notifies parents when children request money, and children when requests are approved/declined. + * + * Responsibilities: + * - Listen for money request events (created, approved, declined) + * - Determine notification recipients (parent or child) + * - Construct appropriate messages + * - Fetch user preferences + * - Call NotificationFactory to send + */ +@Injectable() +export class MoneyRequestNotificationListener { + private readonly logger = new Logger(MoneyRequestNotificationListener.name); + + constructor( + private readonly notificationFactory: NotificationFactory, + private readonly userService: UserService, + ) {} + + /** + * Handle money request created event + * Notifies parent when child requests money + */ + @OnEvent(NOTIFICATION_EVENTS.MONEY_REQUEST_CREATED) + async handleMoneyRequestCreated(event: IMoneyRequestCreatedEvent): Promise { + try { + const { moneyRequest } = event; + + this.logger.log( + `Processing money request notification for request ${moneyRequest.id} - ` + + `Amount: $${moneyRequest.amount}, Reason: ${moneyRequest.reason}` + ); + + await this.notifyParentOfMoneyRequest(moneyRequest); + + this.logger.log( + `Money request notification processed successfully for request ${moneyRequest.id}` + ); + } catch (error: any) { + this.logger.error( + `Failed to process money request notification: ${error?.message || 'Unknown error'}`, + error?.stack + ); + } + } + + /** + * Handle money request approved event + * Notifies child when their money request is approved + */ + @OnEvent(NOTIFICATION_EVENTS.MONEY_REQUEST_APPROVED) + async handleMoneyRequestApproved(event: IMoneyRequestApprovedEvent): Promise { + try { + const { moneyRequest } = event; + + this.logger.log( + `Processing money request approved notification for request ${moneyRequest.id}` + ); + + await this.notifyChildOfApproval(moneyRequest); + + this.logger.log( + `Money request approved notification processed successfully for request ${moneyRequest.id}` + ); + } catch (error: any) { + this.logger.error( + `Failed to process money request approved notification: ${error?.message || 'Unknown error'}`, + error?.stack + ); + } + } + + /** + * Handle money request declined event + * Notifies child when their money request is declined + */ + @OnEvent(NOTIFICATION_EVENTS.MONEY_REQUEST_DECLINED) + async handleMoneyRequestDeclined(event: IMoneyRequestDeclinedEvent): Promise { + try { + const { moneyRequest, rejectionReason } = event; + + this.logger.log( + `Processing money request declined notification for request ${moneyRequest.id}` + ); + + await this.notifyChildOfRejection(moneyRequest, rejectionReason); + + this.logger.log( + `Money request declined notification processed successfully for request ${moneyRequest.id}` + ); + } catch (error: any) { + this.logger.error( + `Failed to process money request declined notification: ${error?.message || 'Unknown error'}`, + error?.stack + ); + } + } + + /** + * Notify parent when child requests money + */ + private async notifyParentOfMoneyRequest(moneyRequest: any): Promise { + try { + const guardian = moneyRequest?.guardian; + const parentUser = guardian?.customer?.user; + + if (!parentUser) { + this.logger.warn(`No parent user found for money request ${moneyRequest.id}, skipping notification`); + return; + } + + const child = moneyRequest?.junior; + const childUser = child?.customer?.user; + const childName = childUser?.firstName || 'Your child'; + const amount = moneyRequest.amount; + const reason = moneyRequest.reason || 'No reason provided'; + + this.logger.debug( + `Notifying parent (user ${parentUser.id}): ${childName} requested $${amount} - ${reason}` + ); + + await this.notificationFactory.send({ + userId: parentUser.id, + title: 'Money Request', + message: `${childName} requested $${amount.toFixed(2)}. Reason: ${reason}`, + scope: NotificationScope.MONEY_REQUEST_CREATED, + preferences: this.getUserPreferences(parentUser), + data: { + moneyRequestId: moneyRequest.id, + childId: childUser?.id, + childName: childName, + amount: amount.toString(), + reason: reason, + timestamp: moneyRequest.createdAt.toISOString(), + type: 'MONEY_REQUEST', + action: 'VIEW_MONEY_REQUEST', + }, + }); + + this.logger.log(`✅ Notified parent ${parentUser.id} about money request ${moneyRequest.id}`); + } catch (error: any) { + this.logger.error( + `Failed to notify parent of money request: ${error?.message || 'Unknown error'}`, + error?.stack + ); + } + } + + /** + * Notify child when their money request is approved + */ + private async notifyChildOfApproval(moneyRequest: any): Promise { + try { + const child = moneyRequest?.junior; + const childUser = child?.customer?.user; + + if (!childUser) { + this.logger.warn(`No child user found for money request ${moneyRequest.id}, skipping notification`); + return; + } + + const amount = moneyRequest.amount; + + this.logger.debug( + `Notifying child (user ${childUser.id}): Money request of $${amount} was approved` + ); + + await this.notificationFactory.send({ + userId: childUser.id, + title: 'Money Request Approved', + message: `Your request for $${amount.toFixed(2)} has been approved. The money has been added to your account.`, + scope: NotificationScope.MONEY_REQUEST_APPROVED, + preferences: this.getUserPreferences(childUser), + data: { + moneyRequestId: moneyRequest.id, + amount: amount.toString(), + timestamp: moneyRequest.updatedAt.toISOString(), + type: 'MONEY_REQUEST_APPROVED', + action: 'VIEW_MONEY_REQUEST', + }, + }); + + this.logger.log(`✅ Notified child ${childUser.id} about approved money request ${moneyRequest.id}`); + } catch (error: any) { + this.logger.error( + `Failed to notify child of approval: ${error?.message || 'Unknown error'}`, + error?.stack + ); + } + } + + /** + * Notify child when their money request is declined + */ + private async notifyChildOfRejection(moneyRequest: any, rejectionReason?: string): Promise { + try { + const child = moneyRequest?.junior; + const childUser = child?.customer?.user; + + if (!childUser) { + this.logger.warn(`No child user found for money request ${moneyRequest.id}, skipping notification`); + return; + } + + const amount = moneyRequest.amount; + const reason = rejectionReason || 'No reason provided'; + + this.logger.debug( + `Notifying child (user ${childUser.id}): Money request of $${amount} was declined` + ); + + await this.notificationFactory.send({ + userId: childUser.id, + title: 'Money Request Declined', + message: `Your request for $${amount.toFixed(2)} has been declined. Reason: ${reason}`, + scope: NotificationScope.MONEY_REQUEST_DECLINED, + preferences: this.getUserPreferences(childUser), + data: { + moneyRequestId: moneyRequest.id, + amount: amount.toString(), + rejectionReason: reason, + timestamp: moneyRequest.updatedAt.toISOString(), + type: 'MONEY_REQUEST_DECLINED', + action: 'VIEW_MONEY_REQUEST', + }, + }); + + this.logger.log(`✅ Notified child ${childUser.id} about declined money request ${moneyRequest.id}`); + } catch (error: any) { + this.logger.error( + `Failed to notify child of rejection: ${error?.message || 'Unknown error'}`, + error?.stack + ); + } + } + + /** + * Extract user preferences from User entity + * Converts User properties to NotificationPreferences interface + */ + private getUserPreferences(user: User): NotificationPreferences { + return { + isPushEnabled: user.isPushEnabled, + isEmailEnabled: user.isEmailEnabled, + isSmsEnabled: user.isSmsEnabled, + }; + } +} diff --git a/src/common/modules/notification/listeners/transaction-notification.listener.ts b/src/common/modules/notification/listeners/transaction-notification.listener.ts index f6ef334..7140784 100644 --- a/src/common/modules/notification/listeners/transaction-notification.listener.ts +++ b/src/common/modules/notification/listeners/transaction-notification.listener.ts @@ -1,5 +1,6 @@ import { 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 { NOTIFICATION_EVENTS } from '../constants/event-names.constant'; @@ -8,6 +9,7 @@ import { NotificationScope } from '../enums/notification-scope.enum'; import { Transaction } from '~/card/entities/transaction.entity'; import { Card } from '~/card/entities/card.entity'; import { User } from '~/user/entities'; +import { UserLocale } from '~/core/enums/user-locale.enum'; /** * TransactionNotificationListener @@ -29,6 +31,7 @@ export class TransactionNotificationListener { constructor( private readonly notificationFactory: NotificationFactory, private readonly userService: UserService, + private readonly i18n: I18nService, ) {} /** @@ -87,18 +90,28 @@ export class TransactionNotificationListener { ? NotificationScope.CHILD_TOP_UP : NotificationScope.CHILD_SPENDING; - const title = isTopUp ? 'Card Topped Up' : 'Purchase Successful'; - + const locale = this.getUserLocale(user); const amount = transaction.transactionAmount; const merchant = transaction.merchantName || 'merchant'; const balance = card.account?.balance || 0; + const currency = card.account?.currency || transaction.transactionCurrency || 'SAR'; + + const title = isTopUp + ? this.i18n.t('app.NOTIFICATION.CHILD_TOP_UP_TITLE', { lang: locale }) + : this.i18n.t('app.NOTIFICATION.CHILD_SPENDING_TITLE', { lang: locale }); const message = isTopUp - ? `Your card has been topped up with $${amount.toFixed(2)}` - : `You spent $${amount.toFixed(2)} at ${merchant}. Balance: $${balance.toFixed(2)}`; + ? this.i18n.t('app.NOTIFICATION.CHILD_TOP_UP_MESSAGE', { + lang: locale, + args: { amount: amount.toString(), currency, balance: balance.toString() }, + }) + : this.i18n.t('app.NOTIFICATION.CHILD_SPENDING_MESSAGE', { + lang: locale, + args: { amount: amount.toString(), currency, merchant, balance: balance.toString() }, + }); this.logger.debug( - `Notifying transaction owner (user ${user.id}) - Amount: $${amount}, Merchant: ${merchant}` + `Notifying transaction owner (user ${user.id}) - Amount: ${amount} ${currency}, Merchant: ${merchant}` ); await this.notificationFactory.send({ @@ -110,6 +123,7 @@ export class TransactionNotificationListener { data: { transactionId: transaction.id, amount: amount.toString(), + currency: currency, merchant: merchant, merchantCategory: transaction.merchantCategoryCode || 'OTHER', balance: balance.toString(), @@ -145,18 +159,28 @@ export class TransactionNotificationListener { } const childUser = customer.user; - const childName = childUser?.firstName || 'Your child'; + const locale = this.getUserLocale(parentUser); + const defaultChildName = this.i18n.t('app.NOTIFICATION.YOUR_CHILD', { lang: locale }); + const childName = childUser?.firstName || defaultChildName; const amount = transaction.transactionAmount; const merchant = transaction.merchantName || 'a merchant'; + const balance = card.account?.balance || 0; + const currency = card.account?.currency || transaction.transactionCurrency || 'SAR'; this.logger.debug( - `Notifying parent (user ${parentUser.id}): ${childName} spent $${amount} at ${merchant}` + `Notifying parent (user ${parentUser.id}): ${childName} spent ${amount} ${currency} at ${merchant}` ); + const title = this.i18n.t('app.NOTIFICATION.PARENT_SPENDING_TITLE', { lang: locale }); + const message = this.i18n.t('app.NOTIFICATION.PARENT_SPENDING_MESSAGE', { + lang: locale, + args: { childName, amount: amount.toString(), currency, merchant, balance: balance.toString() }, + }); + await this.notificationFactory.send({ userId: parentUser.id, - title: 'Child Spending Alert', - message: `${childName} spent $${amount.toFixed(2)} at ${merchant}`, + title, + message, scope: NotificationScope.PARENT_SPENDING_ALERT, preferences: this.getUserPreferences(parentUser), data: { @@ -164,8 +188,10 @@ export class TransactionNotificationListener { childId: childUser.id, childName: childName, amount: amount.toString(), + currency: currency, merchant: merchant, merchantCategory: transaction.merchantCategoryCode || 'OTHER', + balance: balance.toString(), timestamp: transaction.transactionDate.toISOString(), type: 'CHILD_SPENDING', action: 'OPEN_TRANSACTION', @@ -198,18 +224,27 @@ export class TransactionNotificationListener { } const childUser = customer.user; - const childName = childUser?.firstName || 'Your child'; + const locale = this.getUserLocale(parentUser); + 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 currency = card.account?.currency || transaction.transactionCurrency || 'SAR'; this.logger.debug( - `Notifying parent (user ${parentUser.id}): Topped up ${childName}'s card with $${amount}` + `Notifying parent (user ${parentUser.id}): Transferred ${amount} ${currency} to ${childName}` ); + const title = this.i18n.t('app.NOTIFICATION.PARENT_TOP_UP_TITLE', { lang: locale }); + const message = this.i18n.t('app.NOTIFICATION.PARENT_TOP_UP_MESSAGE', { + lang: locale, + args: { amount: amount.toString(), currency, childName, balance: balance.toString() }, + }); + await this.notificationFactory.send({ userId: parentUser.id, - title: 'Top-Up Confirmation', - message: `You topped up ${childName}'s card with $${amount.toFixed(2)}. New balance: $${balance.toFixed(2)}`, + title, + message, scope: NotificationScope.PARENT_TOP_UP_CONFIRMATION, preferences: this.getUserPreferences(parentUser), data: { @@ -217,6 +252,7 @@ export class TransactionNotificationListener { childId: childUser.id, childName: childName, amount: amount.toString(), + currency: currency, balance: balance.toString(), timestamp: transaction.transactionDate.toISOString(), type: 'TOP_UP', @@ -244,6 +280,17 @@ export class TransactionNotificationListener { isSmsEnabled: user.isSmsEnabled, }; } + + /** + * Get user locale for i18n translations + * Defaults to English if not specified + * TODO: Add locale field to User entity in the future + */ + private getUserLocale(user: User): UserLocale { + // For now, default to English + // In the future, this can read from user.locale or user.preferences.locale + return UserLocale.ENGLISH; + } } diff --git a/src/common/modules/notification/notification.module.ts b/src/common/modules/notification/notification.module.ts index f73da8d..733a1d2 100644 --- a/src/common/modules/notification/notification.module.ts +++ b/src/common/modules/notification/notification.module.ts @@ -8,7 +8,11 @@ import { buildMailerOptions, buildTwilioOptions } from '~/core/module-options'; import { UserModule } from '~/user/user.module'; import { NotificationsController } from './controllers'; import { Notification } from './entities'; -import { NotificationCreatedListener, TransactionNotificationListener } from './listeners'; +import { + MoneyRequestNotificationListener, + NotificationCreatedListener, + TransactionNotificationListener, +} from './listeners'; import { NotificationsRepository } from './repositories'; import { FirebaseService, NotificationFactory, NotificationsService, TwilioService } from './services'; import { MessagingSystemFactory, RedisPubSubMessagingService } from './services/messaging'; @@ -35,6 +39,7 @@ import { MessagingSystemFactory, RedisPubSubMessagingService } from './services/ TwilioService, NotificationCreatedListener, TransactionNotificationListener, + MoneyRequestNotificationListener, RedisPubSubMessagingService, MessagingSystemFactory, ], diff --git a/src/i18n/ar/app.json b/src/i18n/ar/app.json index d5e29b9..920237a 100644 --- a/src/i18n/ar/app.json +++ b/src/i18n/ar/app.json @@ -110,5 +110,16 @@ "INSUFFICIENT_BALANCE": "البطاقة لا تحتوي على رصيد كافٍ لإكمال هذا التحويل.", "DOES_NOT_BELONG_TO_GUARDIAN": "البطاقة لا تنتمي إلى ولي الأمر.", "NOT_FOUND": "لم يتم العثور على البطاقة." + }, + "NOTIFICATION": { + "CHILD_TOP_UP_TITLE": "تم شحن البطاقة", + "CHILD_TOP_UP_MESSAGE": "لقد استلمت {{amount}} {{currency}}. الرصيد الإجمالي: {{balance}} {{currency}}", + "CHILD_SPENDING_TITLE": "تمت العملية بنجاح", + "CHILD_SPENDING_MESSAGE": "لقد أنفقت {{amount}} {{currency}} في {{merchant}}. الرصيد: {{balance}} {{currency}}", + "PARENT_TOP_UP_TITLE": "تأكيد الشحن", + "PARENT_TOP_UP_MESSAGE": "لقد قمت بتحويل {{amount}} {{currency}} إلى {{childName}}. الرصيد: {{balance}} {{currency}}", + "PARENT_SPENDING_TITLE": "تنبيه إنفاق الطفل", + "PARENT_SPENDING_MESSAGE": "أنفق {{childName}} {{amount}} {{currency}} في {{merchant}}. الرصيد: {{balance}} {{currency}}", + "YOUR_CHILD": "طفلك" } } diff --git a/src/i18n/en/app.json b/src/i18n/en/app.json index 593d637..cbb1409 100644 --- a/src/i18n/en/app.json +++ b/src/i18n/en/app.json @@ -109,5 +109,16 @@ "INSUFFICIENT_BALANCE": "The card does not have sufficient balance to complete this transfer.", "DOES_NOT_BELONG_TO_GUARDIAN": "The card does not belong to the guardian.", "NOT_FOUND": "The card was not found." + }, + "NOTIFICATION": { + "CHILD_TOP_UP_TITLE": "Card Topped Up", + "CHILD_TOP_UP_MESSAGE": "You received {{amount}} {{currency}}. Total balance: {{balance}} {{currency}}", + "CHILD_SPENDING_TITLE": "Purchase Successful", + "CHILD_SPENDING_MESSAGE": "You spent {{amount}} {{currency}} at {{merchant}}. Balance: {{balance}} {{currency}}", + "PARENT_TOP_UP_TITLE": "Top-Up Confirmation", + "PARENT_TOP_UP_MESSAGE": "You transferred {{amount}} {{currency}} to {{childName}}. Balance: {{balance}} {{currency}}", + "PARENT_SPENDING_TITLE": "Child Spending Alert", + "PARENT_SPENDING_MESSAGE": "{{childName}} spent {{amount}} {{currency}} at {{merchant}}. Balance: {{balance}} {{currency}}", + "YOUR_CHILD": "Your child" } } diff --git a/src/money-request/repositories/money-requests.repository.ts b/src/money-request/repositories/money-requests.repository.ts index af206c3..3e3bb6f 100644 --- a/src/money-request/repositories/money-requests.repository.ts +++ b/src/money-request/repositories/money-requests.repository.ts @@ -56,7 +56,33 @@ export class MoneyRequestsRepository { } return this.moneyRequestRepository.findOne({ where: whereCondition, - relations: ['junior', 'junior.customer', 'junior.customer.user', 'junior.customer.user.profilePicture'], + relations: [ + 'junior', + 'junior.customer', + 'junior.customer.user', + 'junior.customer.user.profilePicture', + ], + }); + } + + findByIdWithAllRelations(id: string, userId?: string, role?: Roles): Promise { + const whereCondition: any = { id }; + if (role === Roles.JUNIOR) { + whereCondition.juniorId = userId; + } else { + whereCondition.guardianId = userId; + } + return this.moneyRequestRepository.findOne({ + where: whereCondition, + relations: [ + 'junior', + 'junior.customer', + 'junior.customer.user', + 'junior.customer.user.profilePicture', + 'guardian', + 'guardian.customer', + 'guardian.customer.user', + ], }); } diff --git a/src/money-request/services/money-requests.service.ts b/src/money-request/services/money-requests.service.ts index 6895206..a057090 100644 --- a/src/money-request/services/money-requests.service.ts +++ b/src/money-request/services/money-requests.service.ts @@ -1,6 +1,13 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; import { Transactional } from 'typeorm-transactional'; import { Roles } from '~/auth/enums'; +import { NOTIFICATION_EVENTS } from '~/common/modules/notification/constants/event-names.constant'; +import { + IMoneyRequestApprovedEvent, + IMoneyRequestCreatedEvent, + IMoneyRequestDeclinedEvent, +} from '~/common/modules/notification/interfaces/notification-events.interface'; import { OciService } from '~/document/services'; import { Junior } from '~/junior/entities/junior.entity'; import { JuniorService } from '~/junior/services'; @@ -16,10 +23,19 @@ export class MoneyRequestsService { private readonly moneyRequestsRepository: MoneyRequestsRepository, private readonly juniorService: JuniorService, private readonly ociService: OciService, + private readonly eventEmitter: EventEmitter2, ) {} async createMoneyRequest(juniorId: string, body: CreateMoneyRequestDto) { const junior = await this.juniorService.findJuniorById(juniorId); const moneyRequest = await this.moneyRequestsRepository.createMoneyRequest(junior.id, junior.guardianId, body); + const moneyRequestWithRelations = await this.moneyRequestsRepository.findByIdWithAllRelations(moneyRequest.id); + + const event: IMoneyRequestCreatedEvent = { + moneyRequest: moneyRequestWithRelations!, + timestamp: new Date(), + }; + this.eventEmitter.emit(NOTIFICATION_EVENTS.MONEY_REQUEST_CREATED, event); + return this.findById(moneyRequest.id); } @@ -63,6 +79,13 @@ export class MoneyRequestsService { moneyRequest.guardianId, ), ]); + + const updatedMoneyRequest = await this.moneyRequestsRepository.findByIdWithAllRelations(id, guardianId, Roles.GUARDIAN); + const event: IMoneyRequestApprovedEvent = { + moneyRequest: updatedMoneyRequest!, + timestamp: new Date(), + }; + this.eventEmitter.emit(NOTIFICATION_EVENTS.MONEY_REQUEST_APPROVED, event); } async rejectMoneyRequest( @@ -85,6 +108,14 @@ export class MoneyRequestsService { } await this.moneyRequestsRepository.rejectMoneyRequest(id, rejectionReasondto?.rejectionReason); + + const updatedMoneyRequest = await this.moneyRequestsRepository.findByIdWithAllRelations(id, guardianId, Roles.GUARDIAN); + const event: IMoneyRequestDeclinedEvent = { + moneyRequest: updatedMoneyRequest!, + rejectionReason: rejectionReasondto?.rejectionReason, + timestamp: new Date(), + }; + this.eventEmitter.emit(NOTIFICATION_EVENTS.MONEY_REQUEST_DECLINED, event); } private async prepareJuniorImages(juniors: Junior[]) { diff --git a/src/user/repositories/device.repository.ts b/src/user/repositories/device.repository.ts index 43c093e..62b6b59 100644 --- a/src/user/repositories/device.repository.ts +++ b/src/user/repositories/device.repository.ts @@ -11,6 +11,10 @@ export class DeviceRepository { return this.deviceRepository.findOne({ where: { deviceId, userId } }); } + findByDeviceId(deviceId: string) { + return this.deviceRepository.findOne({ where: { deviceId } }); + } + createDevice(data: Partial) { return this.deviceRepository.save(data); } diff --git a/src/user/services/device.service.ts b/src/user/services/device.service.ts index 89f41c6..52c4743 100644 --- a/src/user/services/device.service.ts +++ b/src/user/services/device.service.ts @@ -10,6 +10,11 @@ export class DeviceService { return this.deviceRepository.findUserDeviceById(deviceId, userId); } + findByDeviceId(deviceId: string) { + this.logger.log(`Finding device with id ${deviceId} (any user)`); + return this.deviceRepository.findByDeviceId(deviceId); + } + createDevice(data: Partial) { this.logger.log(`Creating device with data ${JSON.stringify(data)}`); return this.deviceRepository.createDevice(data);