diff --git a/functions/src/index.ts b/functions/src/index.ts index 4812211..baaf43f 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,7 +3,6 @@ import { Request } from "firebase-functions/v2/https"; import * as admin from 'firebase-admin'; import * as express from "express"; import * as logger from "firebase-functions/logger"; -import { onDocumentCreated } from "firebase-functions/firestore"; import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; @@ -129,119 +128,6 @@ export const sendSMSMessage = onRequest({ }); }); -interface Invitation { - email: string; - phoneNumber: string; - gymName: string; - invitedByName: string; - status: string; - notificationSent: boolean; - subscriptionName?: string; -} - -export const sendInvitationNotification = onDocumentCreated({ - document: 'client_invitations/{invitationId}', - region: process.env.SERVICES_RGN -}, async (event: any) => { - const invitation = event.data?.data() as Invitation; - const invitationId = event.params.invitationId; - - if (!invitation) { - console.error('Invitation data is missing.'); - return null; - } - - if (invitation.status !== 'pending') { - console.log(`Invitation ${invitationId} is not pending, skipping notification.`); - return null; - } - - if (invitation.notificationSent) { - console.log(`Invitation notification ${invitationId} already sent, skipping.`); - return null; - } - - try { - if (!invitation.phoneNumber) { - console.error('Phone number not found in invitation.'); - return null; - } - - const userQuery = await admin - .firestore() - .collection('users') - .where('phoneNumber', '==', invitation.phoneNumber) - .limit(1) - .get(); - - if (userQuery.empty) { - console.log(`User not found for phone: ${invitation.phoneNumber}`); - return null; - } - - const userDoc = userQuery.docs[0]; - const user = userDoc.data(); - const fcmToken = user?.fcmToken; - - if (!fcmToken) { - console.log(`FCM token not found for user with phone: ${invitation.phoneNumber}`); - return null; - } - - const title = 'New Gym Invitation'; - const body = `${invitation.invitedByName} has invited you to join ${invitation.gymName}`; - - const data: Record = { - type: 'client_invitation', - invitationId: invitationId, - gymName: invitation.gymName || '', - senderName: invitation.invitedByName || '', - subscriptionName: invitation.subscriptionName || '' - }; - - const message: admin.messaging.Message = { - notification: { - title: title, - body: body, - }, - data: data, - android: { - priority: 'high', - notification: { - channelId: 'invitations_channel', - priority: 'high', - defaultSound: true, - defaultVibrateTimings: true, - icon: '@mipmap/ic_launcher', - clickAction: 'FLUTTER_NOTIFICATION_CLICK', - }, - }, - apns: { - payload: { - aps: { - sound: 'default', - badge: 1, - }, - }, - }, - token: fcmToken, - }; - - await admin.messaging().send(message); - console.log(`Invitation notification sent to user with phone: ${invitation.phoneNumber}`); - - await admin.firestore().collection('client_invitations').doc(invitationId).update({ - notificationSent: true, - notificationSentAt: admin.firestore.FieldValue.serverTimestamp() - }); - - return null; - } catch (error) { - console.error('Error sending invitation notification:', error); - return null; - } -}); - export const createCashfreeOrder = onRequest({ region: process.env.SERVICES_RGN }, async (request: Request, response: express.Response) => { @@ -489,8 +375,8 @@ export const sendFCMNotificationByType = onRequest({ const isAccept = notification.status === 'ACCEPTED'; title = isAccept ? 'Invitation Accepted' : 'Invitation Rejected'; body = notification.message || (isAccept ? - `The invitation you shared has been accepted` : - `The invitation you shared has been rejected`); + `The invitation for ${notification.subcriptionName} you shared with ${notification.name} has been accepted` : + `The invitation for ${notification.subcriptionName} you shared with ${notification.name} has been rejected`); data.gymName = notification.gymName || ''; data.clientEmail = notification.clientEmail || ''; data.clientName = notification.name || ''; @@ -568,3 +454,365 @@ export const sendFCMNotificationByType = onRequest({ }); } }); + +export const sendFCMNotificationByPhone = onRequest({ + region: process.env.SERVICES_RGN +}, async (request: Request, response: express.Response) => { + try { + const notificationType = request.body.type || request.query.type; + const phoneNumber = request.body.phoneNumber || request.query.phoneNumber; + + if (!notificationType) { + response.status(400).json({ error: 'Notification type is required' }); + return; + } + + if (!phoneNumber) { + response.status(400).json({ error: 'Phone number is required' }); + return; + } + + const userQuery = await admin + .firestore() + .collection('users') + .where('phoneNumber', '==', phoneNumber) + .get(); + + if (userQuery.empty) { + response.status(404).json({ error: `User not found for phone number: ${phoneNumber}` }); + return; + } + + const userDoc = userQuery.docs[0]; + const user = userDoc.data(); + const fcmToken = user?.fcmToken; + + if (!fcmToken) { + response.status(400).json({ error: `FCM token not found for user with phone: ${phoneNumber}` }); + return; + } + + const invitorIdQuery = admin.firestore().collection('notifications') + .where('phoneNumber', '==', phoneNumber) + .where('type', '==', notificationType); + + const [userIdResults] = await Promise.all([ + invitorIdQuery.get() + ]); + + const notificationDocs = [...userIdResults.docs] + .filter(doc => { + const data = doc.data(); + return data.notificationSent !== true; + }); + + if (notificationDocs.length === 0) { + response.status(404).json({ + error: `No unsent notifications of type '${notificationType}' found for user with phone ${phoneNumber}` + }); + return; + } + + const results = []; + + for (const doc of notificationDocs) { + const notification = doc.data(); + const docId = doc.id; + + let title = 'New Notification'; + let body = notification.message || 'You have a new notification'; + let data: Record = { + type: notification.type, + }; + + switch (notification.type) { + + case 'client_invitations': + let invitationStatus; + if (notification.status === 'ACCEPTED') { + invitationStatus = 'accepted'; + title = 'Invitation Accepted'; + body = notification.message || + `You have rejected the invitation from ${notification.name}`; + } else if (notification.status === 'REJECTED') { + invitationStatus = 'rejected'; + title = 'Invitation Rejected'; + body = notification.message || + `You have rejected the invitation from ${notification.name}`; + } else if (notification.status === 'PENDING') { + invitationStatus = 'pending'; + title = 'New Invitation'; + body = notification.message || + `You have a new invitation pending from ${notification.name}`; + } else { + invitationStatus = 'unknown'; + title = 'Invitation Update'; + body = notification.message || 'There is an update to your invitation'; + } + + data.gymName = notification.gymName || ''; + data.clientEmail = notification.clientEmail || ''; + data.clientName = notification.name || ''; + data.invitationId = notification.invitationId || ''; + data.subscriptionName = notification.subscriptionName || ''; + data.status = invitationStatus; + break; + + default: + logger.info(`Using default handling for notification type: ${notification.type}`); + break; + } + + const message: admin.messaging.Message = { + notification: { + title: title, + body: body, + }, + data: 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, + }; + + try { + const fcmResponse = await admin.messaging().send(message); + logger.info(`FCM notification sent to user with phone ${phoneNumber} for type: ${notification.type}`); + + await admin.firestore().collection('notifications').doc(docId).update({ + notificationSent: true, + sentAt: admin.firestore.FieldValue?.serverTimestamp?.() || new Date() + }); + + results.push({ + success: true, + messageId: fcmResponse + }); + } catch (error) { + logger.error(`Error sending notification ${notification.type}:`, error); + results.push({ + success: false, + type: notification.type, + error: error instanceof Error ? error.message : String(error) + }); + } + } + + response.json({ + success: true, + processed: results.length, + results: results + }); + + } catch (error) { + logger.error('Error processing notifications by phone:', error); + response.status(500).json({ + success: false, + error: error instanceof Error ? error.message : String(error) + }); + } +}); + +export const sendTrainerAssignmentNotification = onRequest({ + region: process.env.SERVICES_RGN +}, async (request: Request, response: express.Response) => { + try { + const notificationType = request.body.type || request.query.type || 'trainer_assigned_to_client'; + const trainerName = request.body.trainerName || request.query.trainerName; + const clientId = request.body.clientId || request.query.clientId; + const gymId = request.body.gymId || request.query.gymId; + + if (!trainerName) { + response.status(400).json({ error: 'Trainer name is required' }); + return; + } + + if (!clientId) { + response.status(400).json({ error: 'Client ID is required' }); + return; + } + + if (!gymId) { + response.status(400).json({ error: 'Gym ID is required' }); + return; + } + + // Find the trainer by name + const trainerQuery = await admin + .firestore() + .collection('trainer_profiles') + .where('fullName', '==', trainerName) + .limit(1) + .get(); + + if (trainerQuery.empty) { + response.status(404).json({ error: `Trainer not found with name: ${trainerName}` }); + return; + } + + const trainerDoc = trainerQuery.docs[0]; + const trainer = trainerDoc.data(); + const trainerId = trainerDoc.id; + + if (!trainer.phoneNumber) { + response.status(400).json({ error: `Phone number not found for trainer: ${trainerName}` }); + return; + } + + // Get client name from client collection + const clientDoc = await admin.firestore().collection('clients').doc(clientId).get(); + if (!clientDoc.exists) { + response.status(404).json({ error: `Client not found with ID: ${clientId}` }); + return; + } + const client = clientDoc.data(); + const clientName = client?.name || client?.fullName || client?.displayName || 'A new client'; + + // Get gym name from gym ID + const gymDoc = await admin.firestore().collection('gyms').doc(gymId).get(); + if (!gymDoc.exists) { + response.status(404).json({ error: `Gym not found with ID: ${gymId}` }); + return; + } + const gym = gymDoc.data(); + const gymName = gym?.name || gym?.gymName || 'your gym'; + + // Find the user document for the trainer using their phone number + const userQuery = await admin + .firestore() + .collection('users') + .where('phoneNumber', '==', trainer.phoneNumber) + .limit(1) + .get(); + + if (userQuery.empty) { + response.status(404).json({ error: `User not found for trainer with phone: ${trainer.phoneNumber}` }); + return; + } + + const userDoc = userQuery.docs[0]; + const user = userDoc.data(); + const fcmToken = user?.fcmToken; + + if (!fcmToken) { + response.status(400).json({ error: `FCM token not found for trainer: ${trainerName}` }); + return; + } + + const notificationQuery = admin.firestore().collection('notifications') + .where('type', '==', notificationType) + .where('clientId', '==', clientId) + .where('gymId', '==', gymId) + .where('trainerName', '==', trainerName); + + const notificationSnapshot = await notificationQuery.get(); + + const unsentNotifications = notificationSnapshot.docs.filter(doc => { + const data = doc.data(); + return data.notificationSent !== true; + }); + + if (unsentNotifications.length === 0) { + response.status(404).json({ + error: `No unsent notifications found for trainer: ${trainerName}, client: ${clientId}, gym: ${gymId}` + }); + return; + } + + const results = []; + + for (const doc of unsentNotifications) { + const docId = doc.id; + + const title = 'New Client Assignment'; + const body = `You have been assigned to ${clientName} at ${gymName}`; + + const data: Record = { + type: notificationType, + trainerId: trainerId, + clientId: clientId, + clientName: clientName, + gymId: gymId, + gymName: gymName + }; + + const message: admin.messaging.Message = { + notification: { + title: title, + body: body, + }, + data: 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, + }; + + try { + const fcmResponse = await admin.messaging().send(message); + logger.info(`Trainer assignment notification sent to ${trainerName}`); + + await admin.firestore().collection('notifications').doc(docId).update({ + notificationSent: true, + sentAt: admin.firestore.FieldValue?.serverTimestamp?.() || new Date() + }); + + results.push({ + success: true, + messageId: fcmResponse + }); + } catch (error) { + logger.error(`Error sending trainer assignment notification:`, error); + results.push({ + success: false, + type: notificationType, + error: error instanceof Error ? error.message : String(error) + }); + } + } + + response.json({ + success: true, + processed: results.length, + results: results + }); + } catch (error) { + logger.error('Error processing trainer assignment notification:', error); + response.status(500).json({ + success: false, + error: error instanceof Error ? error.message : String(error) + }); + } +});