notification-issue #72

Merged
dhanshas merged 7 commits from notification-issue into dev 2025-07-29 07:46:19 +00:00
Showing only changes of commit 9c5da57f97 - Show all commits

View File

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