Changes Updated #70
@ -1,12 +1,14 @@
|
|||||||
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 {
|
||||||
|
senderId?: string;
|
||||||
|
recipientId?: string;
|
||||||
notificationSent?: boolean;
|
notificationSent?: boolean;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
clientId?: string;
|
clientId?: string;
|
||||||
@ -25,10 +27,12 @@ interface NotificationData {
|
|||||||
[key: string]: any;
|
[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;
|
||||||
@ -39,15 +43,25 @@ export const processNotificationOnCreate = onDocumentCreated({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const notification = notificationSnapshot.data() as NotificationData;
|
const notification = notificationSnapshot.data() as NotificationData;
|
||||||
|
|
||||||
if (notification.notificationSent === true) {
|
if (notification.notificationSent === true) {
|
||||||
logger.info(`Notification ${notificationId} already sent, skipping.`);
|
logger.info(`Notification ${notificationId} already sent, skipping.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { fcmToken } = await getUserAndFCMToken(notification);
|
logger.info(
|
||||||
|
`Processing notification ${notificationId} of type: ${notification.type}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const { userId, fcmToken } = await getUserAndFCMToken(notification);
|
||||||
if (!fcmToken) {
|
if (!fcmToken) {
|
||||||
logger.error(`FCM token not found for notification ${notificationId}`);
|
logger.error(
|
||||||
await updateNotificationWithError(notificationId, 'FCM token not found for user');
|
`FCM token not found for notification ${notificationId}, user: ${userId}`
|
||||||
|
);
|
||||||
|
await updateNotificationWithError(
|
||||||
|
notificationId,
|
||||||
|
"FCM token not found for user"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,39 +69,52 @@ export const processNotificationOnCreate = onDocumentCreated({
|
|||||||
try {
|
try {
|
||||||
const fcmResponse = await app.messaging().send({
|
const fcmResponse = await app.messaging().send({
|
||||||
...message,
|
...message,
|
||||||
token: fcmToken
|
token: fcmToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`FCM notification sent successfully: ${fcmResponse}`);
|
logger.info(`FCM notification sent successfully: ${fcmResponse}`);
|
||||||
await markNotificationAsSent(notificationId);
|
await markNotificationAsSent(notificationId);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error sending notification ${notificationId}:`, error);
|
logger.error(`Error sending notification ${notificationId}:`, error);
|
||||||
await updateNotificationWithError(notificationId, error instanceof Error ? error.message : String(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(
|
||||||
|
notification: NotificationData
|
||||||
|
): Promise<{ userId: string | null; fcmToken: string | null }> {
|
||||||
let userId: string | null = null;
|
let userId: string | null = null;
|
||||||
let fcmToken: string | null = null;
|
let fcmToken: string | null = null;
|
||||||
|
|
||||||
if (notification.userId) {
|
if (notification.recipientId) {
|
||||||
|
userId = notification.recipientId;
|
||||||
|
fcmToken = await getFCMTokenFromUserDoc(userId);
|
||||||
|
logger.info(`Using recipientId: ${userId}`);
|
||||||
|
} else if (notification.userId) {
|
||||||
userId = notification.userId;
|
userId = notification.userId;
|
||||||
fcmToken = await getFCMTokenFromUserDoc(userId);
|
fcmToken = await getFCMTokenFromUserDoc(userId);
|
||||||
|
logger.info(`Using userId: ${userId}`);
|
||||||
} else if (notification.clientId) {
|
} else if (notification.clientId) {
|
||||||
userId = notification.clientId;
|
userId = notification.clientId;
|
||||||
fcmToken = await getFCMTokenFromUserDoc(userId);
|
fcmToken = await getFCMTokenFromUserDoc(userId);
|
||||||
|
logger.info(`Using clientId: ${userId}`);
|
||||||
} else if (notification.invitorId) {
|
} else if (notification.invitorId) {
|
||||||
userId = notification.invitorId;
|
userId = notification.invitorId;
|
||||||
fcmToken = await getFCMTokenFromUserDoc(userId);
|
fcmToken = await getFCMTokenFromUserDoc(userId);
|
||||||
|
logger.info(`Using invitorId: ${userId}`);
|
||||||
} else if (notification.phoneNumber) {
|
} else if (notification.phoneNumber) {
|
||||||
|
logger.info(`Looking up user by phone number: ${notification.phoneNumber}`);
|
||||||
const userQuery = await app
|
const userQuery = await app
|
||||||
.firestore()
|
.firestore()
|
||||||
.collection('users')
|
.collection("users")
|
||||||
.where('phoneNumber', '==', notification.phoneNumber)
|
.where("phoneNumber", "==", notification.phoneNumber)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
@ -95,63 +122,176 @@ async function getUserAndFCMToken(notification: NotificationData): Promise<{ use
|
|||||||
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId && !fcmToken) {
|
||||||
|
logger.warn(`User ${userId} found but no FCM token available`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { userId, fcmToken };
|
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
|
||||||
|
): admin.messaging.Message {
|
||||||
|
let title = "New Notification";
|
||||||
|
let body = notification.message || "You have a new notification";
|
||||||
let data: Record<string, string> = {
|
let data: Record<string, string> = {
|
||||||
type: notification.type || 'general',
|
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 ||
|
||||||
|
`${
|
||||||
|
notification.trainerName
|
||||||
|
} has ${notification.status?.toLowerCase()} your request`;
|
||||||
|
data.trainerName = notification.trainerName || "";
|
||||||
|
data.status = notification.status || "";
|
||||||
break;
|
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}`;
|
||||||
|
data.clientName = notification.name || "";
|
||||||
|
data.membershipId = notification.membershipId || "";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'client_invitations':
|
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_update_owner":
|
||||||
|
title = "Trainer Schedule Update";
|
||||||
|
body = notification.message || "A trainer has updated their schedule";
|
||||||
|
data.membershipId = notification.membershipId || "";
|
||||||
|
break;
|
||||||
|
|
||||||
|
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) {
|
if (notification.userId || notification.invitorId) {
|
||||||
const isAccept = notification.status === 'ACCEPTED';
|
const isAccept = notification.status === "ACCEPTED";
|
||||||
title = isAccept ? 'Invitation Accepted' : 'Invitation Rejected';
|
title = isAccept ? "Invitation Accepted" : "Invitation Rejected";
|
||||||
body = notification.message || (isAccept ?
|
body =
|
||||||
`The invitation for ${notification.subscriptionName} you shared with ${notification.name} has been accepted` :
|
notification.message ||
|
||||||
`The invitation for ${notification.subscriptionName} you shared with ${notification.name} has been rejected`);
|
(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) {
|
} else if (notification.phoneNumber) {
|
||||||
const invitationStatus = getInvitationStatus(notification.status);
|
const invitationStatus = getInvitationStatus(notification.status);
|
||||||
title = getInvitationTitle(invitationStatus);
|
title = getInvitationTitle(invitationStatus);
|
||||||
body = notification.message || getInvitationBody(invitationStatus, notification.name);
|
body =
|
||||||
|
notification.message ||
|
||||||
|
getInvitationBody(invitationStatus, notification.name);
|
||||||
data.status = invitationStatus;
|
data.status = invitationStatus;
|
||||||
}
|
}
|
||||||
data.gymName = notification.gymName || '';
|
data.gymName = notification.gymName || "";
|
||||||
data.clientEmail = notification.clientEmail || '';
|
data.clientEmail = notification.clientEmail || "";
|
||||||
data.clientName = notification.name || '';
|
data.clientName = notification.name || "";
|
||||||
data.invitationId = notification.invitationId || '';
|
data.invitationId = notification.invitationId || "";
|
||||||
data.subscriptionName = notification.subscriptionName || '';
|
data.subscriptionName = notification.subscriptionName || "";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.info(`Using default handling for notification type: ${notification.type}`);
|
logger.info(
|
||||||
|
`Using default handling for notification type: ${notification.type}`
|
||||||
|
);
|
||||||
|
title = notification.type
|
||||||
|
? `${notification.type.replace("_", " ").toUpperCase()}`
|
||||||
|
: "Notification";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,64 +299,96 @@ function prepareNotificationMessage(notification: NotificationData, fcmToken: st
|
|||||||
notification: { title, body },
|
notification: { title, body },
|
||||||
data,
|
data,
|
||||||
android: {
|
android: {
|
||||||
priority: 'high',
|
priority: "high",
|
||||||
notification: {
|
notification: {
|
||||||
channelId: 'notifications_channel',
|
channelId: "notifications_channel",
|
||||||
priority: 'high',
|
priority: "high",
|
||||||
defaultSound: true,
|
defaultSound: true,
|
||||||
defaultVibrateTimings: true,
|
defaultVibrateTimings: true,
|
||||||
icon: '@mipmap/ic_launcher',
|
icon: "@mipmap/ic_launcher",
|
||||||
clickAction: 'FLUTTER_NOTIFICATION_CLICK',
|
clickAction: "FLUTTER_NOTIFICATION_CLICK",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
apns: {
|
apns: {
|
||||||
payload: {
|
payload: {
|
||||||
aps: {
|
aps: {
|
||||||
sound: 'default',
|
sound: "default",
|
||||||
badge: 1,
|
badge: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
token: fcmToken,
|
token: fcmToken,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
logger.info(`Prepared notification: ${title} - ${body}`);
|
||||||
return notificationMessage;
|
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}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user