fitlien-services/functions/src/notifications/membershipStatusNotifications.ts
Sharon Dcruz fb23661080
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m54s
expiry-using-payment (#81)
Reviewed-on: #81
Reviewed-by: Dhansh A S <dhanshas@cosq.net>
Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net>
Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
2025-08-05 13:20:00 +00:00

394 lines
10 KiB
TypeScript

import { onSchedule } from "firebase-functions/v2/scheduler";
import { getLogger, getAdmin } from "../shared/config";
import * as admin from "firebase-admin";
const app = getAdmin();
const logger = getLogger();
interface MembershipData {
id?: string;
userId: string;
gymId: string;
status: string;
subscription?: {
name: string;
frequency: string;
};
}
interface ClientFields {
[key: string]: string | undefined;
"first-name"?: string;
"last-name"?: string;
}
interface PaymentData {
id: string;
date: string;
amount: number;
paymentMethod: string;
referenceNumber: string;
dateTimestamp: Date;
createdAt: Date;
discount?: number;
}
export const checkExpiredMemberships = onSchedule(
{
schedule: "*/5 * * * *",
timeZone: "UTC",
region: "#{SERVICES_RGN}#",
},
async (event) => {
logger.info("Starting scheduled membership expiry check...");
try {
const expiredMemberships = await findExpiredMemberships();
if (expiredMemberships.length === 0) {
logger.info("No expired memberships found.");
return;
}
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 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 }> = [];
const batchSize = 10;
const docs = snapshot.docs;
for (let i = 0; i < docs.length; i += batchSize) {
const batch = docs.slice(i, i + batchSize);
const batchResults = await Promise.allSettled(
batch.map(async (doc) => {
const data = doc.data() as MembershipData;
const isExpired = await checkIfMembershipExpired(doc.id, data);
if (isExpired) {
return { id: doc.id, data };
}
return null;
})
);
batchResults.forEach((result) => {
if (result.status === "fulfilled" && result.value) {
expired.push(result.value);
}
});
}
return expired;
} catch (error) {
logger.error("Error finding expired memberships:", error);
throw error;
}
}
async function checkIfMembershipExpired(
membershipId: string,
data: MembershipData
): Promise<boolean> {
try {
if (!data.subscription || !data.subscription.frequency) {
logger.warn(
`Skipping expiry check for membership ${membershipId} with missing subscription data.`
);
return false;
}
let startDate: Date;
const payments = await getPaymentsForMembership(membershipId);
if (payments.length === 0) {
logger.warn(
`No payments found for membership ${membershipId}, cannot determine expiry`
);
return false;
}
const latestPayment = payments[0];
startDate = latestPayment.dateTimestamp;
logger.info(
`Using latest payment date ${startDate.toISOString()} for membership ${membershipId}`
);
const expiryDate = calculateExpiryDate(
startDate,
data.subscription.frequency
);
const now = new Date();
const isExpired = now > expiryDate;
if (isExpired) {
logger.info(
`Membership ${membershipId} expired on ${expiryDate.toISOString()}`
);
}
return isExpired;
} catch (error) {
logger.error(
`Error checking expiry for membership ${membershipId}:`,
error
);
return false;
}
}
async function getPaymentsForMembership(
membershipId: string
): Promise<PaymentData[]> {
try {
const docSnapshot = await app
.firestore()
.collection("membership_payments")
.doc(membershipId)
.get();
if (!docSnapshot.exists) {
return [];
}
const data = docSnapshot.data();
const paymentsData = data?.payments || [];
const payments: PaymentData[] = paymentsData.map((payment: any) => ({
id: payment.id,
date: payment.date,
amount: payment.amount,
paymentMethod: payment.paymentMethod,
referenceNumber: payment.referenceNumber,
dateTimestamp: payment.dateTimestamp.toDate
? payment.dateTimestamp.toDate()
: new Date(payment.dateTimestamp),
createdAt: payment.createdAt.toDate
? payment.createdAt.toDate()
: new Date(payment.createdAt),
discount: payment.discount,
}));
payments.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
return payments;
} catch (error) {
logger.error(
`Error getting payments for membership ${membershipId}:`,
error
);
return [];
}
}
function calculateExpiryDate(startDate: Date, frequency: string): Date {
const expiry = new Date(startDate);
switch (frequency.toLowerCase()) {
case "monthly":
expiry.setMonth(expiry.getMonth() + 1);
break;
case "quarterly":
expiry.setMonth(expiry.getMonth() + 3);
break;
case "half-yearly":
expiry.setMonth(expiry.getMonth() + 6);
break;
case "yearly":
expiry.setFullYear(expiry.getFullYear() + 1);
break;
default:
expiry.setMonth(expiry.getMonth() + 1);
}
return expiry;
}
function calculateRenewalDateFromPayment(
subscription: any,
paymentDate: Date
): Date {
const renewalDate = new Date(paymentDate);
const frequency = subscription.frequency || "Monthly";
switch (frequency.toLowerCase()) {
case "monthly":
renewalDate.setMonth(renewalDate.getMonth() + 1);
break;
case "quarterly":
renewalDate.setMonth(renewalDate.getMonth() + 3);
break;
case "half-yearly":
renewalDate.setMonth(renewalDate.getMonth() + 6);
break;
case "yearly":
renewalDate.setFullYear(renewalDate.getFullYear() + 1);
break;
default:
renewalDate.setMonth(renewalDate.getMonth() + 1);
}
return renewalDate;
}
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 existing = await app
.firestore()
.collection("notifications")
.where("type", "==", "plan_expired")
.where("data.membershipId", "==", membershipId)
.limit(1)
.get();
if (!existing.empty) {
logger.info(`Notification already sent for ${membershipId}, skipping...`);
return;
}
let expiryDate: Date | undefined;
let formattedDate = "Unknown Date";
const payments = await getPaymentsForMembership(membershipId);
if (payments.length > 0) {
const latestPayment = payments[0];
expiryDate = calculateRenewalDateFromPayment(
membershipData.subscription,
latestPayment.dateTimestamp
);
formattedDate = expiryDate.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
}
await app
.firestore()
.collection("notifications")
.add({
senderId: "system",
recipientId: gymOwnerId,
type: "plan_expired",
notificationSent: false,
timestamp: admin.firestore.FieldValue.serverTimestamp(),
read: false,
readBy: [],
data: {
planName: membershipData.subscription?.name || "Unknown Plan",
clientName,
membershipId,
gymName,
ownerId: gymOwnerId,
formattedExpiryDate: formattedDate,
expiryDate: expiryDate
? admin.firestore.Timestamp.fromDate(expiryDate)
: admin.firestore.Timestamp.fromDate(new Date()),
},
});
logger.info(
`Notification sent for expired plan of membership ${membershipId}`
);
} catch (error) {
logger.error(`Error sending notification for ${membershipId}:`, error);
}
}
async function getClientName(
membershipId: string,
clientId: string
): Promise<string> {
try {
const doc = await app
.firestore()
.collection("client_profiles")
.doc(clientId)
.get();
if (!doc.exists) 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 ${membershipId}:`, error);
return "Unknown Client";
}
}
async function getGymOwnerId(gymId: string): Promise<string> {
try {
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;
}
}
async function getGymName(gymId: string): Promise<string> {
try {
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";
}
}