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; assignedAt: admin.firestore.Timestamp; }; } interface ClientFields { [key: string]: string | undefined; "first-name"?: string; "last-name"?: string; } export const checkExpiredMemberships = onSchedule( { schedule: "0 8,14,20 * * *", 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 }> = []; 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 { if ( !data.subscription || !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.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, 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; } async function processExpiredMembership( membershipId: string, membershipData: MembershipData ): Promise { 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 { 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; } 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 notification for ${membershipId}:`, error); } } async function getClientName( membershipId: string, clientId: string ): Promise { 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 { 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 { 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"; } }