From ef166a209ccfd3a4f73b6507be5d57d4d3b7b5a4 Mon Sep 17 00:00:00 2001 From: Sharon Dcruz Date: Tue, 5 Aug 2025 06:16:47 +0000 Subject: [PATCH] expiry-using-payment (#79) Reviewed-on: https://git.cosqnet.com/cosqnet/fitlien-services/pulls/79 Reviewed-by: Dhansh A S Co-authored-by: Sharon Dcruz Co-committed-by: Sharon Dcruz --- .../membershipStatusNotifications.ts | 190 +++++++++++++++--- 1 file changed, 160 insertions(+), 30 deletions(-) diff --git a/functions/src/notifications/membershipStatusNotifications.ts b/functions/src/notifications/membershipStatusNotifications.ts index 797e0fb..ef0c238 100644 --- a/functions/src/notifications/membershipStatusNotifications.ts +++ b/functions/src/notifications/membershipStatusNotifications.ts @@ -13,7 +13,6 @@ interface MembershipData { subscription?: { name: string; frequency: string; - assignedAt: admin.firestore.Timestamp; }; } @@ -23,6 +22,17 @@ interface ClientFields { "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: "0 8,14,20 * * *", @@ -72,11 +82,28 @@ async function findExpiredMemberships(): Promise< 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 }); - }); + 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) { @@ -85,35 +112,103 @@ async function findExpiredMemberships(): Promise< } } -function checkIfMembershipExpired(data: MembershipData): boolean { +async function checkIfMembershipExpired( + membershipId: string, + data: MembershipData +): Promise { try { - if ( - !data.subscription || - !data.subscription.frequency || - !data.subscription.assignedAt - ) { + if (!data.subscription || !data.subscription.frequency) { logger.warn( - `Skipping expiry check for membership ${data.id} with missing subscription data.` + `Skipping expiry check for membership ${membershipId} with missing subscription data.` ); return false; } - const startDate = ( - data.subscription.assignedAt as admin.firestore.Timestamp - ).toDate(); + 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(); - return now > expiryDate; + 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 ${data.id}:`, 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("membershipPayments") + .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()) { @@ -130,11 +225,38 @@ function calculateExpiryDate(startDate: Date, frequency: string): Date { expiry.setFullYear(expiry.getFullYear() + 1); break; default: - expiry.setMonth(expiry.getMonth() + 1); + 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 @@ -174,14 +296,22 @@ async function sendPlanExpiredNotification( return; } - const expiryDate = membershipData.subscription?.assignedAt?.toDate(); - const formattedDate = expiryDate - ? expiryDate.toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - }) - : "Unknown Date"; + 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() @@ -203,9 +333,9 @@ async function sendPlanExpiredNotification( membershipId, gymName, formattedExpiryDate: formattedDate, - expiryDate: - membershipData.subscription?.assignedAt || - admin.firestore.Timestamp.fromDate(new Date()), + expiryDate: expiryDate + ? admin.firestore.Timestamp.fromDate(expiryDate) + : admin.firestore.Timestamp.fromDate(new Date()), }, });