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 { 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 { 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 { 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; } 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 { 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"; } }