import { Inject, Injectable } from '@nestjs/common'; import { DataSource, EntityManager } from 'typeorm'; import { MediaDataService } from 'src/module/meta/service/media-data.service'; import * as admin from 'firebase-admin'; import axios from 'axios'; import { ConfigService } from '@nestjs/config'; import { ReflectionHelper } from 'src/utils/service/reflection-helper.service'; import { NotificationRepository } from '../repository/notification.repository'; @Injectable() export class NotificationsService { constructor( private readonly entityManager: EntityManager, private readonly mediaDataService: MediaDataService, private readonly configService: ConfigService, private readonly reflectionHelper: ReflectionHelper, @Inject('FIREBASE_ADMIN') private readonly firebaseAdmin: typeof admin, private readonly notificationRepository: NotificationRepository, ) {} private tokens: Map = new Map(); // store in memory for now async saveToken(userId: string | undefined, token: string) { if (userId) { this.tokens.set(userId, token); } return { success: true, token }; } async sendToDevice( token: string, title: string, body: string, data?: Record, ) { // Utility to sanitize FCM data payload const sanitizeFCMData = ( payload: Record, ): Record => Object.fromEntries( Object.entries(payload).map(([k, v]) => { if (v === null || v === undefined) return [k, '']; // fallback for null/undefined if (typeof v === 'object') return [k, JSON.stringify(v)]; // preserve structure return [k, String(v)]; // numbers, booleans, strings }), ); const message: admin.messaging.Message = { token, notification: { title, body }, // system notification data: data ? sanitizeFCMData(data) : undefined, }; try { return await this.firebaseAdmin.messaging().send(message); } catch (error) { console.error('Error sending FCM message:', error); throw error; } } // Helper: send to a registered user by userId async sendToUser(userId: string, title: string, body: string) { const token = this.tokens.get(userId); if (!token) return { error: 'No token found for user' }; return this.sendToDevice(token, title, body); } async getAllNotifications( loggedInUser: any, filterQuery?: { is_read?: string }, ) { const { id: userId, level_id, level_type } = loggedInUser; // Fetch notifications from repository const notifications: any = await this.notificationRepository.getAllNotifications( userId, level_id, level_type, filterQuery?.is_read, ); // // Avoid duplicate API calls for each user_id // const mediaCache = new Map< number, { name: string; profile_image: string } >(); const baseUrl = this.configService.get('REDIRECT_BE_URL'); for (const notification of notifications) { const uid = notification?.user_id; if (uid && !mediaCache.has(uid)) { try { const queryParams = new URLSearchParams({ loggedInUser: JSON.stringify(loggedInUser), }).toString(); const response = await axios.get( `${baseUrl}/users/profile-image-url/${uid}?entity_type=USR&${queryParams}`, { headers: { 'Content-Type': 'application/json' } }, ); mediaCache.set(uid, { name: response.data.name, profile_image: response.data.profile_image, }); } catch (err) { console.error('⚠️ Internal Entity API call failed:', err.message); } } const cachedData = mediaCache.get(uid); notification.user_name = cachedData?.name; notification.user_profile = cachedData?.profile_image; } return notifications; } async markAllAsRead(loggedInUser: any) { const { id, level_id, level_type } = loggedInUser; const notificationRepo = this.reflectionHelper.getRepoService('NotificationData'); const result = await notificationRepo.update( { user_id: id, level_id: level_id, level_type: level_type, is_read: false, }, { is_read: true, }, ); return { success: true, affectedRows: result.affected || 0 }; } }