fitlien-services/functions/src/notifications/processNotification.ts

223 lines
8.2 KiB
TypeScript

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<string | null> {
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<string, string> = {
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<void> {
await app.firestore().collection('notifications').doc(notificationId).update({
notificationSent: true,
sentAt: app.firestore.FieldValue.serverTimestamp()
});
}
async function updateNotificationWithError(notificationId: string, error: string): Promise<void> {
await app.firestore().collection('notifications').doc(notificationId).update({
notificationError: error,
updatedAt: app.firestore.FieldValue.serverTimestamp()
});
}