notification-bug-fix (#84)
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m10s
All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m10s
Reviewed-on: #84 Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net> Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
This commit is contained in:
parent
4cf5692386
commit
b66f1603cc
@ -166,6 +166,20 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "notifications",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "recipientId",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"order": "DESCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "workout_logs",
|
||||
"queryScope": "COLLECTION",
|
||||
|
||||
@ -33,6 +33,15 @@ interface PaymentData {
|
||||
discount?: number;
|
||||
}
|
||||
|
||||
interface PersonalTrainerAssign {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
trainerId?: string;
|
||||
clientId: string;
|
||||
membershipId: string;
|
||||
gymId: string;
|
||||
}
|
||||
|
||||
export const checkExpiredMemberships = onSchedule(
|
||||
{
|
||||
schedule: "*/5 * * * *",
|
||||
@ -63,10 +72,18 @@ export const checkExpiredMemberships = onSchedule(
|
||||
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;
|
||||
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(
|
||||
`Completed processing. Expired - Success: ${expiredSuccessful}, Failed: ${expiredFailed}. Expiring - Success: ${expiringSuccessful}, Failed: ${expiringFailed}`
|
||||
@ -139,7 +156,10 @@ async function findMembershipsExpiringIn10Days(): Promise<
|
||||
const batchResults = await Promise.allSettled(
|
||||
batch.map(async (doc) => {
|
||||
const data = doc.data() as MembershipData;
|
||||
const isExpiringIn10Days = await checkIfMembershipExpiringIn10Days(doc.id, data);
|
||||
const isExpiringIn10Days = await checkIfMembershipExpiringIn10Days(
|
||||
doc.id,
|
||||
data
|
||||
);
|
||||
if (isExpiringIn10Days) {
|
||||
return { id: doc.id, data };
|
||||
}
|
||||
@ -241,11 +261,11 @@ async function checkIfMembershipExpiringIn10Days(
|
||||
startDate,
|
||||
data.subscription.frequency
|
||||
);
|
||||
|
||||
|
||||
const now = new Date();
|
||||
const tenDaysFromNow = new Date();
|
||||
tenDaysFromNow.setDate(now.getDate() + 10);
|
||||
|
||||
|
||||
const isExpiringIn10Days = expiryDate > now && expiryDate <= tenDaysFromNow;
|
||||
|
||||
if (isExpiringIn10Days) {
|
||||
@ -356,6 +376,66 @@ function calculateRenewalDateFromPayment(
|
||||
return renewalDate;
|
||||
}
|
||||
|
||||
async function getTrainerAssignmentsForMembership(
|
||||
membershipId: string
|
||||
): Promise<PersonalTrainerAssign[]> {
|
||||
try {
|
||||
const querySnapshot = await app
|
||||
.firestore()
|
||||
.collection("personal_trainer_assignments")
|
||||
.where("membershipId", "==", membershipId)
|
||||
.get();
|
||||
|
||||
return querySnapshot.docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
})) as PersonalTrainerAssign[];
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error getting trainer assignments for membership ${membershipId}:`,
|
||||
error
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getTrainerName(trainerId: string): Promise<string> {
|
||||
try {
|
||||
const doc = await app
|
||||
.firestore()
|
||||
.collection("trainer_profiles")
|
||||
.doc(trainerId)
|
||||
.get();
|
||||
|
||||
if (!doc.exists) {
|
||||
const userDoc = await app
|
||||
.firestore()
|
||||
.collection("users")
|
||||
.doc(trainerId)
|
||||
.get();
|
||||
|
||||
if (userDoc.exists) {
|
||||
const userData = userDoc.data();
|
||||
return userData?.name || userData?.displayName || "Unknown Trainer";
|
||||
}
|
||||
return "Unknown Trainer";
|
||||
}
|
||||
|
||||
const data = doc.data();
|
||||
const fields = data?.fields;
|
||||
if (fields) {
|
||||
const firstName = fields["first-name"] || "";
|
||||
const lastName = fields["last-name"] || "";
|
||||
return `${firstName} ${lastName}`.trim() || "Unknown Trainer";
|
||||
}
|
||||
|
||||
return data?.name || data?.displayName || "Unknown Trainer";
|
||||
} catch (error) {
|
||||
logger.error(`Error getting trainer name for ${trainerId}:`, error);
|
||||
return "Unknown Trainer";
|
||||
}
|
||||
}
|
||||
|
||||
async function processExpiredMembership(
|
||||
membershipId: string,
|
||||
membershipData: MembershipData
|
||||
@ -367,7 +447,10 @@ async function processExpiredMembership(
|
||||
});
|
||||
|
||||
logger.info(`Marked membership ${membershipId} as EXPIRED`);
|
||||
|
||||
await sendPlanExpiredNotification(membershipId, membershipData);
|
||||
|
||||
await sendTrainerNotifications(membershipId, membershipData, "expired");
|
||||
} catch (error) {
|
||||
logger.error(`Error processing membership ${membershipId}:`, error);
|
||||
}
|
||||
@ -379,9 +462,130 @@ async function processExpiringMembership(
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.info(`Processing expiring membership ${membershipId}`);
|
||||
|
||||
await sendPlanExpiringNotification(membershipId, membershipData);
|
||||
|
||||
await sendTrainerNotifications(membershipId, membershipData, "expiring");
|
||||
} catch (error) {
|
||||
logger.error(`Error processing expiring membership ${membershipId}:`, error);
|
||||
logger.error(
|
||||
`Error processing expiring membership ${membershipId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendTrainerNotifications(
|
||||
membershipId: string,
|
||||
membershipData: MembershipData,
|
||||
notificationType: "expired" | "expiring"
|
||||
): Promise<void> {
|
||||
try {
|
||||
const trainerAssignments = await getTrainerAssignmentsForMembership(membershipId);
|
||||
|
||||
if (trainerAssignments.length === 0) {
|
||||
logger.info(`No trainer assignments found for membership ${membershipId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const clientName = await getClientName(membershipId, membershipData.userId);
|
||||
const gymName = await getGymName(membershipData.gymId);
|
||||
|
||||
let expiryDate: Date | undefined;
|
||||
let formattedDate = "Unknown Date";
|
||||
let daysUntilExpiry = 0;
|
||||
|
||||
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",
|
||||
});
|
||||
|
||||
if (notificationType === "expiring") {
|
||||
const now = new Date();
|
||||
const timeDiff = expiryDate.getTime() - now.getTime();
|
||||
daysUntilExpiry = Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||
}
|
||||
}
|
||||
|
||||
for (const assignment of trainerAssignments) {
|
||||
if (!assignment.trainerId) continue;
|
||||
|
||||
try {
|
||||
const trainerName = await getTrainerName(assignment.trainerId);
|
||||
|
||||
const notifType = notificationType === "expired" ? "trainer_client_plan_expired" : "trainer_client_plan_expiring";
|
||||
const existing = await app
|
||||
.firestore()
|
||||
.collection("notifications")
|
||||
.where("type", "==", notifType)
|
||||
.where("recipientId", "==", assignment.trainerId)
|
||||
.where("data.membershipId", "==", membershipId)
|
||||
.where(
|
||||
"data.expiryDate",
|
||||
"==",
|
||||
expiryDate
|
||||
? admin.firestore.Timestamp.fromDate(expiryDate)
|
||||
: admin.firestore.Timestamp.fromDate(new Date())
|
||||
)
|
||||
.limit(1)
|
||||
.get();
|
||||
|
||||
if (!existing.empty) {
|
||||
logger.info(
|
||||
`${notificationType} notification already sent to trainer ${assignment.trainerId} for membership ${membershipId}, skipping...`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const notificationData: any = {
|
||||
senderId: "system",
|
||||
recipientId: assignment.trainerId,
|
||||
type: notifType,
|
||||
notificationSent: false,
|
||||
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||
read: false,
|
||||
readBy: [],
|
||||
data: {
|
||||
planName: membershipData.subscription?.name || "Unknown Plan",
|
||||
clientName,
|
||||
membershipId,
|
||||
gymName,
|
||||
assignmentId: assignment.id,
|
||||
formattedExpiryDate: formattedDate,
|
||||
expiryDate: expiryDate
|
||||
? admin.firestore.Timestamp.fromDate(expiryDate)
|
||||
: admin.firestore.Timestamp.fromDate(new Date()),
|
||||
},
|
||||
};
|
||||
|
||||
if (notificationType === "expiring") {
|
||||
notificationData.data.daysUntilExpiry = daysUntilExpiry;
|
||||
}
|
||||
|
||||
await app.firestore().collection("notifications").add(notificationData);
|
||||
|
||||
logger.info(
|
||||
`${notificationType} notification sent to trainer ${assignment.trainerId} (${trainerName}) for client ${clientName}'s membership ${membershipId}`
|
||||
);
|
||||
} catch (trainerError) {
|
||||
logger.error(
|
||||
`Error sending notification to trainer ${assignment.trainerId} for membership ${membershipId}:`,
|
||||
trainerError
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error sending trainer notifications for membership ${membershipId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -394,19 +598,6 @@ async function sendPlanExpiredNotification(
|
||||
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";
|
||||
|
||||
@ -422,7 +613,26 @@ async function sendPlanExpiredNotification(
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const existing = await app
|
||||
.firestore()
|
||||
.collection("notifications")
|
||||
.where("type", "==", "plan_expired")
|
||||
.where("data.membershipId", "==", membershipId)
|
||||
.where(
|
||||
"data.expiryDate",
|
||||
"==",
|
||||
expiryDate
|
||||
? admin.firestore.Timestamp.fromDate(expiryDate)
|
||||
: admin.firestore.Timestamp.fromDate(new Date())
|
||||
)
|
||||
.limit(1)
|
||||
.get();
|
||||
|
||||
if (!existing.empty) {
|
||||
logger.info(`Notification already sent for ${membershipId}, skipping...`);
|
||||
return;
|
||||
}
|
||||
|
||||
await app
|
||||
@ -435,13 +645,13 @@ async function sendPlanExpiredNotification(
|
||||
notificationSent: false,
|
||||
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||
read: false,
|
||||
readBy: [],
|
||||
readBy: [],
|
||||
data: {
|
||||
planName: membershipData.subscription?.name || "Unknown Plan",
|
||||
clientName,
|
||||
membershipId,
|
||||
gymName,
|
||||
ownerId: gymOwnerId,
|
||||
ownerId: gymOwnerId,
|
||||
formattedExpiryDate: formattedDate,
|
||||
expiryDate: expiryDate
|
||||
? admin.firestore.Timestamp.fromDate(expiryDate)
|
||||
@ -466,19 +676,6 @@ async function sendPlanExpiringNotification(
|
||||
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";
|
||||
let daysUntilExpiry = 10;
|
||||
@ -495,12 +692,34 @@ async function sendPlanExpiringNotification(
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
|
||||
const now = new Date();
|
||||
const timeDiff = expiryDate.getTime() - now.getTime();
|
||||
daysUntilExpiry = Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||
}
|
||||
|
||||
const existing = await app
|
||||
.firestore()
|
||||
.collection("notifications")
|
||||
.where("type", "==", "plan_expiring_soon")
|
||||
.where("data.membershipId", "==", membershipId)
|
||||
.where(
|
||||
"data.expiryDate",
|
||||
"==",
|
||||
expiryDate
|
||||
? admin.firestore.Timestamp.fromDate(expiryDate)
|
||||
: admin.firestore.Timestamp.fromDate(new Date())
|
||||
)
|
||||
.limit(1)
|
||||
.get();
|
||||
|
||||
if (!existing.empty) {
|
||||
logger.info(
|
||||
`Expiring notification already sent for ${membershipId}, skipping...`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await app
|
||||
.firestore()
|
||||
.collection("notifications")
|
||||
@ -511,13 +730,13 @@ async function sendPlanExpiringNotification(
|
||||
notificationSent: false,
|
||||
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||
read: false,
|
||||
readBy: [],
|
||||
readBy: [],
|
||||
data: {
|
||||
planName: membershipData.subscription?.name || "Unknown Plan",
|
||||
clientName,
|
||||
membershipId,
|
||||
gymName,
|
||||
ownerId: gymOwnerId,
|
||||
ownerId: gymOwnerId,
|
||||
formattedExpiryDate: formattedDate,
|
||||
expiryDate: expiryDate
|
||||
? admin.firestore.Timestamp.fromDate(expiryDate)
|
||||
@ -530,7 +749,10 @@ async function sendPlanExpiringNotification(
|
||||
`Expiring notification sent for membership ${membershipId} (expires on ${formattedDate}, ${daysUntilExpiry} days remaining)`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`Error sending expiring notification for ${membershipId}:`, error);
|
||||
logger.error(
|
||||
`Error sending expiring notification for ${membershipId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -240,14 +240,28 @@ function prepareNotificationMessage(
|
||||
title = notification.data?.title || "Plan Expired";
|
||||
body =
|
||||
notification.data?.message ||
|
||||
`${notification.data?.clientName}'s membership subscription for ${notification.data?.planName} has expired.`;
|
||||
`${notification.data?.clientName}'s membership for ${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 membership subscription for ${notification.data?.planName} will expire on ${notification.data?.formattedExpiryDate}.`;
|
||||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} will expire on ${notification.data?.formattedExpiryDate}.`;
|
||||
break;
|
||||
|
||||
case "trainer_client_plan_expired":
|
||||
title = notification.data?.title || "Client Plan Expired";
|
||||
body =
|
||||
notification.data?.message ||
|
||||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} has expired.`;
|
||||
break;
|
||||
|
||||
case "trainer_client_plan_expiring":
|
||||
title = notification.data?.title || "Client Plan Expiring Soon";
|
||||
body =
|
||||
notification.data?.message ||
|
||||
`${notification.data?.clientName}'s membership for ${notification.data?.planName} will expire on ${notification.data?.formattedExpiryDate}.`;
|
||||
break;
|
||||
|
||||
case "schedule_update":
|
||||
|
||||
Loading…
Reference in New Issue
Block a user