Changes Updated #74

Merged
dhanshas merged 2 commits from expiry-notification into dev 2025-08-01 13:20:37 +00:00
3 changed files with 198 additions and 105 deletions
Showing only changes of commit adec108f8a - Show all commits

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import { onDocumentUpdated } from "firebase-functions/v2/firestore";
import { getLogger } from "../shared/config";
import { getAdmin } from "../shared/config";
import { onSchedule } from "firebase-functions/v2/scheduler";
import { getLogger, getAdmin } from "../shared/config";
import * as admin from "firebase-admin";
const app = getAdmin();
@ -14,143 +13,246 @@ interface MembershipData {
subscription?: {
name: string;
duration: number;
frequency: string;
assignedAt: admin.firestore.Timestamp;
};
createdAt: admin.firestore.FieldValue;
updatedAt: admin.firestore.FieldValue;
}
interface ClientFields {
[key: string]: string | undefined;
'first-name'?: string;
"first-name"?: string;
"last-name"?: string;
}
export const processMembershipStatusChange = onDocumentUpdated(
export const checkExpiredMemberships = onSchedule(
{
schedule: "0 8,14,20 * * *",
timeZone: "UTC",
region: "#{SERVICES_RGN}#",
document: "memberships/{membershipId}",
},
async (event) => {
try {
const membershipId = event.params.membershipId;
const beforeData = event.data?.before?.data() as MembershipData;
const afterData = event.data?.after?.data() as MembershipData;
logger.info("Starting scheduled membership expiry check...");
if (!beforeData || !afterData) {
logger.error(`No data found for membership ${membershipId}`);
try {
const expiredMemberships = await findExpiredMemberships();
if (expiredMemberships.length === 0) {
logger.info("No expired memberships found.");
return;
}
// Check if status changed to EXPIRED
if (beforeData.status !== "EXPIRED" && afterData.status === "EXPIRED") {
logger.info(`Membership ${membershipId} status changed to EXPIRED. Sending notification...`);
await sendPlanExpiredNotification(membershipId, afterData);
}
logger.info(
`Found ${expiredMemberships.length} expired memberships to process.`
);
const results = await Promise.allSettled(
expiredMemberships.map((m) => processExpiredMembership(m.id, m.data))
);
const successful = results.filter((r) => r.status === "fulfilled").length;
const failed = results.filter((r) => r.status === "rejected").length;
logger.info(
`Completed processing. Success: ${successful}, Failed: ${failed}`
);
} catch (error) {
logger.error("Error processing membership status change:", error);
logger.error("Error in scheduled membership expiry check:", error);
}
}
);
async function findExpiredMemberships(): Promise<
Array<{ id: string; data: MembershipData }>
> {
try {
const snapshot = await app
.firestore()
.collection("memberships")
.where("status", "==", "ACTIVE")
.get();
const expired: Array<{ id: string; data: MembershipData }> = [];
snapshot.docs.forEach((doc) => {
const data = doc.data() as MembershipData;
const isExpired = checkIfMembershipExpired(data);
if (isExpired) expired.push({ id: doc.id, data });
});
return expired;
} catch (error) {
logger.error("Error finding expired memberships:", error);
throw error;
}
}
function checkIfMembershipExpired(data: MembershipData): boolean {
try {
// Critical update: Use the assignedAt timestamp from the subscription object
if (
!data.subscription ||
!data.subscription.duration ||
!data.subscription.frequency ||
!data.subscription.assignedAt
) {
logger.warn(
`Skipping expiry check for membership ${data.id} with missing subscription data.`
);
return false;
}
const startDate = (
data.subscription.assignedAt as admin.firestore.Timestamp
).toDate();
const expiryDate = calculateExpiryDate(
startDate,
data.subscription.duration,
data.subscription.frequency
);
const now = new Date();
return now > expiryDate;
} catch (error) {
logger.error(`Error checking expiry for membership ${data.id}:`, error);
return false;
}
}
function calculateExpiryDate(
startDate: Date,
duration: number,
frequency: string
): Date {
const expiry = new Date(startDate);
switch (frequency.toLowerCase()) {
case "monthly":
expiry.setMonth(expiry.getMonth() + duration);
break;
case "quarterly":
expiry.setMonth(expiry.getMonth() + 3 * duration);
break;
case "half-yearly":
expiry.setMonth(expiry.getMonth() + 6 * duration);
break;
case "yearly":
expiry.setFullYear(expiry.getFullYear() + duration);
break;
default:
expiry.setMonth(expiry.getMonth() + duration);
}
return expiry;
}
async function processExpiredMembership(
membershipId: string,
membershipData: MembershipData
): Promise<void> {
try {
await app.firestore().collection("memberships").doc(membershipId).update({
status: "EXPIRED",
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
});
logger.info(`Marked membership ${membershipId} as EXPIRED`);
await sendPlanExpiredNotification(membershipId, membershipData);
} catch (error) {
logger.error(`Error processing membership ${membershipId}:`, error);
}
}
async function sendPlanExpiredNotification(
membershipId: string,
membershipData: MembershipData
): Promise<void> {
try {
const clientName = await getClientName(membershipId, membershipData.userId);
const gymOwnerId = await getGymOwnerId(membershipData.gymId);
const gymName = await getGymName(membershipData.gymId);
const expiryDate = new Date();
const formattedExpiryDate = expiryDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
// Create notification document
const notificationData = {
senderId: "system",
recipientId: gymOwnerId,
type: "plan_expired",
notificationSent: false,
timestamp: admin.firestore.FieldValue.serverTimestamp(),
read: false,
data: {
title: "Plan Expired",
message: `The plan ${membershipData.subscription?.name || 'Unknown Plan'} for client ${clientName} has expired.`,
planName: membershipData.subscription?.name || 'Unknown Plan',
clientName: clientName,
membershipId: membershipId,
gymName: gymName,
formattedExpiryDate: formattedExpiryDate,
expiryDate: admin.firestore.Timestamp.fromDate(expiryDate),
},
};
const notificationRef = await app
const existing = await app
.firestore()
.collection("notifications")
.add(notificationData);
.where("type", "==", "plan_expired")
.where("data.membershipId", "==", membershipId)
.limit(1)
.get();
logger.info(`Plan expired notification created with ID: ${notificationRef.id} for gym owner ${gymOwnerId}, membership ${membershipId}`);
if (!existing.empty) {
logger.info(`Notification already sent for ${membershipId}, skipping...`);
return;
}
const expiryDate = membershipData.subscription?.assignedAt?.toDate();
const formattedDate = expiryDate
? expiryDate.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
: "Unknown Date";
await app
.firestore()
.collection("notifications")
.add({
senderId: "system",
recipientId: gymOwnerId,
type: "plan_expired",
notificationSent: false,
timestamp: admin.firestore.FieldValue.serverTimestamp(),
read: false,
data: {
title: "Plan Expired",
message: `The plan ${
membershipData.subscription?.name || "Unknown Plan"
} for client ${clientName} has expired.`,
planName: membershipData.subscription?.name || "Unknown Plan",
clientName,
membershipId,
gymName,
formattedExpiryDate: formattedDate,
expiryDate:
membershipData.subscription?.assignedAt ||
admin.firestore.Timestamp.fromDate(new Date()),
},
});
logger.info(
`Notification sent for expired plan of membership ${membershipId}`
);
} catch (error) {
logger.error(`Error sending plan expired notification for membership ${membershipId}:`, error);
throw error;
logger.error(`Error sending notification for ${membershipId}:`, error);
}
}
async function getClientName(membershipId: string, clientId: string): Promise<string> {
async function getClientName(
membershipId: string,
clientId: string
): Promise<string> {
try {
const clientProfileDoc = await app
const doc = await app
.firestore()
.collection("client_profiles")
.doc(clientId)
.get();
if (!doc.exists) return "Unknown Client";
if (!clientProfileDoc.exists) {
logger.warn(`Client profile not found for clientId: ${clientId}`);
return "Unknown Client";
}
const clientData = clientProfileDoc.data();
const fields = clientData?.fields as ClientFields;
if (fields) {
const firstName = fields['first-name'] || '';
return firstName || "Unknown Client";
}
return "Unknown Client";
const fields = doc.data()?.fields as ClientFields;
const firstName = fields?.["first-name"] || "";
const lastName = fields?.["last-name"] || "";
return `${firstName} ${lastName}`.trim() || "Unknown Client";
} catch (error) {
logger.error(`Error getting client name for membership ${membershipId}:`, error);
logger.error(`Error getting client name for ${membershipId}:`, error);
return "Unknown Client";
}
}
async function getGymOwnerId(gymId: string): Promise<string> {
try {
const gymDoc = await app
.firestore()
.collection("gyms")
.doc(gymId)
.get();
if (!gymDoc.exists) {
throw new Error(`Gym not found: ${gymId}`);
}
const gymData = gymDoc.data();
const userId = gymData?.userId;
if (!userId) {
throw new Error(`No userId found in gym document: ${gymId}`);
}
return userId;
const doc = await app.firestore().collection("gyms").doc(gymId).get();
const data = doc.data();
if (!data?.userId) throw new Error(`userId not found for gym ${gymId}`);
return data.userId;
} catch (error) {
logger.error(`Error getting gym owner ID for gym ${gymId}:`, error);
throw error;
@ -159,18 +261,9 @@ async function getGymOwnerId(gymId: string): Promise<string> {
async function getGymName(gymId: string): Promise<string> {
try {
const gymDoc = await app
.firestore()
.collection("gyms")
.doc(gymId)
.get();
if (!gymDoc.exists) {
return "Unknown Gym";
}
const gymData = gymDoc.data();
return gymData?.name || gymData?.gymName || "Unknown Gym";
const doc = await app.firestore().collection("gyms").doc(gymId).get();
const data = doc.data();
return data?.name || data?.gymName || "Unknown Gym";
} catch (error) {
logger.error(`Error getting gym name for gym ${gymId}:`, error);
return "Unknown Gym";