Compare commits
66 Commits
expiry-usi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 61f0d29f37 | |||
| 209c7c65b0 | |||
| ad31bc8e80 | |||
| f0d167a671 | |||
| e762d0fb79 | |||
| 5ca05c6490 | |||
| 7ec65e4ab1 | |||
| b41431b151 | |||
| 218ff1d02e | |||
| 4f71ca273b | |||
| 4d52d1c7f8 | |||
| aee40521d3 | |||
| a450e93e2b | |||
| 36015d2b83 | |||
| 51fa0825ca | |||
| 3e455fc83a | |||
| 0672a19a60 | |||
| 172fa2edae | |||
| d0c00d8172 | |||
| f93931867c | |||
| 70d76bab2e | |||
| 308cb0fab6 | |||
| 165cd74a17 | |||
| b190a371b6 | |||
| 5543ba5e7a | |||
| 5d6824a6f4 | |||
| abf7a04633 | |||
| 835f478665 | |||
| 5f43a86036 | |||
| 1e09f7a676 | |||
| 5e48f695f8 | |||
| e483b7ad46 | |||
| cd59b9890d | |||
| 939567f7c0 | |||
| 7c494154ba | |||
| 8f5956a825 | |||
| 5ff7b8bb84 | |||
| 7259e67833 | |||
| 81c5241e95 | |||
| 209354ec6b | |||
| 39ea6dcafb | |||
| f6b1545cf6 | |||
| 195262a6de | |||
| d8ae223cce | |||
| c2914b16bb | |||
| b66f1603cc | |||
| d3c9e86c7c | |||
| 4cf5692386 | |||
| 8b308fb9a6 | |||
| 237dd8a263 | |||
| fb23661080 | |||
| 9d51393aa5 | |||
| cc5b6d6987 | |||
| ef2cd80226 | |||
| f19e3b012d | |||
| f2e37e88ed | |||
| a0134466ee | |||
| ecbe9d184b | |||
| 7a796243b0 | |||
| 7db9e479ad | |||
| cf6f4625ad | |||
| 6434f6e3fa | |||
| 2147963523 | |||
| 18569d38d3 | |||
| 5bc3d6dfff | |||
| e8ca80df48 |
@ -19,9 +19,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
|
|
||||||
- name: Clean install
|
|
||||||
run: npm clean-install
|
|
||||||
|
|
||||||
- name: Copy .env.example to .env
|
- name: Copy .env.example to .env
|
||||||
run: cp functions/.env.example functions/.env
|
run: cp functions/.env.example functions/.env
|
||||||
|
|
||||||
|
|||||||
@ -19,9 +19,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
|
|
||||||
- name: Clean install
|
|
||||||
run: npm clean-install
|
|
||||||
|
|
||||||
- name: Copy .env.example to .env
|
- name: Copy .env.example to .env
|
||||||
run: cp functions/.env.example functions/.env
|
run: cp functions/.env.example functions/.env
|
||||||
|
|
||||||
|
|||||||
@ -19,9 +19,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
|
|
||||||
- name: Clean install
|
|
||||||
run: npm clean-install
|
|
||||||
|
|
||||||
- name: Copy .env.example to .env
|
- name: Copy .env.example to .env
|
||||||
run: cp functions/.env.example functions/.env
|
run: cp functions/.env.example functions/.env
|
||||||
|
|
||||||
|
|||||||
@ -57,11 +57,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collectionGroup": "memberships",
|
"collectionGroup": "gyms",
|
||||||
"queryScope": "COLLECTION",
|
"queryScope": "COLLECTION",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "gymId",
|
"fieldPath": "userId",
|
||||||
"order": "ASCENDING"
|
"order": "ASCENDING"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -71,33 +71,29 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collectionGroup": "notifications",
|
"collectionGroup": "gyms",
|
||||||
"queryScope": "COLLECTION",
|
"queryScope": "COLLECTION",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "data.clientId",
|
"fieldPath": "isApproved",
|
||||||
"order": "ASCENDING"
|
"order": "ASCENDING"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "timestamp",
|
"fieldPath": "createdAt",
|
||||||
"order": "DESCENDING"
|
"order": "ASCENDING"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collectionGroup": "notifications",
|
"collectionGroup": "memberships",
|
||||||
"queryScope": "COLLECTION",
|
"queryScope": "COLLECTION",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "data.clientId",
|
"fieldPath": "gymId",
|
||||||
"order": "ASCENDING"
|
"order": "ASCENDING"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "type",
|
"fieldPath": "createdAt",
|
||||||
"order": "ASCENDING"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "timestamp",
|
|
||||||
"order": "DESCENDING"
|
"order": "DESCENDING"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -125,39 +121,7 @@
|
|||||||
"queryScope": "COLLECTION",
|
"queryScope": "COLLECTION",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "data.ownerId",
|
"fieldPath": "recipientId",
|
||||||
"order": "ASCENDING"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "timestamp",
|
|
||||||
"order": "DESCENDING"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collectionGroup": "notifications",
|
|
||||||
"queryScope": "COLLECTION",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "data.ownerId",
|
|
||||||
"order": "ASCENDING"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "type",
|
|
||||||
"order": "ASCENDING"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "timestamp",
|
|
||||||
"order": "DESCENDING"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collectionGroup": "notifications",
|
|
||||||
"queryScope": "COLLECTION",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "data.trainerId",
|
|
||||||
"order": "ASCENDING"
|
"order": "ASCENDING"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
9
functions/package-lock.json
generated
9
functions/package-lock.json
generated
@ -15,7 +15,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"firebase-admin": "^12.6.0",
|
"firebase-admin": "^12.6.0",
|
||||||
"firebase-functions": "^6.0.1",
|
"firebase-functions": "^6.4.0",
|
||||||
"form-data": "^4.0.1",
|
"form-data": "^4.0.1",
|
||||||
"functions": "file:",
|
"functions": "file:",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
@ -4897,9 +4897,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/firebase-functions": {
|
"node_modules/firebase-functions": {
|
||||||
"version": "6.3.2",
|
"version": "6.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.4.0.tgz",
|
||||||
"integrity": "sha512-FC3A1/nhqt1ZzxRnj5HZLScQaozAcFSD/vSR8khqSoFNOfxuXgwJS6ZABTB7+v+iMD5z6Mmxw6OfqITUBuI7OQ==",
|
"integrity": "sha512-Q/LGhJrmJEhT0dbV60J4hCkVSeOM6/r7xJS/ccmkXzTWMjo+UPAYX9zlQmGlEjotstZ0U9GtQSJSgbB2Z+TJDg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/cors": "^2.8.5",
|
"@types/cors": "^2.8.5",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"firebase-admin": "^12.6.0",
|
"firebase-admin": "^12.6.0",
|
||||||
"firebase-functions": "^6.0.1",
|
"firebase-functions": "^6.4.0",
|
||||||
"form-data": "^4.0.1",
|
"form-data": "^4.0.1",
|
||||||
"functions": "file:",
|
"functions": "file:",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
|
|||||||
@ -4,12 +4,14 @@ import * as admin from "firebase-admin";
|
|||||||
|
|
||||||
const app = getAdmin();
|
const app = getAdmin();
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
|
const kTrainerRole = "Trainer";
|
||||||
|
|
||||||
interface MembershipData {
|
interface MembershipData {
|
||||||
id?: string;
|
id?: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
gymId: string;
|
gymId: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
expirationDate?: admin.firestore.Timestamp;
|
||||||
subscription?: {
|
subscription?: {
|
||||||
name: string;
|
name: string;
|
||||||
frequency: string;
|
frequency: string;
|
||||||
@ -33,36 +35,83 @@ interface PaymentData {
|
|||||||
discount?: number;
|
discount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PersonalTrainerAssign {
|
||||||
|
id: string;
|
||||||
|
ownerId: string;
|
||||||
|
trainerId?: string;
|
||||||
|
clientId: string;
|
||||||
|
membershipId: string;
|
||||||
|
gymId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const checkExpiredMemberships = onSchedule(
|
export const checkExpiredMemberships = onSchedule(
|
||||||
{
|
{
|
||||||
schedule: "*/5 * * * *",
|
schedule: "0 8,14,20 * * *",
|
||||||
timeZone: "UTC",
|
timeZone: "UTC",
|
||||||
region: "#{SERVICES_RGN}#",
|
region: "#{SERVICES_RGN}#",
|
||||||
},
|
},
|
||||||
|
|
||||||
async (event) => {
|
async (event) => {
|
||||||
logger.info("Starting scheduled membership expiry check...");
|
logger.info("Starting scheduled membership expiry check...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await updateDaysUntilExpiryForAllMemberships();
|
||||||
const expiredMemberships = await findExpiredMemberships();
|
const expiredMemberships = await findExpiredMemberships();
|
||||||
|
const expiringMemberships = await findMembershipsExpiringIn10Days();
|
||||||
|
|
||||||
if (expiredMemberships.length === 0) {
|
const expiredMembershipsWithoutExpiryDate =
|
||||||
logger.info("No expired memberships found.");
|
await findExpiredMembershipsWithoutExpiryDate();
|
||||||
|
|
||||||
|
if (
|
||||||
|
expiredMemberships.length === 0 &&
|
||||||
|
expiringMemberships.length === 0 &&
|
||||||
|
expiredMembershipsWithoutExpiryDate.length === 0
|
||||||
|
) {
|
||||||
|
logger.info(
|
||||||
|
"No expired, expiring, or unprocessed expired memberships found."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Found ${expiredMemberships.length} expired memberships to process.`
|
`Found ${expiredMemberships.length} expired memberships, ${expiringMemberships.length} memberships expiring in 10 days, and ${expiredMembershipsWithoutExpiryDate.length} expired memberships without expiry dates 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 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(
|
logger.info(
|
||||||
`Completed processing. Success: ${successful}, Failed: ${failed}`
|
`Completed processing. Expired - Success: ${expiredSuccessful}, Failed: ${expiredFailed}. Expiring - Success: ${expiringSuccessful}, Failed: ${expiringFailed}. Updates - Success: ${updateSuccessful}, Failed: ${updateFailed}`
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error in scheduled membership expiry check:", error);
|
logger.error("Error in scheduled membership expiry check:", error);
|
||||||
@ -70,6 +119,83 @@ export const checkExpiredMemberships = onSchedule(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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<
|
async function findExpiredMemberships(): Promise<
|
||||||
Array<{ id: string; data: MembershipData }>
|
Array<{ id: string; data: MembershipData }>
|
||||||
> {
|
> {
|
||||||
@ -112,6 +238,51 @@ async function findExpiredMemberships(): Promise<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
async function checkIfMembershipExpired(
|
||||||
membershipId: string,
|
membershipId: string,
|
||||||
data: MembershipData
|
data: MembershipData
|
||||||
@ -165,6 +336,56 @@ async function checkIfMembershipExpired(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
async function getPaymentsForMembership(
|
||||||
membershipId: string
|
membershipId: string
|
||||||
): Promise<PaymentData[]> {
|
): Promise<PaymentData[]> {
|
||||||
@ -257,23 +478,385 @@ function calculateRenewalDateFromPayment(
|
|||||||
return renewalDate;
|
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(
|
async function processExpiredMembership(
|
||||||
membershipId: string,
|
membershipId: string,
|
||||||
membershipData: MembershipData
|
membershipData: MembershipData
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await app.firestore().collection("memberships").doc(membershipId).update({
|
const payments = await getPaymentsForMembership(membershipId);
|
||||||
status: "EXPIRED",
|
if (payments.length > 0) {
|
||||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
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`);
|
logger.info(`Marked membership ${membershipId} as EXPIRED`);
|
||||||
|
|
||||||
await sendPlanExpiredNotification(membershipId, membershipData);
|
await sendPlanExpiredNotification(membershipId, membershipData);
|
||||||
|
await sendTrainerNotifications(membershipId, membershipData, "expired");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error processing membership ${membershipId}:`, 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(
|
async function sendPlanExpiredNotification(
|
||||||
membershipId: string,
|
membershipId: string,
|
||||||
membershipData: MembershipData
|
membershipData: MembershipData
|
||||||
@ -283,19 +866,6 @@ async function sendPlanExpiredNotification(
|
|||||||
const gymOwnerId = await getGymOwnerId(membershipData.gymId);
|
const gymOwnerId = await getGymOwnerId(membershipData.gymId);
|
||||||
const gymName = await getGymName(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 expiryDate: Date | undefined;
|
||||||
let formattedDate = "Unknown Date";
|
let formattedDate = "Unknown Date";
|
||||||
|
|
||||||
@ -313,6 +883,26 @@ async function sendPlanExpiredNotification(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
await app
|
||||||
.firestore()
|
.firestore()
|
||||||
.collection("notifications")
|
.collection("notifications")
|
||||||
@ -323,15 +913,13 @@ async function sendPlanExpiredNotification(
|
|||||||
notificationSent: false,
|
notificationSent: false,
|
||||||
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
read: false,
|
read: false,
|
||||||
|
readBy: [],
|
||||||
data: {
|
data: {
|
||||||
title: "Plan Expired",
|
|
||||||
message: `The plan ${
|
|
||||||
membershipData.subscription?.name || "Unknown Plan"
|
|
||||||
} for client ${clientName} has expired.`,
|
|
||||||
planName: membershipData.subscription?.name || "Unknown Plan",
|
planName: membershipData.subscription?.name || "Unknown Plan",
|
||||||
clientName,
|
clientName,
|
||||||
membershipId,
|
membershipId,
|
||||||
gymName,
|
gymName,
|
||||||
|
ownerId: gymOwnerId,
|
||||||
formattedExpiryDate: formattedDate,
|
formattedExpiryDate: formattedDate,
|
||||||
expiryDate: expiryDate
|
expiryDate: expiryDate
|
||||||
? admin.firestore.Timestamp.fromDate(expiryDate)
|
? admin.firestore.Timestamp.fromDate(expiryDate)
|
||||||
@ -347,6 +935,95 @@ 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);
|
||||||
|
|
||||||
|
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(
|
async function getClientName(
|
||||||
membershipId: string,
|
membershipId: string,
|
||||||
clientId: string
|
clientId: string
|
||||||
|
|||||||
@ -82,15 +82,6 @@ async function getUserAndFCMToken(
|
|||||||
if (notification.recipientId) {
|
if (notification.recipientId) {
|
||||||
targetUserId = notification.recipientId;
|
targetUserId = notification.recipientId;
|
||||||
logger.info(`Using top-level recipientId: ${targetUserId}`);
|
logger.info(`Using top-level recipientId: ${targetUserId}`);
|
||||||
} else if (notification.data?.userId) {
|
|
||||||
targetUserId = notification.data.userId;
|
|
||||||
logger.info(`Using data.userId: ${targetUserId}`);
|
|
||||||
} else if (notification.data?.clientId) {
|
|
||||||
targetUserId = notification.data.clientId;
|
|
||||||
logger.info(`Using data.clientId: ${targetUserId}`);
|
|
||||||
} else if (notification.data?.invitorId) {
|
|
||||||
targetUserId = notification.data.invitorId;
|
|
||||||
logger.info(`Using data.invitorId: ${targetUserId}`);
|
|
||||||
} else if (notification.data?.phoneNumber) {
|
} else if (notification.data?.phoneNumber) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Looking up user by phone number from data: ${notification.data.phoneNumber}`
|
`Looking up user by phone number from data: ${notification.data.phoneNumber}`
|
||||||
@ -181,8 +172,8 @@ function prepareNotificationMessage(
|
|||||||
title =
|
title =
|
||||||
notification.data?.title ||
|
notification.data?.title ||
|
||||||
(notification.data?.status === "accepted"
|
(notification.data?.status === "accepted"
|
||||||
? "Trainer Request Accepted"
|
? "Trainer Invitation Accepted"
|
||||||
: "Trainer Request Update");
|
: "Trainer Invitation Update");
|
||||||
body =
|
body =
|
||||||
notification.data?.message ||
|
notification.data?.message ||
|
||||||
`${
|
`${
|
||||||
@ -240,7 +231,28 @@ function prepareNotificationMessage(
|
|||||||
title = notification.data?.title || "Plan Expired";
|
title = notification.data?.title || "Plan Expired";
|
||||||
body =
|
body =
|
||||||
notification.data?.message ||
|
notification.data?.message ||
|
||||||
`The plan ${notification.data?.planName} for client ${notification.data?.clientName} 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 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;
|
break;
|
||||||
|
|
||||||
case "schedule_update":
|
case "schedule_update":
|
||||||
|
|||||||
242
package-lock.json
generated
242
package-lock.json
generated
@ -1,242 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "fitlien-services",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"dependencies": {
|
|
||||||
"@types/busboy": "^1.5.4",
|
|
||||||
"@types/nodemailer": "^6.4.17",
|
|
||||||
"@types/pdfkit": "^0.13.9",
|
|
||||||
"busboy": "^1.6.0",
|
|
||||||
"date-fns": "^4.1.0",
|
|
||||||
"nodemailer": "^7.0.3",
|
|
||||||
"pdfkit": "^0.17.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@swc/helpers": {
|
|
||||||
"version": "0.5.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
|
||||||
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/busboy": {
|
|
||||||
"version": "1.5.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz",
|
|
||||||
"integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
|
||||||
"version": "22.10.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
|
|
||||||
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"undici-types": "~6.20.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/nodemailer": {
|
|
||||||
"version": "6.4.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
|
|
||||||
"integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/pdfkit": {
|
|
||||||
"version": "0.13.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.13.9.tgz",
|
|
||||||
"integrity": "sha512-RDG8Yb1zT7I01FfpwK7nMSA433XWpblMqSCtA5vJlSyavWZb303HUYPCel6JTiDDFqwGLvtAnYbH8N/e0Cb89g==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/base64-js": {
|
|
||||||
"version": "1.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
|
||||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/brotli": {
|
|
||||||
"version": "1.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
|
||||||
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
|
||||||
"dependencies": {
|
|
||||||
"base64-js": "^1.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/busboy": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
|
||||||
"dependencies": {
|
|
||||||
"streamsearch": "^1.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.16.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/clone": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/crypto-js": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
|
||||||
},
|
|
||||||
"node_modules/date-fns": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/kossnocorp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dfa": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
|
|
||||||
},
|
|
||||||
"node_modules/fast-deep-equal": {
|
|
||||||
"version": "3.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
|
||||||
},
|
|
||||||
"node_modules/fontkit": {
|
|
||||||
"version": "2.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
|
|
||||||
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/helpers": "^0.5.12",
|
|
||||||
"brotli": "^1.3.2",
|
|
||||||
"clone": "^2.1.2",
|
|
||||||
"dfa": "^1.2.0",
|
|
||||||
"fast-deep-equal": "^3.1.3",
|
|
||||||
"restructure": "^3.0.0",
|
|
||||||
"tiny-inflate": "^1.0.3",
|
|
||||||
"unicode-properties": "^1.4.0",
|
|
||||||
"unicode-trie": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jpeg-exif": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ=="
|
|
||||||
},
|
|
||||||
"node_modules/linebreak": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"base64-js": "0.0.8",
|
|
||||||
"unicode-trie": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/linebreak/node_modules/base64-js": {
|
|
||||||
"version": "0.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
|
|
||||||
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nodemailer": {
|
|
||||||
"version": "7.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz",
|
|
||||||
"integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pako": {
|
|
||||||
"version": "0.2.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
|
||||||
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
|
|
||||||
},
|
|
||||||
"node_modules/pdfkit": {
|
|
||||||
"version": "0.17.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.1.tgz",
|
|
||||||
"integrity": "sha512-Kkf1I9no14O/uo593DYph5u3QwiMfby7JsBSErN1WqeyTgCBNJE3K4pXBn3TgkdKUIVu+buSl4bYUNC+8Up4xg==",
|
|
||||||
"dependencies": {
|
|
||||||
"crypto-js": "^4.2.0",
|
|
||||||
"fontkit": "^2.0.4",
|
|
||||||
"jpeg-exif": "^1.1.4",
|
|
||||||
"linebreak": "^1.1.0",
|
|
||||||
"png-js": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/png-js": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
|
|
||||||
},
|
|
||||||
"node_modules/restructure": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="
|
|
||||||
},
|
|
||||||
"node_modules/streamsearch": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tiny-inflate": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
|
|
||||||
},
|
|
||||||
"node_modules/tslib": {
|
|
||||||
"version": "2.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
|
||||||
},
|
|
||||||
"node_modules/undici-types": {
|
|
||||||
"version": "6.20.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
|
||||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
|
|
||||||
},
|
|
||||||
"node_modules/unicode-properties": {
|
|
||||||
"version": "1.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
|
|
||||||
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
|
|
||||||
"dependencies": {
|
|
||||||
"base64-js": "^1.3.0",
|
|
||||||
"unicode-trie": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unicode-trie": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"pako": "^0.2.5",
|
|
||||||
"tiny-inflate": "^1.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"@types/busboy": "^1.5.4",
|
|
||||||
"busboy": "^1.6.0",
|
|
||||||
"date-fns": "^4.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user