import { onDocumentCreated } from "firebase-functions/v2/firestore"; import { getLogger } from "../shared/config"; import { getAdmin } from "../shared/config"; import * as admin from 'firebase-admin'; const app = getAdmin(); const logger = getLogger(); interface NotificationData { notificationSent?: boolean; userId?: string; clientId?: string; invitorId?: string; phoneNumber?: string; message?: string; type?: string; status?: string; gymName?: string; trainerName?: string; membershipId?: string; subscriptionName?: string; name?: string; clientEmail?: string; invitationId?: string; [key: string]: any; } export const processNotificationOnCreate = onDocumentCreated({ region: '#{SERVICES_RGN}#', document: 'notifications/{notificationId}' }, async (event) => { try { const notificationSnapshot = event.data; const notificationId = event.params.notificationId; if (!notificationSnapshot) { logger.error(`No data found for notification ${notificationId}`); return; } const notification = notificationSnapshot.data() as NotificationData; if (notification.notificationSent === true) { logger.info(`Notification ${notificationId} already sent, skipping.`); return; } const { fcmToken } = await getUserAndFCMToken(notification); if (!fcmToken) { logger.error(`FCM token not found for notification ${notificationId}`); await updateNotificationWithError(notificationId, 'FCM token not found for user'); return; } const message = prepareNotificationMessage(notification, fcmToken); try { const fcmResponse = await app.messaging().send({ ...message, token: fcmToken }); logger.info(`FCM notification sent successfully: ${fcmResponse}`); await markNotificationAsSent(notificationId); } catch (error) { logger.error(`Error sending notification ${notificationId}:`, error); await updateNotificationWithError(notificationId, error instanceof Error ? error.message : String(error)); } } catch (error) { logger.error('Error processing notification:', error); } }); async function getUserAndFCMToken(notification: NotificationData): Promise<{ userId: string | null; fcmToken: string | null }> { let userId: string | null = null; let fcmToken: string | null = null; if (notification.userId) { userId = notification.userId; fcmToken = await getFCMTokenFromUserDoc(userId); } else if (notification.clientId) { userId = notification.clientId; fcmToken = await getFCMTokenFromUserDoc(userId); } else if (notification.invitorId) { userId = notification.invitorId; fcmToken = await getFCMTokenFromUserDoc(userId); } else if (notification.phoneNumber) { const userQuery = await app .firestore() .collection('users') .where('phoneNumber', '==', notification.phoneNumber) .limit(1) .get(); if (!userQuery.empty) { const userDoc = userQuery.docs[0]; userId = userDoc.id; fcmToken = userDoc.data()?.fcmToken; } } return { userId, fcmToken }; } async function getFCMTokenFromUserDoc(userId: string): Promise { const userDoc = await app.firestore().collection('users').doc(userId).get(); return userDoc.exists ? userDoc.data()?.fcmToken : null; } function prepareNotificationMessage(notification: NotificationData, fcmToken: string): admin.messaging.Message { let title = 'New Notification'; let body = notification.message || 'You have a new notification'; let data: Record = { type: notification.type || 'general', }; switch (notification.type) { case 'day_pass_entry': const isAccepted = notification.status === 'ACCEPTED'; title = isAccepted ? 'Day Pass Approved' : 'Day Pass Denied'; body = notification.message || (isAccepted ? 'Your day pass has been approved' : 'Your day pass has been denied'); data.gymName = notification.gymName || ''; break; case 'trainer_assigned_to_client': title = 'Trainer Assigned'; body = notification.message || `${notification.trainerName} has been assigned as your trainer`; data.trainerName = notification.trainerName || ''; data.membershipId = notification.membershipId || ''; break; case 'client_invitations': if (notification.userId || notification.invitorId) { const isAccept = notification.status === 'ACCEPTED'; title = isAccept ? 'Invitation Accepted' : 'Invitation Rejected'; body = notification.message || (isAccept ? `The invitation for ${notification.subscriptionName} you shared with ${notification.name} has been accepted` : `The invitation for ${notification.subscriptionName} you shared with ${notification.name} has been rejected`); } else if (notification.phoneNumber) { const invitationStatus = getInvitationStatus(notification.status); title = getInvitationTitle(invitationStatus); body = notification.message || getInvitationBody(invitationStatus, notification.name); data.status = invitationStatus; } data.gymName = notification.gymName || ''; data.clientEmail = notification.clientEmail || ''; data.clientName = notification.name || ''; data.invitationId = notification.invitationId || ''; data.subscriptionName = notification.subscriptionName || ''; break; default: logger.info(`Using default handling for notification type: ${notification.type}`); break; } const notificationMessage: admin.messaging.Message = { notification: { title, body }, data, android: { priority: 'high', notification: { channelId: 'notifications_channel', priority: 'high', defaultSound: true, defaultVibrateTimings: true, icon: '@mipmap/ic_launcher', clickAction: 'FLUTTER_NOTIFICATION_CLICK', }, }, apns: { payload: { aps: { sound: 'default', badge: 1, }, }, }, token: fcmToken, }; return notificationMessage; } function getInvitationStatus(status?: string): string { if (status === 'ACCEPTED') return 'accepted'; if (status === 'REJECTED') return 'rejected'; if (status === 'PENDING') return 'pending'; return 'unknown'; } function getInvitationTitle(status: string): string { switch (status) { case 'accepted': return 'Invitation Accepted'; case 'rejected': return 'Invitation Rejected'; case 'pending': return 'New Invitation'; default: return 'Invitation Update'; } } function getInvitationBody(status: string, name?: string): string { switch (status) { case 'accepted': return `You have accepted the invitation from ${name}`; case 'rejected': return `You have rejected the invitation from ${name}`; case 'pending': return `You have a new invitation pending from ${name}`; default: return 'There is an update to your invitation'; } } async function markNotificationAsSent(notificationId: string): Promise { await app.firestore().collection('notifications').doc(notificationId).update({ notificationSent: true, sentAt: app.firestore.FieldValue.serverTimestamp() }); } async function updateNotificationWithError(notificationId: string, error: string): Promise { await app.firestore().collection('notifications').doc(notificationId).update({ notificationError: error, updatedAt: app.firestore.FieldValue.serverTimestamp() }); }