Changes Updated (#82)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m28s
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 3m28s
Reviewed-on: #82 Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net> Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
This commit is contained in:
parent
fb23661080
commit
237dd8a263
@ -44,25 +44,32 @@ export const checkExpiredMemberships = onSchedule(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const expiredMemberships = await findExpiredMemberships();
|
const expiredMemberships = await findExpiredMemberships();
|
||||||
|
const expiringMemberships = await findMembershipsExpiringIn2Days();
|
||||||
|
|
||||||
if (expiredMemberships.length === 0) {
|
if (expiredMemberships.length === 0 && expiringMemberships.length === 0) {
|
||||||
logger.info("No expired memberships found.");
|
logger.info("No expired or expiring memberships found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Found ${expiredMemberships.length} expired memberships to process.`
|
`Found ${expiredMemberships.length} expired memberships and ${expiringMemberships.length} memberships expiring in 2 days to process.`
|
||||||
);
|
);
|
||||||
|
|
||||||
const results = await Promise.allSettled(
|
const expiredResults = await Promise.allSettled(
|
||||||
expiredMemberships.map((m) => processExpiredMembership(m.id, m.data))
|
expiredMemberships.map((m) => processExpiredMembership(m.id, m.data))
|
||||||
);
|
);
|
||||||
|
|
||||||
const successful = results.filter((r) => r.status === "fulfilled").length;
|
const expiringResults = await Promise.allSettled(
|
||||||
const failed = results.filter((r) => r.status === "rejected").length;
|
expiringMemberships.map((m) => processExpiringMembership(m.id, m.data))
|
||||||
|
);
|
||||||
|
|
||||||
|
const expiredSuccessful = expiredResults.filter((r) => r.status === "fulfilled").length;
|
||||||
|
const expiredFailed = expiredResults.filter((r) => r.status === "rejected").length;
|
||||||
|
const expiringSuccessful = expiringResults.filter((r) => r.status === "fulfilled").length;
|
||||||
|
const expiringFailed = expiringResults.filter((r) => r.status === "rejected").length;
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Completed processing. Success: ${successful}, Failed: ${failed}`
|
`Completed processing. Expired - Success: ${expiredSuccessful}, Failed: ${expiredFailed}. Expiring - Success: ${expiringSuccessful}, Failed: ${expiringFailed}`
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error in scheduled membership expiry check:", error);
|
logger.error("Error in scheduled membership expiry check:", error);
|
||||||
@ -112,6 +119,48 @@ async function findExpiredMemberships(): Promise<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findMembershipsExpiringIn2Days(): Promise<
|
||||||
|
Array<{ id: string; data: MembershipData }>
|
||||||
|
> {
|
||||||
|
try {
|
||||||
|
const snapshot = await app
|
||||||
|
.firestore()
|
||||||
|
.collection("memberships")
|
||||||
|
.where("status", "==", "ACTIVE")
|
||||||
|
.get();
|
||||||
|
|
||||||
|
const expiring: 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 isExpiringIn2Days = await checkIfMembershipExpiringIn2Days(doc.id, data);
|
||||||
|
if (isExpiringIn2Days) {
|
||||||
|
return { id: doc.id, data };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
batchResults.forEach((result) => {
|
||||||
|
if (result.status === "fulfilled" && result.value) {
|
||||||
|
expiring.push(result.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return expiring;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error finding memberships expiring in 2 days:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function checkIfMembershipExpired(
|
async function checkIfMembershipExpired(
|
||||||
membershipId: string,
|
membershipId: string,
|
||||||
data: MembershipData
|
data: MembershipData
|
||||||
@ -165,6 +214,56 @@ async function checkIfMembershipExpired(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkIfMembershipExpiringIn2Days(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
const startDate = latestPayment.dateTimestamp;
|
||||||
|
|
||||||
|
const expiryDate = calculateExpiryDate(
|
||||||
|
startDate,
|
||||||
|
data.subscription.frequency
|
||||||
|
);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const twoDaysFromNow = new Date();
|
||||||
|
twoDaysFromNow.setDate(now.getDate() + 2);
|
||||||
|
|
||||||
|
const isExpiringIn2Days = expiryDate > now && expiryDate <= twoDaysFromNow;
|
||||||
|
|
||||||
|
if (isExpiringIn2Days) {
|
||||||
|
logger.info(
|
||||||
|
`Membership ${membershipId} will expire on ${expiryDate.toISOString()} (within 2 days)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isExpiringIn2Days;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Error checking 2-day expiry for membership ${membershipId}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getPaymentsForMembership(
|
async function getPaymentsForMembership(
|
||||||
membershipId: string
|
membershipId: string
|
||||||
): Promise<PaymentData[]> {
|
): Promise<PaymentData[]> {
|
||||||
@ -274,6 +373,17 @@ async function processExpiredMembership(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function processExpiringMembership(
|
||||||
|
membershipId: string,
|
||||||
|
membershipData: MembershipData
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info(`Processing expiring membership ${membershipId}`);
|
||||||
|
await sendPlanExpiringNotification(membershipId, membershipData);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error processing expiring membership ${membershipId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function sendPlanExpiredNotification(
|
async function sendPlanExpiredNotification(
|
||||||
membershipId: string,
|
membershipId: string,
|
||||||
@ -347,6 +457,78 @@ async function sendPlanExpiredNotification(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendPlanExpiringNotification(
|
||||||
|
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_expiring_soon")
|
||||||
|
.where("data.membershipId", "==", membershipId)
|
||||||
|
.limit(1)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (!existing.empty) {
|
||||||
|
logger.info(`Expiring 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_expiring_soon",
|
||||||
|
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()),
|
||||||
|
daysUntilExpiry: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Expiring notification sent for membership ${membershipId} (expires on ${formattedDate})`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error sending expiring notification for ${membershipId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getClientName(
|
async function getClientName(
|
||||||
membershipId: string,
|
membershipId: string,
|
||||||
clientId: string
|
clientId: string
|
||||||
@ -390,4 +572,4 @@ async function getGymName(gymId: string): Promise<string> {
|
|||||||
logger.error(`Error getting gym name for gym ${gymId}:`, error);
|
logger.error(`Error getting gym name for gym ${gymId}:`, error);
|
||||||
return "Unknown Gym";
|
return "Unknown Gym";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -240,7 +240,14 @@ function prepareNotificationMessage(
|
|||||||
title = notification.data?.title || "Plan Expired";
|
title = notification.data?.title || "Plan Expired";
|
||||||
body =
|
body =
|
||||||
notification.data?.message ||
|
notification.data?.message ||
|
||||||
`${notification.data?.clientName}/s subscription for plan ${notification.data?.planName} has expired.`;
|
`${notification.data?.clientName}'s subscription for plan ${notification.data?.planName} has expired.`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "plan_expiring_soon":
|
||||||
|
title = notification.data?.title || "Plan Expiring Soon";
|
||||||
|
body =
|
||||||
|
notification.data?.message ||
|
||||||
|
`${notification.data?.clientName}'s subscription for plan ${notification.data?.planName} will expire on ${notification.data?.formattedExpiryDate}.`;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "schedule_update":
|
case "schedule_update":
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user