Compare commits

..

29 Commits
dev ... main

Author SHA1 Message Date
61f0d29f37 Merge branch 'qa'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 3m30s
2025-09-11 14:32:04 +05:30
209c7c65b0 Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 3m37s
2025-09-11 14:25:39 +05:30
5ca05c6490 Merge branch 'dev'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 4m1s
2025-08-25 18:39:53 +05:30
7ec65e4ab1 Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 4m1s
2025-08-25 18:35:11 +05:30
4f71ca273b Merge branch 'qa'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 1m40s
2025-08-22 17:39:15 +05:30
4d52d1c7f8 Merged from dev
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 1m40s
2025-08-22 17:36:45 +05:30
aee40521d3 Merge branch 'dev' into qa 2025-08-22 17:35:21 +05:30
36015d2b83 Removed npm install at root level
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 3m42s
2025-08-22 17:18:57 +05:30
51fa0825ca Removed npm install from root
Some checks failed
Deploy FitLien services to QA / Deploy to QA (push) Successful in 3m36s
Deploy FitLien services / Deploy (push) Failing after 8s
2025-08-22 17:04:54 +05:30
3e455fc83a Merge branch 'dev' into qa
Some checks failed
Deploy FitLien services to QA / Deploy to QA (push) Failing after 8s
2025-08-22 16:58:23 +05:30
f6b1545cf6 Merge branch 'dev' into qa
Some checks failed
Deploy FitLien services to QA / Deploy to QA (push) Failing after 1m33s
2025-08-18 19:05:38 +05:30
195262a6de Merge branch 'qa'
Some checks failed
Deploy FitLien services / Deploy (push) Failing after 2m8s
Deploy FitLien services to QA / Deploy to QA (push) Failing after 1m42s
2025-08-18 18:53:46 +05:30
d3c9e86c7c Merge branch 'dev'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 4m10s
2025-08-08 18:53:15 +05:30
8b308fb9a6 Merge branch 'dev'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 5m14s
2025-08-07 20:17:17 +05:30
9d51393aa5 Merge branch 'dev'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 3m39s
2025-07-02 21:28:42 +05:30
cc5b6d6987 Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 3m48s
2025-07-02 21:28:06 +05:30
ef2cd80226 Merge branch 'qa'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 4m2s
2025-06-24 15:19:54 +05:30
f19e3b012d Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 3m53s
2025-06-24 15:19:20 +05:30
f2e37e88ed Merge branch 'dev'
All checks were successful
Deploy FitLien services / Deploy (push) Successful in 4m24s
2025-06-12 19:47:27 +05:30
a0134466ee Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 3m42s
2025-06-12 19:45:40 +05:30
ecbe9d184b Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to QA / Deploy to QA (push) Successful in 3m47s
Deploy FitLien services / Deploy (push) Successful in 4m30s
2025-05-30 12:01:13 +05:30
7a796243b0 Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to Dev / Deploy to QA (push) Successful in 4m13s
2025-05-28 10:55:32 +05:30
7db9e479ad Merge branch 'dev' into qa
All checks were successful
Deploy FitLien services to Dev / Deploy to QA (push) Successful in 4m13s
2025-05-26 23:38:30 +05:30
cf6f4625ad Removed env var FITLIENHOST 2025-04-21 12:28:07 +05:30
6434f6e3fa Merge branch 'dev' into qa 2025-04-21 12:17:34 +05:30
2147963523 Merge branch 'dev' into qa 2025-04-16 18:12:16 +05:30
18569d38d3 Merge branch 'dev' into qa 2025-04-16 04:35:58 +05:30
5bc3d6dfff Merge branch 'dev' into qa 2025-04-16 03:53:43 +05:30
e8ca80df48 Merge branch 'dev' into qa 2025-04-14 15:02:28 +05:30
6 changed files with 14 additions and 516 deletions

View File

@ -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

View File

@ -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

View File

@ -6,25 +6,18 @@ setGlobalOptions({
timeoutSeconds: 540, timeoutSeconds: 540,
minInstances: 0, minInstances: 0,
maxInstances: 10, maxInstances: 10,
concurrency: 80, concurrency: 80
}); });
export * from "./shared/config"; export * from './shared/config';
export { sendEmailSES } from "./email"; export { sendEmailSES } from './email';
export { sendSMSMessage } from "./sms"; export { sendSMSMessage } from './sms';
export { accessFile } from "./storage"; export { accessFile } from './storage';
export { processNotificationOnCreate,checkExpiredMemberships } from './notifications';
export * from './payments';
export { getPlaceDetails, getPlacesAutocomplete } from './places';
export { registerClient } from './users';
export { export {
processNotificationOnCreate, esslGetUserDetails, esslUpdateUser,
checkExpiredMemberships, esslDeleteUser, esslGetEmployeePunchLogs
} from "./notifications"; } from './dooraccess';
export * from "./payments";
export { getPlaceDetails, getPlacesAutocomplete } from "./places";
export { registerClient } from "./users";
export {
esslGetUserDetails,
esslUpdateUser,
esslDeleteUser,
esslGetEmployeePunchLogs,
} from "./dooraccess";
export { getMemberCache, updateMemberCache } from "./memberCache";

View File

@ -1 +0,0 @@
export { getMemberCache, updateMemberCache } from "./memberCache";

View File

@ -1,446 +0,0 @@
import { onRequest } from "firebase-functions/v2/https";
import { getLogger, getAdmin } from "../shared/config";
import * as admin from "firebase-admin";
const app = getAdmin();
const logger = getLogger();
const CACHE_FOLDER = "gym_member_cache";
interface MembershipData {
gymId?: string;
userId?: string;
status?: string;
subscriptionId?: string;
subscription?: {
hasPersonalTraining?: boolean;
frequency?: string;
price?: number;
[key: string]: any;
};
isPartialPayment?: boolean;
remainingAmount?: number;
daysUntilExpiry?: number;
expirationDate?: admin.firestore.Timestamp;
createdAt?: admin.firestore.Timestamp;
updatedAt?: admin.firestore.Timestamp;
[key: string]: any;
}
interface ClientFields {
fields?: { [key: string]: any };
[key: string]: any;
}
interface MinimalCacheEntry {
membershipId: string;
userId: string;
status: string;
displayName: string;
email?: string | null;
phoneNumber?: string | null;
alternateContact?: string | null;
renewalDate?: string | null;
expirationDate?: string | null;
createdAt?: string | null;
daysUntilExpiry?: number | null;
hasPersonalTraining: boolean;
hasPartialPayment: boolean;
remainingAmount: number;
subscriptionId?: string | null;
lastUpdated: string;
}
interface MinimalJsonCacheData {
gymId: string;
members: MinimalCacheEntry[];
totalMembers: number;
lastUpdated: string;
cacheVersion: string;
}
export const getMemberCache = onRequest(
{
region: "#{SERVICES_RGN}#",
cors: true,
},
async (req, res) => {
try {
if (req.method === "OPTIONS") {
res.set("Access-Control-Allow-Origin", "*");
res.set("Access-Control-Allow-Methods", "GET, POST");
res.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.status(204).send("");
return;
}
res.set("Access-Control-Allow-Origin", "*");
res.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
let requestData: any = {};
if (req.method === "GET") {
requestData = req.query;
} else if (req.method === "POST") {
requestData = req.body;
}
const { gymId } = requestData;
if (!gymId) {
res.status(400).json({ error: "gymId is required" });
return;
}
const fileName = `${CACHE_FOLDER}/${gymId}.json`;
const file = app.storage().bucket().file(fileName);
try {
const [fileBuffer] = await file.download();
const jsonData: MinimalJsonCacheData = JSON.parse(
fileBuffer.toString()
);
logger.info(
`Retrieved ${jsonData.totalMembers} members from cache for gym ${gymId}`
);
res.status(200).json(jsonData);
} catch (error) {
logger.error(`Error reading cache file for gym ${gymId}:`, error);
res.status(404).json({
error: "Cache not found for this gym. Please update cache first.",
gymId,
members: [],
totalMembers: 0,
lastUpdated: new Date().toISOString(),
cacheVersion: "3.1",
});
}
} catch (error) {
logger.error("Error getting member cache:", error);
res.status(500).json({ error: "Error retrieving cached data" });
}
}
);
export const updateMemberCache = onRequest(
{
region: "#{SERVICES_RGN}#",
cors: true,
timeoutSeconds: 540,
},
async (req, res) => {
try {
if (req.method === "OPTIONS") {
res.set("Access-Control-Allow-Origin", "*");
res.set("Access-Control-Allow-Methods", "POST");
res.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.status(204).send("");
return;
}
res.set("Access-Control-Allow-Origin", "*");
res.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (req.method !== "POST") {
res.status(405).json({ error: "Method not allowed. Use POST." });
return;
}
const { gymId, membershipIds } = req.body;
let incrementalUpdate = req.body.incrementalUpdate || false;
if (!gymId) {
res.status(400).json({ error: "gymId is required" });
return;
}
logger.info(
`Starting cache ${
incrementalUpdate ? "incremental update" : "full refresh"
} for gym: ${gymId}`
);
let members: MinimalCacheEntry[] = [];
let existingData: MinimalJsonCacheData | null = null;
if (incrementalUpdate) {
try {
const fileName = `${CACHE_FOLDER}/${gymId}.json`;
const file = app.storage().bucket().file(fileName);
const [fileBuffer] = await file.download();
existingData = JSON.parse(fileBuffer.toString());
if (existingData) {
members = [...existingData.members];
logger.info(
`Loaded existing cache with ${members.length} members for incremental update`
);
} else {
logger.warn(
"Existing cache data is invalid, performing full refresh"
);
incrementalUpdate = false;
}
} catch (error) {
logger.warn(
`Could not load existing cache for incremental update, performing full refresh: ${error}`
);
incrementalUpdate = false;
}
}
if (incrementalUpdate && membershipIds && Array.isArray(membershipIds)) {
await updateSpecificMembers(gymId, membershipIds, members);
} else {
members = await fetchAllMinimalMembers(gymId);
}
const jsonData: MinimalJsonCacheData = {
gymId,
members,
totalMembers: members.length,
lastUpdated: new Date().toISOString(),
cacheVersion: "3.1",
};
await saveCacheToStorage(gymId, jsonData);
const updateType = incrementalUpdate
? "incremental update"
: "full refresh";
logger.info(
`Cache ${updateType} completed successfully for gym ${gymId} with ${members.length} members`
);
res.status(200).json({
success: true,
message: `Cache ${updateType} completed successfully for gym ${gymId}`,
totalMembers: members.length,
lastUpdated: jsonData.lastUpdated,
updateType: updateType,
});
} catch (error) {
logger.error("Error updating member cache:", error);
res.status(500).json({
error: "Error updating cache",
details: error instanceof Error ? error.message : String(error),
});
}
}
);
async function fetchAllMinimalMembers(
gymId: string
): Promise<MinimalCacheEntry[]> {
const members: MinimalCacheEntry[] = [];
const membershipsSnapshot = await app
.firestore()
.collection("memberships")
.where("gymId", "==", gymId)
.orderBy("createdAt", "desc")
.get();
const batchSize = 10;
for (let i = 0; i < membershipsSnapshot.docs.length; i += batchSize) {
const batch = membershipsSnapshot.docs.slice(i, i + batchSize);
const batchPromises = batch.map(async (doc) => {
try {
const membershipData = doc.data() as MembershipData;
const userId = membershipData.userId;
if (!userId) {
logger.warn(
`Skipping member with ID ${doc.id} due to missing userId`
);
return null;
}
return await generateMinimalCacheEntry(userId, doc.id, membershipData, gymId);
} catch (error) {
logger.error(`Error processing member ${doc.id}:`, error);
return null;
}
});
const batchResults = await Promise.all(batchPromises);
const validResults = batchResults.filter(
(member): member is MinimalCacheEntry => member !== null
);
members.push(...validResults);
}
return members;
}
async function updateSpecificMembers(
gymId: string,
membershipIds: string[],
existingMembers: MinimalCacheEntry[]
): Promise<void> {
logger.info(`Updating ${membershipIds.length} specific members`);
const existingMembersMap = new Map<string, number>();
existingMembers.forEach((member, index) => {
existingMembersMap.set(member.membershipId, index);
});
const batchSize = 10;
for (let i = 0; i < membershipIds.length; i += batchSize) {
const batch = membershipIds.slice(i, i + batchSize);
const membershipDocs = await Promise.all(
batch.map(async (membershipId) => {
try {
const doc = await app
.firestore()
.collection("memberships")
.doc(membershipId)
.get();
return doc.exists ? { id: doc.id, data: doc.data() } : null;
} catch (error) {
logger.error(`Error fetching membership ${membershipId}:`, error);
return null;
}
})
);
const batchPromises = membershipDocs
.filter((doc): doc is { id: string; data: any } => doc !== null)
.map(async (doc) => {
try {
const membershipData = doc.data as MembershipData;
const userId = membershipData.userId;
if (!userId) {
logger.warn(
`Skipping member with ID ${doc.id} due to missing userId`
);
return null;
}
return await generateMinimalCacheEntry(
userId,
doc.id,
membershipData,
gymId
);
} catch (error) {
logger.error(`Error processing member ${doc.id}:`, error);
return null;
}
});
const batchResults = await Promise.all(batchPromises);
batchResults.forEach((updatedMember) => {
if (updatedMember) {
const existingIndex = existingMembersMap.get(
updatedMember.membershipId
);
if (existingIndex !== undefined) {
existingMembers[existingIndex] = updatedMember;
} else {
existingMembers.push(updatedMember);
}
}
});
}
}
async function saveCacheToStorage(
gymId: string,
jsonData: MinimalJsonCacheData
): Promise<void> {
const fileName = `${CACHE_FOLDER}/${gymId}.json`;
const file = app.storage().bucket().file(fileName);
await file.save(JSON.stringify(jsonData, null, 2), {
metadata: {
contentType: "application/json",
metadata: {
gymId: gymId,
totalMembers: jsonData.totalMembers.toString(),
generatedAt: jsonData.lastUpdated,
cacheVersion: jsonData.cacheVersion,
},
},
});
}
async function generateMinimalCacheEntry(
userId: string,
membershipId: string,
membershipData: MembershipData,
gymId: string
): Promise<MinimalCacheEntry> {
try {
let firstName = "";
let lastName = "";
let email = "";
let phoneNumber = "";
let alternateContact = "";
try {
const clientFieldsSnapshot = await app
.firestore()
.collection("client_profiles")
.where("uid", "==", userId)
.get();
if (!clientFieldsSnapshot.empty) {
const fieldDoc = clientFieldsSnapshot.docs[0];
const fieldData = fieldDoc.data() as ClientFields;
const fields = fieldData.fields || {};
firstName = fields["first-name"] || fieldData["first-name"] || "";
lastName = fields["last-name"] || fieldData["last-name"] || "";
email = fields["email"] || fieldData["email"] || "";
phoneNumber = fields["phone-number"] || fieldData["phone-number"] || "";
alternateContact =
fields["alternate-contact"] || fieldData["alternate-contact"] || "";
}
} catch (error) {
logger.error(`Error getting fields for user ${userId}:`, error);
}
const daysUntilExpiry = membershipData.daysUntilExpiry || null;
const displayName =
firstName.length === 0
? "Unknown"
: lastName.length === 0
? firstName
: `${firstName} ${lastName}`;
const isPartial = membershipData.isPartialPayment === true;
const remaining = membershipData.remainingAmount || 0;
const hasPartialPayment = isPartial && remaining > 0;
const minimalEntry: MinimalCacheEntry = {
membershipId,
userId,
status: membershipData.status || "N/A",
displayName,
email: email || null,
phoneNumber: phoneNumber || null,
alternateContact: alternateContact || null,
renewalDate: null,
expirationDate: membershipData.expirationDate
? membershipData.expirationDate.toDate().toISOString()
: null,
createdAt: membershipData.createdAt
? membershipData.createdAt.toDate().toISOString()
: null,
daysUntilExpiry,
hasPersonalTraining:
membershipData.subscription?.hasPersonalTraining === true,
hasPartialPayment,
remainingAmount: remaining,
subscriptionId: membershipData.subscriptionId || null,
lastUpdated: new Date().toISOString(),
};
return minimalEntry;
} catch (error) {
logger.error("Error generating minimal cache entry:", error);
throw error;
}
}

View File

@ -6,40 +6,6 @@ const app = getAdmin();
const logger = getLogger(); const logger = getLogger();
const kTrainerRole = "Trainer"; const kTrainerRole = "Trainer";
async function updateCacheForMembership(
gymId: string,
membershipId: string
): Promise<void> {
try {
const response = await fetch(
`https://updatemembercache-2k7djjvd3q-el.a.run.app/updateMemberCache`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
gymId: gymId,
incrementalUpdate: true,
membershipIds: [membershipId],
}),
}
);
if (response.ok) {
logger.info(
`Cache updated successfully for membership ${membershipId} in gym ${gymId}`
);
} else {
logger.warn(
`Cache update failed for membership ${membershipId}: ${response.status}`
);
}
} catch (error) {
logger.error(`Error updating cache for membership ${membershipId}:`, error);
}
}
interface MembershipData { interface MembershipData {
id?: string; id?: string;
userId: string; userId: string;
@ -184,7 +150,6 @@ async function findExpiredMembershipsWithoutExpiryDate(): Promise<
throw error; throw error;
} }
} }
async function updateExpiryDateForExpiredMembership( async function updateExpiryDateForExpiredMembership(
membershipId: string, membershipId: string,
membershipData: MembershipData membershipData: MembershipData
@ -219,8 +184,6 @@ async function updateExpiryDateForExpiredMembership(
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: admin.firestore.FieldValue.serverTimestamp(),
}); });
await updateCacheForMembership(membershipData.gymId, membershipId);
logger.info( logger.info(
`Updated expiry date for expired membership ${membershipId}: ${expiryDate.toISOString()}` `Updated expiry date for expired membership ${membershipId}: ${expiryDate.toISOString()}`
); );
@ -550,8 +513,6 @@ async function updateDaysUntilExpiryForAllMemberships(): Promise<void> {
.doc(doc.id) .doc(doc.id)
.update(updateData); .update(updateData);
await updateCacheForMembership(data.gymId, doc.id);
logger.info( logger.info(
`Updated membership ${doc.id} with daysUntilExpiry: ${daysUntilExpiry}` `Updated membership ${doc.id} with daysUntilExpiry: ${daysUntilExpiry}`
); );
@ -574,6 +535,7 @@ async function updateDaysUntilExpiryForAllMemberships(): Promise<void> {
throw error; throw error;
} }
} }
async function calculateDaysUntilExpiry( async function calculateDaysUntilExpiry(
membershipId: string, membershipId: string,
data: MembershipData data: MembershipData
@ -703,8 +665,6 @@ async function processExpiredMembership(
}); });
} }
await updateCacheForMembership(membershipData.gymId, membershipId);
logger.info(`Marked membership ${membershipId} as EXPIRED`); logger.info(`Marked membership ${membershipId} as EXPIRED`);
await sendPlanExpiredNotification(membershipId, membershipData); await sendPlanExpiredNotification(membershipId, membershipData);
@ -737,8 +697,6 @@ async function processExpiringMembership(
expirationDate: admin.firestore.Timestamp.fromDate(expiryDate), expirationDate: admin.firestore.Timestamp.fromDate(expiryDate),
updatedAt: admin.firestore.FieldValue.serverTimestamp(), updatedAt: admin.firestore.FieldValue.serverTimestamp(),
}); });
await updateCacheForMembership(membershipData.gymId, membershipId);
} }
await sendPlanExpiringNotification(membershipId, membershipData); await sendPlanExpiringNotification(membershipId, membershipData);