Changes Updated #70
| @ -1,222 +1,394 @@ | ||||
| import { onDocumentCreated } from "firebase-functions/v2/firestore"; | ||||
| import { getLogger } from "../shared/config"; | ||||
| import { getAdmin } from "../shared/config"; | ||||
| import * as admin from 'firebase-admin'; | ||||
| 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; | ||||
|   senderId?: string; | ||||
|   recipientId?: string; | ||||
|   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) => { | ||||
| export const processNotificationOnCreate = onDocumentCreated( | ||||
|   { | ||||
|     region: "#{SERVICES_RGN}#", | ||||
|     document: "notifications/{notificationId}", | ||||
|   }, | ||||
|   async (event) => { | ||||
|     try { | ||||
|         const notificationSnapshot = event.data; | ||||
|         const notificationId = event.params.notificationId; | ||||
|       const notificationSnapshot = event.data; | ||||
|       const notificationId = event.params.notificationId; | ||||
| 
 | ||||
|         if (!notificationSnapshot) { | ||||
|             logger.error(`No data found for notification ${notificationId}`); | ||||
|             return; | ||||
|         } | ||||
|       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 notification = notificationSnapshot.data() as NotificationData; | ||||
| 
 | ||||
|         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; | ||||
|         } | ||||
|       if (notification.notificationSent === true) { | ||||
|         logger.info(`Notification ${notificationId} already sent, skipping.`); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|         const message = prepareNotificationMessage(notification, fcmToken); | ||||
|         try { | ||||
|             const fcmResponse = await app.messaging().send({ | ||||
|                 ...message, | ||||
|                 token: fcmToken | ||||
|             }); | ||||
|       logger.info( | ||||
|         `Processing notification ${notificationId} of type: ${notification.type}` | ||||
|       ); | ||||
| 
 | ||||
|             logger.info(`FCM notification sent successfully: ${fcmResponse}`); | ||||
|             await markNotificationAsSent(notificationId); | ||||
|       const { userId, fcmToken } = await getUserAndFCMToken(notification); | ||||
|       if (!fcmToken) { | ||||
|         logger.error( | ||||
|           `FCM token not found for notification ${notificationId}, user: ${userId}` | ||||
|         ); | ||||
|         await updateNotificationWithError( | ||||
|           notificationId, | ||||
|           "FCM token not found for user" | ||||
|         ); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|         } catch (error) { | ||||
|             logger.error(`Error sending notification ${notificationId}:`, error); | ||||
|             await updateNotificationWithError(notificationId, error instanceof Error ? error.message : String(error)); | ||||
|         } | ||||
|       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); | ||||
|       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; | ||||
| 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 (notification.recipientId) { | ||||
|     userId = notification.recipientId; | ||||
|     fcmToken = await getFCMTokenFromUserDoc(userId); | ||||
|     logger.info(`Using recipientId: ${userId}`); | ||||
|   } else if (notification.userId) { | ||||
|     userId = notification.userId; | ||||
|     fcmToken = await getFCMTokenFromUserDoc(userId); | ||||
|     logger.info(`Using userId: ${userId}`); | ||||
|   } else if (notification.clientId) { | ||||
|     userId = notification.clientId; | ||||
|     fcmToken = await getFCMTokenFromUserDoc(userId); | ||||
|     logger.info(`Using clientId: ${userId}`); | ||||
|   } else if (notification.invitorId) { | ||||
|     userId = notification.invitorId; | ||||
|     fcmToken = await getFCMTokenFromUserDoc(userId); | ||||
|     logger.info(`Using invitorId: ${userId}`); | ||||
|   } else if (notification.phoneNumber) { | ||||
|     logger.info(`Looking up user by phone number: ${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; | ||||
|         } | ||||
|     if (!userQuery.empty) { | ||||
|       const userDoc = userQuery.docs[0]; | ||||
|       userId = userDoc.id; | ||||
|       fcmToken = userDoc.data()?.fcmToken; | ||||
|       logger.info(`Found user by phone: ${userId}`); | ||||
|     } else { | ||||
|       logger.warn( | ||||
|         `No user found with phone number: ${notification.phoneNumber}` | ||||
|       ); | ||||
|     } | ||||
|   } else { | ||||
|     logger.error("No valid user identifier found in notification"); | ||||
|   } | ||||
| 
 | ||||
|     return { userId, fcmToken }; | ||||
|   if (userId && !fcmToken) { | ||||
|     logger.warn(`User ${userId} found but no FCM token available`); | ||||
|   } | ||||
| 
 | ||||
|   return { userId, fcmToken }; | ||||
| } | ||||
| 
 | ||||
| async function getFCMTokenFromUserDoc(userId: string): Promise<string | null> { | ||||
|     const userDoc = await app.firestore().collection('users').doc(userId).get(); | ||||
|     return userDoc.exists ? userDoc.data()?.fcmToken : null; | ||||
|   try { | ||||
|     const userDoc = await app.firestore().collection("users").doc(userId).get(); | ||||
|     if (userDoc.exists) { | ||||
|       const userData = userDoc.data(); | ||||
|       const fcmToken = userData?.fcmToken; | ||||
|       if (!fcmToken) { | ||||
|         logger.warn(`User ${userId} exists but has no FCM token`); | ||||
|       } | ||||
|       return fcmToken; | ||||
|     } else { | ||||
|       logger.warn(`User document not found: ${userId}`); | ||||
|       return null; | ||||
|     } | ||||
|   } catch (error) { | ||||
|     logger.error(`Error fetching user ${userId}:`, error); | ||||
|     return 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<string, string> = { | ||||
|         type: notification.type || 'general', | ||||
|     }; | ||||
| 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<string, string> = { | ||||
|     type: notification.type || "general", | ||||
|     notificationId: "notification_" + Date.now(), | ||||
|   }; | ||||
| 
 | ||||
|     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; | ||||
|   switch (notification.type) { | ||||
|     case "trainer_response": | ||||
|       title = | ||||
|         notification.status === "ACCEPTED" | ||||
|           ? "Trainer Request Accepted" | ||||
|           : "Trainer Request Update"; | ||||
|       body = | ||||
|         notification.message || | ||||
|         `${ | ||||
|           notification.trainerName | ||||
|         } has ${notification.status?.toLowerCase()} your request`;
 | ||||
|       data.trainerName = notification.trainerName || ""; | ||||
|       data.status = notification.status || ""; | ||||
|       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 "trainer_assignment": | ||||
|       title = "New Client Assignment"; | ||||
|       body = | ||||
|         notification.message || | ||||
|         `You have been assigned to ${notification.name}`; | ||||
|       data.clientName = notification.name || ""; | ||||
|       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; | ||||
|     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; | ||||
| 
 | ||||
|         default: | ||||
|             logger.info(`Using default handling for notification type: ${notification.type}`); | ||||
|             break; | ||||
|     } | ||||
|     case "trainer_update_owner": | ||||
|       title = "Trainer Schedule Update"; | ||||
|       body = notification.message || "A trainer has updated their schedule"; | ||||
|       data.membershipId = notification.membershipId || ""; | ||||
|       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', | ||||
|             }, | ||||
|     case "trainer_update_client": | ||||
|       title = "Schedule Update"; | ||||
|       body = notification.message || "Your training schedule has been updated"; | ||||
|       data.membershipId = notification.membershipId || ""; | ||||
|       break; | ||||
| 
 | ||||
|     case "plan_renewal": | ||||
|       title = "Plan Renewal"; | ||||
|       body = | ||||
|         notification.message || | ||||
|         `Plan ${notification.subscriptionName} has been renewed`; | ||||
|       data.planName = notification.subscriptionName || ""; | ||||
|       data.membershipId = notification.membershipId || ""; | ||||
|       break; | ||||
| 
 | ||||
|     case "plan_assigned": | ||||
|       title = "New Plan Assigned"; | ||||
|       body = | ||||
|         notification.message || | ||||
|         `You have been assigned ${notification.subscriptionName} at ${notification.gymName}`; | ||||
|       data.planName = notification.subscriptionName || ""; | ||||
|       data.gymName = notification.gymName || ""; | ||||
|       data.membershipId = notification.membershipId || ""; | ||||
|       break; | ||||
| 
 | ||||
|     case "schedule_update": | ||||
|       title = "Schedule Update"; | ||||
|       body = notification.message || "Your training schedule has been updated"; | ||||
|       data.gymName = notification.gymName || ""; | ||||
|       break; | ||||
| 
 | ||||
|     case "attendance_dispute": | ||||
|       title = "Attendance Dispute"; | ||||
|       body = | ||||
|         notification.message || | ||||
|         `${notification.name} has disputed an attendance record`; | ||||
|       data.disputedBy = notification.name || ""; | ||||
|       data.membershipId = notification.membershipId || ""; | ||||
|       break; | ||||
| 
 | ||||
|     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 || ""; | ||||
|       data.status = notification.status || ""; | ||||
|       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}` | ||||
|       ); | ||||
|       title = notification.type | ||||
|         ? `${notification.type.replace("_", " ").toUpperCase()}` | ||||
|         : "Notification"; | ||||
|       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, | ||||
|         }, | ||||
|         apns: { | ||||
|             payload: { | ||||
|                 aps: { | ||||
|                     sound: 'default', | ||||
|                     badge: 1, | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|         token: fcmToken, | ||||
|     }; | ||||
|     return notificationMessage; | ||||
|       }, | ||||
|     }, | ||||
|     token: fcmToken, | ||||
|   }; | ||||
| 
 | ||||
|   logger.info(`Prepared notification: ${title} - ${body}`); | ||||
|   return notificationMessage; | ||||
| } | ||||
| 
 | ||||
| function getInvitationStatus(status?: string): string { | ||||
|     if (status === 'ACCEPTED') return 'accepted'; | ||||
|     if (status === 'REJECTED') return 'rejected'; | ||||
|     if (status === 'PENDING') return 'pending'; | ||||
|     return 'unknown'; | ||||
|   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'; | ||||
|     } | ||||
|   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'; | ||||
|     } | ||||
|   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<void> { | ||||
|     await app.firestore().collection('notifications').doc(notificationId).update({ | ||||
|   try { | ||||
|     await app | ||||
|       .firestore() | ||||
|       .collection("notifications") | ||||
|       .doc(notificationId) | ||||
|       .update({ | ||||
|         notificationSent: true, | ||||
|         sentAt: app.firestore.FieldValue.serverTimestamp() | ||||
|     }); | ||||
|         sentAt: admin.firestore.FieldValue.serverTimestamp(), | ||||
|       }); | ||||
|     logger.info(`Notification ${notificationId} marked as sent`); | ||||
|   } catch (error) { | ||||
|     logger.error(`Error marking notification as sent: ${error}`); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function updateNotificationWithError(notificationId: string, error: string): Promise<void> { | ||||
|     await app.firestore().collection('notifications').doc(notificationId).update({ | ||||
| async function updateNotificationWithError( | ||||
|   notificationId: string, | ||||
|   error: string | ||||
| ): Promise<void> { | ||||
|   try { | ||||
|     await app | ||||
|       .firestore() | ||||
|       .collection("notifications") | ||||
|       .doc(notificationId) | ||||
|       .update({ | ||||
|         notificationError: error, | ||||
|         updatedAt: app.firestore.FieldValue.serverTimestamp() | ||||
|     }); | ||||
|         notificationSent: false, | ||||
|         updatedAt: admin.firestore.FieldValue.serverTimestamp(), | ||||
|       }); | ||||
|     logger.info(`Notification ${notificationId} marked with error: ${error}`); | ||||
|   } catch (updateError) { | ||||
|     logger.error(`Error updating notification with error: ${updateError}`); | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user