All checks were successful
Deploy FitLien services to Dev / Deploy to Dev (push) Successful in 4m30s
Co-authored-by: Dhansh A S <dhanshas@cosq.net> Reviewed-on: #114 Reviewed-by: Dhansh A S <dhanshas@cosq.net> Co-authored-by: Sharon Dcruz <sharondcruz@cosq.net> Co-committed-by: Sharon Dcruz <sharondcruz@cosq.net>
1071 lines
30 KiB
TypeScript
1071 lines
30 KiB
TypeScript
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();
|
|
const kTrainerRole = "Trainer";
|
|
|
|
interface MembershipData {
|
|
id?: string;
|
|
userId: string;
|
|
gymId: string;
|
|
status: string;
|
|
expirationDate?: admin.firestore.Timestamp;
|
|
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;
|
|
}
|
|
|
|
interface PersonalTrainerAssign {
|
|
id: string;
|
|
ownerId: string;
|
|
trainerId?: string;
|
|
clientId: string;
|
|
membershipId: string;
|
|
gymId: 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 {
|
|
await updateDaysUntilExpiryForAllMemberships();
|
|
const expiredMemberships = await findExpiredMemberships();
|
|
const expiringMemberships = await findMembershipsExpiringIn10Days();
|
|
|
|
const expiredMembershipsWithoutExpiryDate =
|
|
await findExpiredMembershipsWithoutExpiryDate();
|
|
|
|
if (
|
|
expiredMemberships.length === 0 &&
|
|
expiringMemberships.length === 0 &&
|
|
expiredMembershipsWithoutExpiryDate.length === 0
|
|
) {
|
|
logger.info(
|
|
"No expired, expiring, or unprocessed expired memberships found."
|
|
);
|
|
return;
|
|
}
|
|
|
|
logger.info(
|
|
`Found ${expiredMemberships.length} expired memberships, ${expiringMemberships.length} memberships expiring in 10 days, and ${expiredMembershipsWithoutExpiryDate.length} expired memberships without expiry dates to process.`
|
|
);
|
|
|
|
const expiredResults = await Promise.allSettled(
|
|
expiredMemberships.map((m) => processExpiredMembership(m.id, m.data))
|
|
);
|
|
|
|
const expiringResults = await Promise.allSettled(
|
|
expiringMemberships.map((m) => processExpiringMembership(m.id, m.data))
|
|
);
|
|
|
|
const updateResults = await Promise.allSettled(
|
|
expiredMembershipsWithoutExpiryDate.map((m) =>
|
|
updateExpiryDateForExpiredMembership(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 updateSuccessful = updateResults.filter(
|
|
(r) => r.status === "fulfilled"
|
|
).length;
|
|
const updateFailed = updateResults.filter(
|
|
(r) => r.status === "rejected"
|
|
).length;
|
|
|
|
logger.info(
|
|
`Completed processing. Expired - Success: ${expiredSuccessful}, Failed: ${expiredFailed}. Expiring - Success: ${expiringSuccessful}, Failed: ${expiringFailed}. Updates - Success: ${updateSuccessful}, Failed: ${updateFailed}`
|
|
);
|
|
} catch (error) {
|
|
logger.error("Error in scheduled membership expiry check:", error);
|
|
}
|
|
}
|
|
);
|
|
|
|
async function findExpiredMembershipsWithoutExpiryDate(): Promise<
|
|
Array<{ id: string; data: MembershipData }>
|
|
> {
|
|
try {
|
|
const snapshot = await app
|
|
.firestore()
|
|
.collection("memberships")
|
|
.where("status", "==", "EXPIRED")
|
|
.get();
|
|
|
|
const membershipsWithoutExpiryDate: Array<{
|
|
id: string;
|
|
data: MembershipData;
|
|
}> = [];
|
|
|
|
snapshot.docs.forEach((doc) => {
|
|
const data = doc.data() as MembershipData;
|
|
if (!data.expirationDate) {
|
|
membershipsWithoutExpiryDate.push({ id: doc.id, data });
|
|
}
|
|
});
|
|
|
|
return membershipsWithoutExpiryDate;
|
|
} catch (error) {
|
|
logger.error(
|
|
"Error finding expired memberships without expiry date:",
|
|
error
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
async function updateExpiryDateForExpiredMembership(
|
|
membershipId: string,
|
|
membershipData: MembershipData
|
|
): Promise<void> {
|
|
try {
|
|
if (
|
|
!membershipData.subscription ||
|
|
!membershipData.subscription.frequency
|
|
) {
|
|
logger.warn(`Skipping membership ${membershipId} - no subscription data`);
|
|
return;
|
|
}
|
|
|
|
const payments = await getPaymentsForMembership(membershipId);
|
|
if (payments.length === 0) {
|
|
logger.warn(`No payments found for membership ${membershipId}`);
|
|
return;
|
|
}
|
|
|
|
const latestPayment = payments[0];
|
|
const expiryDate = calculateExpiryDate(
|
|
latestPayment.dateTimestamp,
|
|
membershipData.subscription.frequency
|
|
);
|
|
|
|
await app
|
|
.firestore()
|
|
.collection("memberships")
|
|
.doc(membershipId)
|
|
.update({
|
|
expirationDate: admin.firestore.Timestamp.fromDate(expiryDate),
|
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
});
|
|
|
|
logger.info(
|
|
`Updated expiry date for expired membership ${membershipId}: ${expiryDate.toISOString()}`
|
|
);
|
|
} catch (error) {
|
|
logger.error(
|
|
`Error updating expiry date for membership ${membershipId}:`,
|
|
error
|
|
);
|
|
throw 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 findMembershipsExpiringIn10Days(): 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 isExpiringIn10Days = await checkIfMembershipExpiringIn10Days(
|
|
doc.id,
|
|
data
|
|
);
|
|
if (isExpiringIn10Days) {
|
|
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 10 days:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function checkIfMembershipExpired(
|
|
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;
|
|
}
|
|
|
|
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 checkIfMembershipExpiringIn10Days(
|
|
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 tenDaysFromNow = new Date();
|
|
tenDaysFromNow.setDate(now.getDate() + 10);
|
|
|
|
const isExpiringIn10Days = expiryDate > now && expiryDate <= tenDaysFromNow;
|
|
|
|
if (isExpiringIn10Days) {
|
|
logger.info(
|
|
`Membership ${membershipId} will expire on ${expiryDate.toISOString()} (within 10 days)`
|
|
);
|
|
}
|
|
|
|
return isExpiringIn10Days;
|
|
} catch (error) {
|
|
logger.error(
|
|
`Error checking 10-day expiry for membership ${membershipId}:`,
|
|
error
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function getPaymentsForMembership(
|
|
membershipId: string
|
|
): Promise<PaymentData[]> {
|
|
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 updateDaysUntilExpiryForAllMemberships(): Promise<void> {
|
|
try {
|
|
logger.info(
|
|
"Starting to update daysUntilExpiry for all active memberships..."
|
|
);
|
|
|
|
const snapshot = await app
|
|
.firestore()
|
|
.collection("memberships")
|
|
.where("status", "==", "ACTIVE")
|
|
.get();
|
|
|
|
const batchSize = 10;
|
|
const docs = snapshot.docs;
|
|
let updatedCount = 0;
|
|
|
|
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 daysUntilExpiry = await calculateDaysUntilExpiry(doc.id, data);
|
|
|
|
if (daysUntilExpiry !== null) {
|
|
const updateData: any = {
|
|
daysUntilExpiry: daysUntilExpiry,
|
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
};
|
|
|
|
await app
|
|
.firestore()
|
|
.collection("memberships")
|
|
.doc(doc.id)
|
|
.update(updateData);
|
|
|
|
logger.info(
|
|
`Updated membership ${doc.id} with daysUntilExpiry: ${daysUntilExpiry}`
|
|
);
|
|
return doc.id;
|
|
}
|
|
return null;
|
|
})
|
|
);
|
|
|
|
batchResults.forEach((result) => {
|
|
if (result.status === "fulfilled" && result.value) {
|
|
updatedCount++;
|
|
}
|
|
});
|
|
}
|
|
|
|
logger.info(`Updated daysUntilExpiry for ${updatedCount} memberships`);
|
|
} catch (error) {
|
|
logger.error("Error updating daysUntilExpiry for memberships:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function calculateDaysUntilExpiry(
|
|
membershipId: string,
|
|
data: MembershipData
|
|
): Promise<number | null> {
|
|
try {
|
|
if (!data.subscription || !data.subscription.frequency) {
|
|
logger.warn(
|
|
`Skipping expiry calculation for membership ${membershipId} with missing subscription data.`
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const payments = await getPaymentsForMembership(membershipId);
|
|
if (payments.length === 0) {
|
|
logger.warn(
|
|
`No payments found for membership ${membershipId}, cannot determine expiry`
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const latestPayment = payments[0];
|
|
const startDate = latestPayment.dateTimestamp;
|
|
|
|
const expiryDate = calculateExpiryDate(
|
|
startDate,
|
|
data.subscription.frequency
|
|
);
|
|
|
|
const now = new Date();
|
|
|
|
const timeDiff = expiryDate.getTime() - now.getTime();
|
|
const daysUntilExpiry = Math.floor(timeDiff / (1000 * 3600 * 24));
|
|
|
|
return Math.max(0, daysUntilExpiry);
|
|
} catch (error) {
|
|
logger.error(
|
|
`Error calculating daysUntilExpiry for membership ${membershipId}:`,
|
|
error
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
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
|
|
): Promise<void> {
|
|
try {
|
|
const payments = await getPaymentsForMembership(membershipId);
|
|
if (payments.length > 0) {
|
|
const latestPayment = payments[0];
|
|
const expiryDate = calculateExpiryDate(
|
|
latestPayment.dateTimestamp,
|
|
membershipData.subscription?.frequency || "monthly"
|
|
);
|
|
|
|
await app
|
|
.firestore()
|
|
.collection("memberships")
|
|
.doc(membershipId)
|
|
.update({
|
|
expirationDate: admin.firestore.Timestamp.fromDate(expiryDate),
|
|
status: "EXPIRED",
|
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
});
|
|
} else {
|
|
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);
|
|
await sendTrainerNotifications(membershipId, membershipData, "expired");
|
|
} catch (error) {
|
|
logger.error(`Error processing membership ${membershipId}:`, error);
|
|
}
|
|
}
|
|
|
|
async function processExpiringMembership(
|
|
membershipId: string,
|
|
membershipData: MembershipData
|
|
): Promise<void> {
|
|
try {
|
|
logger.info(`Processing expiring membership ${membershipId}`);
|
|
|
|
const payments = await getPaymentsForMembership(membershipId);
|
|
if (payments.length > 0) {
|
|
const latestPayment = payments[0];
|
|
const expiryDate = calculateExpiryDate(
|
|
latestPayment.dateTimestamp,
|
|
membershipData.subscription?.frequency || "monthly"
|
|
);
|
|
|
|
await app
|
|
.firestore()
|
|
.collection("memberships")
|
|
.doc(membershipId)
|
|
.update({
|
|
expirationDate: admin.firestore.Timestamp.fromDate(expiryDate),
|
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
});
|
|
}
|
|
|
|
await sendPlanExpiringNotification(membershipId, membershipData);
|
|
await sendTrainerNotifications(membershipId, membershipData, "expiring");
|
|
} catch (error) {
|
|
logger.error(
|
|
`Error processing expiring membership ${membershipId}:`,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
async function getTrainerUserId(trainerId: string): Promise<string> {
|
|
try {
|
|
const trainerDoc = await app
|
|
.firestore()
|
|
.collection("trainer_profiles")
|
|
.doc(trainerId)
|
|
.get();
|
|
|
|
if (!trainerDoc.exists) {
|
|
throw new Error(`Trainer profile not found for ID: ${trainerId}`);
|
|
}
|
|
|
|
const trainerData = trainerDoc.data();
|
|
if (!trainerData?.userId) {
|
|
throw new Error(`userId not found in trainer profile: ${trainerId}`);
|
|
}
|
|
|
|
return trainerData.userId;
|
|
} catch (error) {
|
|
logger.error(`Error getting userId for trainer ${trainerId}:`, error);
|
|
return trainerId;
|
|
}
|
|
}
|
|
|
|
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.floor(timeDiff / (1000 * 3600 * 24));
|
|
}
|
|
}
|
|
|
|
for (const assignment of trainerAssignments) {
|
|
if (!assignment.trainerId) continue;
|
|
|
|
try {
|
|
const trainerName = await getTrainerName(assignment.trainerId);
|
|
const trainerUserId = await getTrainerUserId(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", "==", trainerUserId)
|
|
.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: trainerUserId,
|
|
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,
|
|
role: kTrainerRole,
|
|
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
|
|
);
|
|
}
|
|
}
|
|
|
|
async function sendPlanExpiredNotification(
|
|
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);
|
|
|
|
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",
|
|
});
|
|
}
|
|
|
|
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
|
|
.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 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);
|
|
|
|
let expiryDate: Date | undefined;
|
|
let formattedDate = "Unknown Date";
|
|
let daysUntilExpiry = 10;
|
|
|
|
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",
|
|
});
|
|
|
|
const now = new Date();
|
|
const timeDiff = expiryDate.getTime() - now.getTime();
|
|
daysUntilExpiry = Math.floor(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")
|
|
.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: daysUntilExpiry,
|
|
},
|
|
});
|
|
|
|
logger.info(
|
|
`Expiring notification sent for membership ${membershipId} (expires on ${formattedDate}, ${daysUntilExpiry} days remaining)`
|
|
);
|
|
} catch (error) {
|
|
logger.error(
|
|
`Error sending expiring notification for ${membershipId}:`,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
async function getClientName(
|
|
membershipId: string,
|
|
clientId: string
|
|
): Promise<string> {
|
|
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<string> {
|
|
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<string> {
|
|
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";
|
|
}
|
|
}
|