Compare commits

..

1 Commits

Author SHA1 Message Date
5a34c0e686 Changes updated 2025-07-04 15:03:04 +05:30
13 changed files with 539 additions and 1501 deletions

View File

@ -19,6 +19,9 @@ jobs:
with: with:
node-version: 22 node-version: 22
- name: Clean install
run: npm clean-install
- name: Copy .env.example to .env - name: Copy .env.example to .env
run: cp functions/.env.example functions/.env run: cp functions/.env.example functions/.env

View File

@ -19,6 +19,9 @@ jobs:
with: with:
node-version: 22 node-version: 22
- name: Clean install
run: npm clean-install
- name: Copy .env.example to .env - name: Copy .env.example to .env
run: cp functions/.env.example functions/.env run: cp functions/.env.example functions/.env

View File

@ -19,6 +19,9 @@ jobs:
with: with:
node-version: 22 node-version: 22
- name: Clean install
run: npm clean-install
- name: Copy .env.example to .env - name: Copy .env.example to .env
run: cp functions/.env.example functions/.env run: cp functions/.env.example functions/.env

View File

@ -56,34 +56,6 @@
} }
] ]
}, },
{
"collectionGroup": "gyms",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "gyms",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isApproved",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "ASCENDING"
}
]
},
{ {
"collectionGroup": "memberships", "collectionGroup": "memberships",
"queryScope": "COLLECTION", "queryScope": "COLLECTION",
@ -103,11 +75,7 @@
"queryScope": "COLLECTION", "queryScope": "COLLECTION",
"fields": [ "fields": [
{ {
"fieldPath": "type", "fieldPath": "clientId",
"order": "ASCENDING"
},
{
"fieldPath": "userId",
"order": "ASCENDING" "order": "ASCENDING"
}, },
{ {
@ -121,7 +89,75 @@
"queryScope": "COLLECTION", "queryScope": "COLLECTION",
"fields": [ "fields": [
{ {
"fieldPath": "recipientId", "fieldPath": "clientId",
"order": "ASCENDING"
},
{
"fieldPath": "type",
"order": "ASCENDING"
},
{
"fieldPath": "timestamp",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "notifications",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "ownerId",
"order": "ASCENDING"
},
{
"fieldPath": "timestamp",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "notifications",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "ownerId",
"order": "ASCENDING"
},
{
"fieldPath": "type",
"order": "ASCENDING"
},
{
"fieldPath": "timestamp",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "notifications",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "trainerId",
"order": "ASCENDING"
},
{
"fieldPath": "timestamp",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "notifications",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "type",
"order": "ASCENDING"
},
{
"fieldPath": "userId",
"order": "ASCENDING" "order": "ASCENDING"
}, },
{ {
@ -176,6 +212,34 @@
} }
] ]
}, },
{
"collectionGroup": "gyms",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "userid",
"order": "ASCENDING"
},
{
"fieldPath": "name",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "gyms",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isApproved",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "ASCENDING"
}
]
},
{ {
"collectionGroup": "terms_and_conditions", "collectionGroup": "terms_and_conditions",
"queryScope": "COLLECTION", "queryScope": "COLLECTION",

View File

@ -15,7 +15,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"firebase-admin": "^12.6.0", "firebase-admin": "^12.6.0",
"firebase-functions": "^6.4.0", "firebase-functions": "^6.0.1",
"form-data": "^4.0.1", "form-data": "^4.0.1",
"functions": "file:", "functions": "file:",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
@ -4897,10 +4897,9 @@
} }
}, },
"node_modules/firebase-functions": { "node_modules/firebase-functions": {
"version": "6.4.0", "version": "6.3.2",
"resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.4.0.tgz", "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.3.2.tgz",
"integrity": "sha512-Q/LGhJrmJEhT0dbV60J4hCkVSeOM6/r7xJS/ccmkXzTWMjo+UPAYX9zlQmGlEjotstZ0U9GtQSJSgbB2Z+TJDg==", "integrity": "sha512-FC3A1/nhqt1ZzxRnj5HZLScQaozAcFSD/vSR8khqSoFNOfxuXgwJS6ZABTB7+v+iMD5z6Mmxw6OfqITUBuI7OQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@types/cors": "^2.8.5", "@types/cors": "^2.8.5",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",

View File

@ -22,7 +22,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"firebase-admin": "^12.6.0", "firebase-admin": "^12.6.0",
"firebase-functions": "^6.4.0", "firebase-functions": "^6.0.1",
"form-data": "^4.0.1", "form-data": "^4.0.1",
"functions": "file:", "functions": "file:",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",

View File

@ -207,10 +207,10 @@ function createGetEmployeePunchLogsRequest(username: string, password: string,
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope"> <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
<soap12:Body> <soap12:Body>
<GetEmployeePunchLogs xmlns="http://tempuri.org/"> <GetEmployeePunchLogs xmlns="http://tempuri.org/">
<UserName>${escapeXml(username)}</UserName> <UserName>cosqclient</UserName>
<Password>${escapeXml(password)}</Password> <Password>3bbb58d5</Password>
<EmployeeCode>${escapeXml(employeeCode)}</EmployeeCode> <EmployeeCode>1</EmployeeCode>
<AttendanceDate>${escapeXml(attendanceDate)}</AttendanceDate> <AttendanceDate>2025-05-24</AttendanceDate>
</GetEmployeePunchLogs> </GetEmployeePunchLogs>
</soap12:Body> </soap12:Body>
</soap12:Envelope>`; </soap12:Envelope>`;
@ -232,15 +232,9 @@ function parseGetEmployeePunchLogsResponse(soapResponse: string, attendanceDate:
const xmlDoc = parser.parseFromString(soapResponse, "text/xml"); const xmlDoc = parser.parseFromString(soapResponse, "text/xml");
const currentElement = xmlDoc.documentElement.firstChild as HTMLElement; const currentElement = xmlDoc.documentElement.firstChild as HTMLElement;
const resultText = currentElement.textContent; const resultText = currentElement.textContent;
if (!resultText || resultText.trim() === '' || resultText.trim() === ';;' || resultText.trim() === ';') {
return [];
}
const punchLogs: Date[] = []; const punchLogs: Date[] = [];
const parts = resultText.split(';'); const parts = resultText!.split(';');
for (const part of parts) { for (const part of parts) {
if (!part || part.trim() === '') {
continue;
}
try { try {
const logDateTime = new Date(part); const logDateTime = new Date(part);
if (isNaN(logDateTime.getTime())) { if (isNaN(logDateTime.getTime())) {
@ -251,12 +245,8 @@ function parseGetEmployeePunchLogsResponse(soapResponse: string, attendanceDate:
try { try {
const timeParts = part.split(','); const timeParts = part.split(',');
for (const timePart of timeParts) { for (const timePart of timeParts) {
if (!timePart || timePart.trim() === '') {
continue;
}
try { try {
const logDateTime = createDateFromTime(rootDate, timePart.trim()); const logDateTime = createDateFromTime(rootDate, timePart);
punchLogs.push(logDateTime); punchLogs.push(logDateTime);
} catch { } catch {
continue; continue;
@ -271,7 +261,6 @@ function parseGetEmployeePunchLogsResponse(soapResponse: string, attendanceDate:
return sortedLogs; return sortedLogs;
} }
async function sendSoapRequest(soapRequest: string, endpoint: string) { async function sendSoapRequest(soapRequest: string, endpoint: string) {
try { try {
const headers: any = { const headers: any = {

View File

@ -7,13 +7,13 @@ setGlobalOptions({
minInstances: 0, minInstances: 0,
maxInstances: 10, maxInstances: 10,
concurrency: 80 concurrency: 80
}); });
export * from './shared/config'; export * from './shared/config';
export { sendEmailSES } from './email'; export { sendEmailSES } from './email';
export { sendSMSMessage } from './sms'; export { sendSMSMessage } from './sms';
export { accessFile } from './storage'; export { accessFile } from './storage';
export { processNotificationOnCreate,checkExpiredMemberships } from './notifications'; export { processNotificationOnCreate } from './notifications';
export * from './payments'; export * from './payments';
export { getPlaceDetails, getPlacesAutocomplete } from './places'; export { getPlaceDetails, getPlacesAutocomplete } from './places';
export { registerClient } from './users'; export { registerClient } from './users';

View File

@ -1,3 +1 @@
export { processNotificationOnCreate } from './processNotification'; export { processNotificationOnCreate } from './processNotification';
export { checkExpiredMemberships } from "./membershipStatusNotifications";

File diff suppressed because it is too large Load Diff

View File

@ -1,422 +1,222 @@
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; notificationSent?: boolean;
recipientId?: string; userId?: string;
type?: string; clientId?: string;
notificationSent?: boolean; invitorId?: string;
timestamp?: admin.firestore.FieldValue; phoneNumber?: string;
read?: boolean; message?: string;
data?: { [key: string]: any }; 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( export const processNotificationOnCreate = onDocumentCreated({
{ region: '#{SERVICES_RGN}#',
region: "#{SERVICES_RGN}#", document: 'notifications/{notificationId}'
document: "notifications/{notificationId}", }, async (event) => {
},
async (event) => {
try { try {
const notificationSnapshot = event.data; const notificationSnapshot = event.data;
const notificationId = event.params.notificationId; const notificationId = event.params.notificationId;
if (!notificationSnapshot) { if (!notificationSnapshot) {
logger.error(`No data found for notification ${notificationId}`); logger.error(`No data found for notification ${notificationId}`);
return; return;
} }
const notification = notificationSnapshot.data() as NotificationData; const notification = notificationSnapshot.data() as NotificationData;
if (notification.notificationSent === true) {
logger.info(`Notification ${notificationId} already sent, skipping.`);
return;
}
if (notification.notificationSent === true) { const { fcmToken } = await getUserAndFCMToken(notification);
logger.info(`Notification ${notificationId} already sent, skipping.`); if (!fcmToken) {
return; logger.error(`FCM token not found for notification ${notificationId}`);
} await updateNotificationWithError(notificationId, 'FCM token not found for user');
return;
}
logger.info( const message = prepareNotificationMessage(notification, fcmToken);
`Processing notification ${notificationId} of type: ${notification.type}` try {
); const fcmResponse = await app.messaging().send({
...message,
token: fcmToken
});
const { userId, fcmToken } = await getUserAndFCMToken(notification); logger.info(`FCM notification sent successfully: ${fcmResponse}`);
if (!fcmToken) { await markNotificationAsSent(notificationId);
logger.error(
`FCM token not found for notification ${notificationId}, user: ${userId}`
);
await updateNotificationWithError(
notificationId,
"FCM token not found for user"
);
return;
}
const message = prepareNotificationMessage(notification, fcmToken); } catch (error) {
try { logger.error(`Error sending notification ${notificationId}:`, error);
const fcmResponse = await app.messaging().send(message); await updateNotificationWithError(notificationId, error instanceof Error ? error.message : String(error));
}
logger.info(`FCM notification sent successfully: ${fcmResponse}`);
await markNotificationAsSent(notificationId);
} catch (error) {
logger.error(`Error sending notification ${notificationId}:`, error);
await updateNotificationWithError(
notificationId,
error instanceof Error ? error.message : String(error)
);
}
} catch (error) { } catch (error) {
logger.error("Error processing notification:", error); logger.error('Error processing notification:', error);
} }
} });
);
async function getUserAndFCMToken( async function getUserAndFCMToken(notification: NotificationData): Promise<{ userId: string | null; fcmToken: string | null }> {
notification: NotificationData let userId: string | null = null;
): Promise<{ userId: string | null; fcmToken: string | null }> { let fcmToken: string | null = null;
let targetUserId: string | null = null;
let fcmToken: string | null = null;
if (notification.recipientId) { if (notification.userId) {
targetUserId = notification.recipientId; userId = notification.userId;
logger.info(`Using top-level recipientId: ${targetUserId}`); fcmToken = await getFCMTokenFromUserDoc(userId);
} else if (notification.data?.phoneNumber) { } else if (notification.clientId) {
logger.info( userId = notification.clientId;
`Looking up user by phone number from data: ${notification.data.phoneNumber}` fcmToken = await getFCMTokenFromUserDoc(userId);
); } else if (notification.invitorId) {
const userQuery = await app userId = notification.invitorId;
.firestore() fcmToken = await getFCMTokenFromUserDoc(userId);
.collection("users") } else if (notification.phoneNumber) {
.where("phoneNumber", "==", notification.data.phoneNumber) const userQuery = await app
.limit(1) .firestore()
.get(); .collection('users')
.where('phoneNumber', '==', notification.phoneNumber)
.limit(1)
.get();
if (!userQuery.empty) { if (!userQuery.empty) {
const userDoc = userQuery.docs[0]; const userDoc = userQuery.docs[0];
targetUserId = userDoc.id; userId = userDoc.id;
fcmToken = userDoc.data()?.fcmToken; fcmToken = userDoc.data()?.fcmToken;
logger.info(`Found user by phone: ${targetUserId}`); }
} else {
logger.warn(
`No user found with phone number from data: ${notification.data.phoneNumber}`
);
} }
} else {
logger.error("No valid user identifier found in notification or its data");
}
if (targetUserId && !fcmToken) { return { userId, fcmToken };
fcmToken = await getFCMTokenFromUserDoc(targetUserId);
}
if (targetUserId && !fcmToken) {
logger.warn(`User ${targetUserId} found but no FCM token available`);
}
return { userId: targetUserId, fcmToken };
} }
async function getFCMTokenFromUserDoc(userId: string): Promise<string | null> { async function getFCMTokenFromUserDoc(userId: string): Promise<string | null> {
try { const userDoc = await app.firestore().collection('users').doc(userId).get();
const userDoc = await app.firestore().collection("users").doc(userId).get(); return userDoc.exists ? userDoc.data()?.fcmToken : null;
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( function prepareNotificationMessage(notification: NotificationData, fcmToken: string): admin.messaging.Message {
notification: NotificationData, let title = 'New Notification';
fcmToken: string let body = notification.message || 'You have a new notification';
): admin.messaging.TokenMessage { let data: Record<string, string> = {
let title = notification.data?.title || "New Notification"; type: notification.type || 'general',
let body = notification.data?.message || "You have a new notification"; };
let fcmData: Record<string, string> = { switch (notification.type) {
type: notification.type || "general", case 'day_pass_entry':
notificationId: "notification_" + Date.now().toString(), 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;
if (notification.senderId) fcmData.senderId = notification.senderId; case 'trainer_assigned_to_client':
if (notification.recipientId) fcmData.recipientId = notification.recipientId; title = 'Trainer Assigned';
if (notification.read !== undefined) fcmData.read = String(notification.read); body = notification.message || `${notification.trainerName} has been assigned as your trainer`;
data.trainerName = notification.trainerName || '';
data.membershipId = notification.membershipId || '';
break;
if (notification.data) { case 'client_invitations':
for (const key in notification.data) { if (notification.userId || notification.invitorId) {
if (Object.prototype.hasOwnProperty.call(notification.data, key)) { const isAccept = notification.status === 'ACCEPTED';
const value = notification.data[key]; title = isAccept ? 'Invitation Accepted' : 'Invitation Rejected';
if (typeof value === "object" && value !== null) { body = notification.message || (isAccept ?
fcmData[key] = JSON.stringify(value); `The invitation for ${notification.subscriptionName} you shared with ${notification.name} has been accepted` :
} else { `The invitation for ${notification.subscriptionName} you shared with ${notification.name} has been rejected`);
fcmData[key] = String(value); } 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;
} }
}
switch (notification.type) { const notificationMessage: admin.messaging.Message = {
case "trainer_response": notification: { title, body },
title = data,
notification.data?.title || android: {
(notification.data?.status === "accepted" priority: 'high',
? "Trainer Invitation Accepted" notification: {
: "Trainer Invitation Update"); channelId: 'notifications_channel',
body = priority: 'high',
notification.data?.message || defaultSound: true,
`${ defaultVibrateTimings: true,
notification.data?.trainerName icon: '@mipmap/ic_launcher',
} has ${notification.data?.status?.toLowerCase()} your request`; clickAction: 'FLUTTER_NOTIFICATION_CLICK',
break; },
case "trainer_assignment":
title = notification.data?.title || "New Client Assignment";
body =
notification.data?.message ||
`You have been assigned to train ${notification.data?.name}.`;
break;
case "trainer_assigned_to_client":
title = notification.data?.title || "Trainer Assigned";
body =
notification.data?.message ||
`${notification.data?.trainerName} has been assigned as your trainer.`;
break;
case "trainer_update_owner":
title = notification.data?.title || "Trainer Schedule Update";
body =
notification.data?.message || "A trainer has updated their schedule";
break;
case "trainer_update_client":
title = notification.data?.title || "Schedule Update";
body =
notification.data?.message || "Your training schedule has been updated";
if (notification.data?.oldTimeSlot && notification.data?.newTimeSlot) {
body += ` from ${notification.data.oldTimeSlot} to ${notification.data.newTimeSlot}`;
if (notification.data?.formattedDate) {
body += ` on ${notification.data.formattedDate}`;
}
}
break;
case "plan_renewal":
title = notification.data?.title || "Plan Renewal";
body =
notification.data?.message ||
`Plan ${notification.data?.subscriptionName} has been renewed`;
break;
case "plan_assigned":
title = notification.data?.title || "New Plan Assigned";
body =
notification.data?.message ||
`You have been assigned ${notification.data?.subscriptionName} at ${notification.data?.gymName}`;
break;
case "plan_expired":
title = notification.data?.title || "Plan Expired";
body =
notification.data?.message ||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} has expired.`;
break;
case "plan_expiring_soon":
title = notification.data?.title || "Plan Expiring Soon";
body =
notification.data?.message ||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} will expire on ${notification.data?.formattedExpiryDate}.`;
break;
case "trainer_client_plan_expired":
title = notification.data?.title || "Client Plan Expired";
body =
notification.data?.message ||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} has expired.`;
break;
case "trainer_client_plan_expiring":
title = notification.data?.title || "Client Plan Expiring Soon";
body =
notification.data?.message ||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} will expire on ${notification.data?.formattedExpiryDate}.`;
break;
case "schedule_update":
title = notification.data?.title || "Schedule Update";
body =
notification.data?.message || "Your training schedule has been updated";
if (notification.data?.oldTimeSlot && notification.data?.newTimeSlot) {
body += ` from ${notification.data.oldTimeSlot} to ${notification.data.newTimeSlot}`;
if (notification.data?.formattedDate) {
body += ` on ${notification.data.formattedDate}`;
}
}
break;
case "attendance_dispute":
title = notification.data?.title || "Attendance Dispute";
body =
notification.data?.message ||
`${notification.data?.name} has disputed an attendance record`;
if (notification.data?.logTime) {
body += ` for ${notification.data.logTime}`;
}
break;
case "day_pass_entry":
const isAccepted = notification.data?.status === "ACCEPTED";
title =
notification.data?.title ||
(isAccepted ? "Day Pass Approved" : "Day Pass Denied");
body =
notification.data?.message ||
(isAccepted
? "Your day pass has been approved"
: "Your day pass has been denied");
break;
case "client_invitations":
if (notification.data?.userId || notification.data?.invitorId) {
const isAccept = notification.data?.status === "ACCEPTED";
title =
notification.data?.title ||
(isAccept ? "Invitation Accepted" : "Invitation Rejected");
body =
notification.data?.message ||
(isAccept
? `The invitation for ${notification.data?.subscriptionName} you shared with ${notification.data?.name} has been accepted`
: `The invitation for ${notification.data?.subscriptionName} you shared with ${notification.data?.name} has been rejected`);
} else if (notification.data?.phoneNumber) {
const invitationStatus = getInvitationStatus(notification.data?.status);
title =
notification.data?.title || getInvitationTitle(invitationStatus);
body =
notification.data?.message ||
getInvitationBody(invitationStatus, notification.data?.name);
fcmData.status = invitationStatus;
}
break;
default:
logger.info(
`Using default handling for notification type: ${notification.type}`
);
title =
notification.data?.title ||
(notification.type
? `${notification.type.replace("_", " ").toUpperCase()}`
: "Notification");
break;
}
const notificationMessage: admin.messaging.TokenMessage = {
notification: { title, body },
data: fcmData,
android: {
priority: "high",
notification: {
channelId: "notifications_channel",
priority: "high",
defaultSound: true,
defaultVibrateTimings: true,
icon: "@mipmap/ic_launcher",
clickAction: "FLUTTER_NOTIFICATION_CLICK",
},
},
apns: {
payload: {
aps: {
sound: "default",
badge: 1,
}, },
}, apns: {
}, payload: {
token: fcmToken, aps: {
}; sound: 'default',
badge: 1,
logger.info(`Prepared notification: ${title} - ${body}`); },
return notificationMessage; },
},
token: fcmToken,
};
return notificationMessage;
} }
function getInvitationStatus(status?: string): string { function getInvitationStatus(status?: string): string {
if (status === "ACCEPTED") return "accepted"; if (status === 'ACCEPTED') return 'accepted';
if (status === "REJECTED") return "rejected"; if (status === 'REJECTED') return 'rejected';
if (status === "PENDING") return "pending"; if (status === 'PENDING') return 'pending';
return "unknown"; return 'unknown';
} }
function getInvitationTitle(status: string): string { function getInvitationTitle(status: string): string {
switch (status) { switch (status) {
case "accepted": case 'accepted': return 'Invitation Accepted';
return "Invitation Accepted"; case 'rejected': return 'Invitation Rejected';
case "rejected": case 'pending': return 'New Invitation';
return "Invitation Rejected"; default: return 'Invitation Update';
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": case 'accepted': return `You have accepted the invitation from ${name}`;
return `You have accepted the invitation from ${name}`; case 'rejected': return `You have rejected the invitation from ${name}`;
case "rejected": case 'pending': return `You have a new invitation pending from ${name}`;
return `You have rejected the invitation from ${name}`; default: return 'There is an update to your invitation';
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> {
try { await app.firestore().collection('notifications').doc(notificationId).update({
await app
.firestore()
.collection("notifications")
.doc(notificationId)
.update({
notificationSent: true, notificationSent: true,
sentAt: admin.firestore.FieldValue.serverTimestamp(), sentAt: app.firestore.FieldValue.serverTimestamp()
}); });
logger.info(`Notification ${notificationId} marked as sent`);
} catch (error) {
logger.error(`Error marking notification as sent: ${error}`);
}
} }
async function updateNotificationWithError( async function updateNotificationWithError(notificationId: string, error: string): Promise<void> {
notificationId: string, await app.firestore().collection('notifications').doc(notificationId).update({
error: string
): Promise<void> {
try {
await app
.firestore()
.collection("notifications")
.doc(notificationId)
.update({
notificationError: error, notificationError: error,
notificationSent: false, updatedAt: app.firestore.FieldValue.serverTimestamp()
updatedAt: admin.firestore.FieldValue.serverTimestamp(), });
});
logger.info(`Notification ${notificationId} marked with error: ${error}`);
} catch (updateError) {
logger.error(`Error updating notification with error: ${updateError}`);
}
} }

242
package-lock.json generated Normal file
View File

@ -0,0 +1,242 @@
{
"name": "fitlien-services",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@types/busboy": "^1.5.4",
"@types/nodemailer": "^6.4.17",
"@types/pdfkit": "^0.13.9",
"busboy": "^1.6.0",
"date-fns": "^4.1.0",
"nodemailer": "^7.0.3",
"pdfkit": "^0.17.1"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@types/busboy": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz",
"integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "22.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/nodemailer": {
"version": "6.4.17",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
"integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/pdfkit": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.13.9.tgz",
"integrity": "sha512-RDG8Yb1zT7I01FfpwK7nMSA433XWpblMqSCtA5vJlSyavWZb303HUYPCel6JTiDDFqwGLvtAnYbH8N/e0Cb89g==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/brotli": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
"dependencies": {
"base64-js": "^1.1.2"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/dfa": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fontkit": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
"dependencies": {
"@swc/helpers": "^0.5.12",
"brotli": "^1.3.2",
"clone": "^2.1.2",
"dfa": "^1.2.0",
"fast-deep-equal": "^3.1.3",
"restructure": "^3.0.0",
"tiny-inflate": "^1.0.3",
"unicode-properties": "^1.4.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/jpeg-exif": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
"integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ=="
},
"node_modules/linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"dependencies": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
}
},
"node_modules/linebreak/node_modules/base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/nodemailer": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz",
"integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
},
"node_modules/pdfkit": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.1.tgz",
"integrity": "sha512-Kkf1I9no14O/uo593DYph5u3QwiMfby7JsBSErN1WqeyTgCBNJE3K4pXBn3TgkdKUIVu+buSl4bYUNC+8Up4xg==",
"dependencies": {
"crypto-js": "^4.2.0",
"fontkit": "^2.0.4",
"jpeg-exif": "^1.1.4",
"linebreak": "^1.1.0",
"png-js": "^1.0.0"
}
},
"node_modules/png-js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
},
"node_modules/restructure": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
},
"node_modules/unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
"dependencies": {
"base64-js": "^1.3.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
}
}
}

7
package.json Normal file
View File

@ -0,0 +1,7 @@
{
"dependencies": {
"@types/busboy": "^1.5.4",
"busboy": "^1.6.0",
"date-fns": "^4.1.0"
}
}