From 05d2b918b4133de30748a80d19695d9da67cbc54 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Thu, 3 Apr 2025 17:09:23 +0530 Subject: [PATCH 1/5] cashfree service added --- firebase.json | 18 +++++ functions/.env.example | 3 + functions/src/index.ts | 152 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 170 insertions(+), 3 deletions(-) diff --git a/firebase.json b/firebase.json index da6f4cb..076f09a 100644 --- a/firebase.json +++ b/firebase.json @@ -21,6 +21,24 @@ "storage": { "rules": "storage.rules" }, + "emulators": { + "functions": { + "port": 5001 + }, + "firestore": { + "port": 8084 + }, + "storage": { + "port": 9199 + }, + "ui": { + "enabled": true, + "port": 4000 + }, + "auth": { + "port": 9099 + } + }, "remoteconfig": { "template": "remoteconfig.template.json" } diff --git a/functions/.env.example b/functions/.env.example index 7c2adec..381bfad 100644 --- a/functions/.env.example +++ b/functions/.env.example @@ -5,3 +5,6 @@ TWILIO_ACCOUNT_SID=#{TWILIO_ACCOUNT_SID}# TWILIO_AUTH_TOKEN=#{TWILIO_AUTH_TOKEN}# TWILIO_PHONE_NUMBER=#{TWILIO_PHONE_NUMBER}# SERVICES_RGN=#{SERVICES_RGN}# +CASHFREE_CLIENT_ID=#{CASHFREE_CLIENT_ID}# +CASHFREE_CLIENT_SECRET=#{CASHFREE_CLIENT_SECRET}# + diff --git a/functions/src/index.ts b/functions/src/index.ts index 33a2aa5..f8c6544 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -8,6 +8,8 @@ import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; import * as https from 'https'; +import axios from "axios"; + const formData = require('form-data'); const Mailgun = require('mailgun.js'); const { convert } = require('html-to-text'); @@ -78,7 +80,7 @@ export const sendEmailWithAttachment = onRequest({ } }); export const sendEmailMessage = onRequest({ - region: '#{SERVICES_RGN}#' + region: process.env.SERVICES_RGN }, (request: Request, response: express.Response) => { const mailgun = new Mailgun(formData); const mailGunClient = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY }); @@ -107,7 +109,7 @@ export const sendEmailMessage = onRequest({ }); export const sendSMSMessage = onRequest({ - region: '#{SERVICES_RGN}#' + region: process.env.SERVICES_RGN }, (request: Request, response: express.Response) => { const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN); const { to, body } = request.body; @@ -136,7 +138,7 @@ interface Invitation { export const notifyInvitation = onDocumentCreated({ document: 'notifications/{notificationId}', - region: '#{SERVICES_RGN}#' + region: process.env.SERVICES_RGN }, async (event: any) => { const invitation = event.data?.data() as Invitation; const invitationId = event.params.invitationId; @@ -204,5 +206,149 @@ export const notifyInvitation = onDocumentCreated({ } }); +export const createCashfreeOrder = onRequest({ + region: process.env.SERVICES_RGN +}, async (request: Request, response: express.Response) => { + try { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + response.status(401).json({ error: 'Unauthorized' }); + return; + } + const idToken = authHeader.split('Bearer ')[1]; + const decodedToken = await admin.auth().verifyIdToken(idToken); + const uid = decodedToken.uid; + const { + amount, + customerName, + customerEmail, + customerPhone, + productInfo + } = request.body; + + if (!amount || !customerEmail || !customerPhone) { + response.status(400).json({ error: 'Missing required fields' }); + return; + } + + const clientId = process.env.CASHFREE_CLIENT_ID; + const clientSecret = process.env.CASHFREE_CLIENT_SECRET; + const isTest = true; + + const apiUrl = isTest + ? 'https://sandbox.cashfree.com/pg/orders' + : 'https://api.cashfree.com/pg/orders'; + + const orderId = `order_${Date.now()}_${uid.substring(0, 6)}`; + + const cashfreeResponse = await axios.post( + apiUrl, + { + order_id: orderId, + order_amount: amount, + order_currency: 'INR', + customer_details: { + customer_id: uid, + customer_name: customerName || 'Fitlien User', + customer_email: customerEmail, + customer_phone: customerPhone + }, + order_meta: { + return_url: `https://fitlien.com/payment/status?order_id={order_id}`, + // notify_url: `https://$filien.web.app/verifyCashfreePayment` + }, + order_note: productInfo || 'Fitlien Membership' + }, + { + headers: { + 'x-api-version': '2022-09-01', + 'x-client-id': clientId, + 'x-client-secret': clientSecret, + 'Content-Type': 'application/json' + } + } + ); + + await admin.firestore().collection('payment_orders').doc(orderId).set({ + userId: uid, + amount: amount, + customerEmail: customerEmail, + customerPhone: customerPhone, + orderStatus: 'CREATED', + paymentGateway: 'Cashfree', + createdAt: admin.firestore.FieldValue.serverTimestamp(), + ...cashfreeResponse.data + }); + + response.json({ + order_id: cashfreeResponse.data.order_id, + payment_session_id: cashfreeResponse.data.payment_session_id + }); + + logger.info(`Cashfree order created: ${orderId}`); + } catch (error: any) { + logger.error('Cashfree order creation error:', error); + response.status(500).json({ + error: 'Failed to create payment order', + details: error.response?.data || error.message + }); + } +}); + +export const verifyCashfreePayment = onRequest({ + region: process.env.SERVICES_RGN +}, async (request: Request, response: express.Response) => { + try { + const orderId = request.body.order_id || request.query.order_id; + + if (!orderId) { + response.status(400).json({ error: 'Order ID is required' }); + return; + } + + const clientId = process.env.CASHFREE_CLIENT_ID; + const clientSecret = process.env.CASHFREE_CLIENT_SECRET; + const isTest = process.env.CASHFREE_ENVIRONMENT !== 'production'; + + const apiUrl = isTest + ? `https://sandbox.cashfree.com/pg/orders/${orderId}` + : `https://api.cashfree.com/pg/orders/${orderId}`; + + const cashfreeResponse = await axios.get( + apiUrl, + { + headers: { + 'x-api-version': '2022-09-01', + 'x-client-id': clientId, + 'x-client-secret': clientSecret + } + } + ); + + await admin.firestore().collection('payment_orders').doc(orderId).update({ + orderStatus: cashfreeResponse.data.order_status, + paymentDetails: cashfreeResponse.data, + updatedAt: admin.firestore.FieldValue.serverTimestamp() + }); + + if (request.headers['x-webhook-source'] === 'cashfree') { + response.status(200).send('OK'); + return; + } + + response.json({ + status: cashfreeResponse.data.order_status, + paymentDetails: cashfreeResponse.data + }); + + logger.info(`Cashfree payment verified: ${orderId}`); + } catch (error: any) { + logger.error('Cashfree payment verification error:', error); + response.status(500).json({ + error: 'Failed to verify payment status', + details: error.response?.data || error.message + }); + } +}); -- 2.43.0 From c32e5b1d63f9978bf6c5004813ab93d61ad401eb Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Thu, 3 Apr 2025 19:18:47 +0530 Subject: [PATCH 2/5] Update index.ts --- functions/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/functions/src/index.ts b/functions/src/index.ts index f8c6544..c5f1c80 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -278,7 +278,7 @@ export const createCashfreeOrder = onRequest({ customerPhone: customerPhone, orderStatus: 'CREATED', paymentGateway: 'Cashfree', - createdAt: admin.firestore.FieldValue.serverTimestamp(), + createdAt: new Date(), ...cashfreeResponse.data }); @@ -330,7 +330,7 @@ export const verifyCashfreePayment = onRequest({ await admin.firestore().collection('payment_orders').doc(orderId).update({ orderStatus: cashfreeResponse.data.order_status, paymentDetails: cashfreeResponse.data, - updatedAt: admin.firestore.FieldValue.serverTimestamp() + updatedAt: new Date() }); if (request.headers['x-webhook-source'] === 'cashfree') { -- 2.43.0 From f8803c4ff4ffb9ef19385fa9635d56c7ef860705 Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:44:12 +0530 Subject: [PATCH 3/5] Update index.ts --- functions/src/index.ts | 260 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 238 insertions(+), 22 deletions(-) diff --git a/functions/src/index.ts b/functions/src/index.ts index c5f1c80..4812211 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -20,7 +20,7 @@ if (!admin.apps.length) { admin.initializeApp(); } export const sendEmailWithAttachment = onRequest({ - region: '#{SERVICES_RGN}#' + region: process.env.SERVICES_RGN }, async (request: Request, response: express.Response) => { try { const { toAddress, subject, message, fileUrl, fileName } = request.body; @@ -134,10 +134,13 @@ interface Invitation { phoneNumber: string; gymName: string; invitedByName: string; + status: string; + notificationSent: boolean; + subscriptionName?: string; } -export const notifyInvitation = onDocumentCreated({ - document: 'notifications/{notificationId}', +export const sendInvitationNotification = onDocumentCreated({ + document: 'client_invitations/{invitationId}', region: process.env.SERVICES_RGN }, async (event: any) => { const invitation = event.data?.data() as Invitation; @@ -148,41 +151,60 @@ export const notifyInvitation = onDocumentCreated({ 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('email', '==', invitation.email) .where('phoneNumber', '==', invitation.phoneNumber) .limit(1) .get(); - + if (userQuery.empty) { - console.log( - `User not found for email: ${invitation.email} and phone: ${invitation.phoneNumber}.` - ); + console.log(`User not found for phone: ${invitation.phoneNumber}`); return null; } - - const user = userQuery.docs[0].data(); - const fcmToken = user.fcmToken; + + const userDoc = userQuery.docs[0]; + const user = userDoc.data(); + const fcmToken = user?.fcmToken; if (!fcmToken) { - console.log(`FCM token not found for user: ${invitation.email}.`); + 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: 'New Gym Invitation', - body: `${invitation.invitedByName} has invited you to join ${invitation.gymName}`, - }, - data: { - type: 'invitation', - invitationId: invitationId, - gymName: invitation.gymName, - senderName: invitation.invitedByName, + title: title, + body: body, }, + data: data, android: { priority: 'high', notification: { @@ -194,11 +216,25 @@ export const notifyInvitation = onDocumentCreated({ clickAction: 'FLUTTER_NOTIFICATION_CLICK', }, }, + apns: { + payload: { + aps: { + sound: 'default', + badge: 1, + }, + }, + }, token: fcmToken, }; await admin.messaging().send(message); - console.log(`Invitation notification sent to ${invitation.email}.`); + 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); @@ -257,7 +293,7 @@ export const createCashfreeOrder = onRequest({ }, order_meta: { return_url: `https://fitlien.com/payment/status?order_id={order_id}`, - // notify_url: `https://$filien.web.app/verifyCashfreePayment` + notify_url: `https://filien.web.app/verifyCashfreePayment` }, order_note: productInfo || 'Fitlien Membership' }, @@ -352,3 +388,183 @@ export const verifyCashfreePayment = onRequest({ }); } }); + +export const sendFCMNotificationByType = onRequest({ + region: process.env.SERVICES_RGN +}, async (request: Request, response: express.Response) => { + try { + const notificationType = request.body.type || request.query.type; + const userId = request.body.userId || request.body.clientId || request.query.userId || request.query.clientId || request.body.invitorId || request.query.invitorId; + + if (!notificationType) { + response.status(400).json({ error: 'Notification type is required' }); + return; + } + + if (!userId) { + response.status(400).json({ error: 'User ID is required' }); + return; + } + + const userDoc = await admin.firestore().collection('users').doc(userId).get(); + + if (!userDoc.exists) { + response.status(404).json({ error: `User not found for ID: ${userId}` }); + return; + } + + const user = userDoc.data(); + const fcmToken = user?.fcmToken; + + if (!fcmToken) { + response.status(400).json({ error: `FCM token not found for user: ${userId}` }); + return; + } + + const userIdQuery = admin.firestore().collection('notifications') + .where('userId', '==', userId) + .where('type', '==', notificationType); + + const clientIdQuery = admin.firestore().collection('notifications') + .where('clientId', '==', userId) + .where('type', '==', notificationType); + + const invitorIdQuery = admin.firestore().collection('notifications') + .where('invitorId', '==', userId) + .where('type', '==', notificationType); + + const [userIdResults, clientIdResults, invitorIdResults] = await Promise.all([ + userIdQuery.get(), + clientIdQuery.get(), + invitorIdQuery.get() + ]); + + const notificationDocs = [...userIdResults.docs, ...clientIdResults.docs, ...invitorIdResults.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/client ${userId}` + }); + return; + } + + const results = []; + + for (const doc of notificationDocs) { + const notification = doc.data(); + const docId = doc.id; + if (notification.notificationSent) { + logger.info(`Notification ${notificationType} already sent, skipping.`); + continue; + } + + let title = 'New Notification'; + let body = notification.message || 'You have a new notification'; + let data: Record = { + type: notification.type, + }; + + 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': + 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`); + data.gymName = notification.gymName || ''; + data.clientEmail = notification.clientEmail || ''; + data.clientName = notification.name || ''; + data.invitationId = notification.invitationId || ''; + data.subcriptionname= notification.subcriptionname || ''; + 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 ${userId} 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 ${notificationType}:`, 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 notifications:', error); + response.status(500).json({ + success: false, + error: error instanceof Error ? error.message : String(error) + }); + } +}); -- 2.43.0 From 0f177ad902badb727b16190ba190d4f8eac171fd Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 8 Apr 2025 04:54:48 +0530 Subject: [PATCH 4/5] device notifications --- functions/src/index.ts | 480 +++++++++++++++++++++++++++++++---------- 1 file changed, 364 insertions(+), 116 deletions(-) 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) + }); + } +}); -- 2.43.0 From 10edbc9fcf352836ff7fb1dcb060dc33dfad026e Mon Sep 17 00:00:00 2001 From: AllenTJ7 <163137620+AllenTJ7@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:50:47 +0530 Subject: [PATCH 5/5] document change --- firebase.json | 2 +- functions/src/index.ts | 648 +++++++++-------------------------------- 2 files changed, 144 insertions(+), 506 deletions(-) diff --git a/firebase.json b/firebase.json index 076f09a..abf2c13 100644 --- a/firebase.json +++ b/firebase.json @@ -26,7 +26,7 @@ "port": 5001 }, "firestore": { - "port": 8084 + "port": 8079 }, "storage": { "port": 9199 diff --git a/functions/src/index.ts b/functions/src/index.ts index baaf43f..16c3edc 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -8,6 +8,7 @@ import * as path from 'path'; import * as fs from 'fs'; import * as https from 'https'; import axios from "axios"; +import { onDocumentCreated } from "firebase-functions/firestore"; const formData = require('form-data'); const Mailgun = require('mailgun.js'); @@ -275,279 +276,122 @@ export const verifyCashfreePayment = onRequest({ } }); -export const sendFCMNotificationByType = onRequest({ - region: process.env.SERVICES_RGN -}, async (request: Request, response: express.Response) => { +export const processNotificationOnCreate = onDocumentCreated({ + region: process.env.SERVICES_RGN, + document: 'notifications/{notificationId}' +}, async (event) => { try { - const notificationType = request.body.type || request.query.type; - const userId = request.body.userId || request.body.clientId || request.query.userId || request.query.clientId || request.body.invitorId || request.query.invitorId; + const notification = event.data?.data(); + const notificationId = event.params.notificationId; - if (!notificationType) { - response.status(400).json({ error: 'Notification type is required' }); + if (!notification) { + logger.error(`No data found for notification ${notificationId}`); return; } - if (!userId) { - response.status(400).json({ error: 'User ID is required' }); + if (notification.notificationSent === true) { + logger.info(`Notification ${notificationId} already sent, skipping.`); return; } - const userDoc = await admin.firestore().collection('users').doc(userId).get(); + let userId = null; + let fcmToken = null; - if (!userDoc.exists) { - response.status(404).json({ error: `User not found for ID: ${userId}` }); - return; + if (notification.userId) { + userId = notification.userId; + const userDoc = await admin.firestore().collection('users').doc(userId).get(); + if (userDoc.exists) { + fcmToken = userDoc.data()?.fcmToken; + } + } else if (notification.clientId) { + userId = notification.clientId; + const userDoc = await admin.firestore().collection('users').doc(userId).get(); + if (userDoc.exists) { + fcmToken = userDoc.data()?.fcmToken; + } + } else if (notification.invitorId) { + userId = notification.invitorId; + const userDoc = await admin.firestore().collection('users').doc(userId).get(); + if (userDoc.exists) { + fcmToken = userDoc.data()?.fcmToken; + } + } else if (notification.phoneNumber) { + const userQuery = await admin + .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; + } } - const user = userDoc.data(); - const fcmToken = user?.fcmToken; - if (!fcmToken) { - response.status(400).json({ error: `FCM token not found for user: ${userId}` }); - return; - } - - const userIdQuery = admin.firestore().collection('notifications') - .where('userId', '==', userId) - .where('type', '==', notificationType); - - const clientIdQuery = admin.firestore().collection('notifications') - .where('clientId', '==', userId) - .where('type', '==', notificationType); - - const invitorIdQuery = admin.firestore().collection('notifications') - .where('invitorId', '==', userId) - .where('type', '==', notificationType); - - const [userIdResults, clientIdResults, invitorIdResults] = await Promise.all([ - userIdQuery.get(), - clientIdQuery.get(), - invitorIdQuery.get() - ]); - - const notificationDocs = [...userIdResults.docs, ...clientIdResults.docs, ...invitorIdResults.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/client ${userId}` + logger.error(`FCM token not found for notification ${notificationId}`); + await admin.firestore().collection('notifications').doc(notificationId).update({ + notificationError: 'FCM token not found for user', + updatedAt: admin.firestore.FieldValue?.serverTimestamp?.() || new Date() }); return; } - const results = []; + let title = 'New Notification'; + let body = notification.message || 'You have a new notification'; + let data: Record = { + type: notification.type, + }; - for (const doc of notificationDocs) { - const notification = doc.data(); - const docId = doc.id; - if (notification.notificationSent) { - logger.info(`Notification ${notificationType} already sent, skipping.`); - continue; - } - - let title = 'New Notification'; - let body = notification.message || 'You have a new notification'; - let data: Record = { - type: notification.type, - }; - - 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': - const isAccept = notification.status === 'ACCEPTED'; - title = isAccept ? 'Invitation Accepted' : 'Invitation Rejected'; - body = notification.message || (isAccept ? - `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 || ''; - data.invitationId = notification.invitationId || ''; - data.subcriptionname= notification.subcriptionname || ''; - 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 ${userId} for type: ${notification.type}`); + 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; - await admin.firestore().collection('notifications').doc(docId).update({ - notificationSent: true, - sentAt: admin.firestore.FieldValue?.serverTimestamp?.() || new Date() - }); + 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; - results.push({ - success: true, - messageId: fcmResponse - }); - } catch (error) { - logger.error(`Error sending notification ${notificationType}:`, 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 notifications:', error); - response.status(500).json({ - success: false, - error: error instanceof Error ? error.message : String(error) - }); - } -}); - -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'; + 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) { + let invitationStatus; + if (notification.status === 'ACCEPTED') { + invitationStatus = 'accepted'; + title = 'Invitation Accepted'; + body = notification.message || + `You have accepted 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.status = invitationStatus; } data.gymName = notification.gymName || ''; @@ -555,264 +399,58 @@ export const sendFCMNotificationByPhone = onRequest({ 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 = { + + 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: { - title: title, - body: body, - }, - data: data, - android: { + channelId: 'notifications_channel', priority: 'high', - notification: { - channelId: 'notifications_channel', - priority: 'high', - defaultSound: true, - defaultVibrateTimings: true, - icon: '@mipmap/ic_launcher', - clickAction: 'FLUTTER_NOTIFICATION_CLICK', + 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, - }; + }, + token: fcmToken, + }; + + try { + const fcmResponse = await admin.messaging().send(message); + logger.info(`FCM notification sent successfully: ${fcmResponse}`); - 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}` + await admin.firestore().collection('notifications').doc(notificationId).update({ + notificationSent: true, + sentAt: admin.firestore.FieldValue?.serverTimestamp?.() || new Date() + }); + } catch (error) { + logger.error(`Error sending notification ${notificationId}:`, error); + + await admin.firestore().collection('notifications').doc(notificationId).update({ + notificationError: error instanceof Error ? error.message : String(error), + updatedAt: admin.firestore.FieldValue?.serverTimestamp?.() || new Date() }); - 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) - }); + logger.error('Error processing notification:', error); } -}); +}); \ No newline at end of file -- 2.43.0