Merge pull request #76 from Zod-Alkhair/feature/notification-system-fcm-registration

feat: implement money request notification system
This commit is contained in:
Majdalkilany0
2026-01-12 16:15:19 +03:00
committed by GitHub
14 changed files with 500 additions and 25 deletions

View File

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

View File

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

View File

@ -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',
}
/**

View File

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

View File

@ -1,2 +1,3 @@
export * from './notification-created.listener';
export * from './transaction-notification.listener';
export * from './money-request-notification.listener';

View File

@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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,
};
}
}

View File

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

View File

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

View File

@ -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": "طفلك"
}
}

View File

@ -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"
}
}

View File

@ -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<MoneyRequest | null> {
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',
],
});
}

View File

@ -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[]) {

View File

@ -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<Device>) {
return this.deviceRepository.save(data);
}

View File

@ -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<Device>) {
this.logger.log(`Creating device with data ${JSON.stringify(data)}`);
return this.deviceRepository.createDevice(data);