Compare commits

..

29 Commits

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
5 changed files with 1 additions and 503 deletions

View File

@ -19,9 +19,6 @@ jobs:
with:
node-version: 22
- name: Clean install
run: npm clean-install
- name: Copy .env.example to .env
run: cp functions/.env.example functions/.env

View File

@ -19,9 +19,6 @@ jobs:
with:
node-version: 22
- name: Clean install
run: npm clean-install
- name: Copy .env.example to .env
run: cp functions/.env.example functions/.env

View File

@ -13,7 +13,7 @@ export * from './shared/config';
export { sendEmailSES } from './email';
export { sendSMSMessage } from './sms';
export { accessFile } from './storage';
export { processNotificationOnCreate, checkExpiredMemberships } from './notifications';
export { processNotificationOnCreate,checkExpiredMemberships } from './notifications';
export * from './payments';
export { getPlaceDetails, getPlacesAutocomplete } from './places';
export { registerClient } from './users';
@ -21,11 +21,3 @@ export {
esslGetUserDetails, esslUpdateUser,
esslDeleteUser, esslGetEmployeePunchLogs
} from './dooraccess';
export {
generateMemberCache,
updateTrainerAssignmentCache,
getCachedMembers,
rebuildGymCache,
batchRebuildCaches
} from './memberCache';

View File

@ -1,7 +0,0 @@
export {
generateMemberCache,
updateTrainerAssignmentCache,
getCachedMembers,
rebuildGymCache,
batchRebuildCaches
} from './memberCache';

View File

@ -1,481 +0,0 @@
import { onDocumentWritten } from "firebase-functions/v2/firestore";
import { onCall, HttpsError } 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;
subscription?: {
hasPersonalTraining?: boolean;
frequency?: string;
[key: string]: any;
};
isPartialPayment?: boolean;
remainingAmount?: number;
createdAt?: admin.firestore.Timestamp;
updatedAt?: admin.firestore.Timestamp;
[key: string]: any;
}
interface ClientFields {
fields?: { [key: string]: string };
[key: string]: any;
}
interface PaymentData {
dateTimestamp?: admin.firestore.Timestamp;
[key: string]: any;
}
interface TrainerAssignment {
id?: string;
membershipId?: string;
timeSlot?: any[];
createdAt?: admin.firestore.Timestamp;
[key: string]: any;
}
interface CacheEntry {
userId: string;
membershipId: string;
memberData: {
daysUntilExpiry?: number | null;
hasPartialPayment: boolean;
createdAt?: string | null;
updatedAt?: string | null;
[key: string]: any;
};
fields: { [key: string]: string };
renewalDate?: string | null;
trainerAssignments: any[];
timeSlots: any[];
lastUpdated: string;
}
interface JsonCacheData {
gymId: string;
members: CacheEntry[];
totalMembers: number;
lastUpdated: string;
cacheVersion: string;
}
export const generateMemberCache = onDocumentWritten(
{
region: "#{SERVICES_RGN}#",
document: "memberships/{membershipId}",
},
async (event) => {
try {
const membershipId = event.params.membershipId;
const membershipData = event.data?.after?.exists
? event.data.after.data() as MembershipData
: null;
if (!membershipData) {
await rebuildGymCacheFromDeletion(membershipId);
return;
}
const gymId = membershipData.gymId;
if (!gymId) {
logger.warn(`No gymId found for membership ${membershipId}`);
return;
}
await rebuildGymCachee(gymId);
logger.info(`JSON cache updated for gym ${gymId} after member ${membershipId} change`);
} catch (error) {
logger.error('Error updating member cache:', error);
}
}
);
export const updateTrainerAssignmentCache = onDocumentWritten(
{
region: "#{SERVICES_RGN}#",
document: "personal_trainer_assignments/{assignmentId}",
},
async (event) => {
try {
const assignmentData = event.data?.after?.exists
? event.data.after.data() as TrainerAssignment
: null;
const oldAssignmentData = event.data?.before?.exists
? event.data.before.data() as TrainerAssignment
: null;
const gymIds = new Set<string>();
if (assignmentData?.membershipId) {
const gymId = await getGymIdFromMembershipId(assignmentData.membershipId);
if (gymId) gymIds.add(gymId);
}
if (oldAssignmentData?.membershipId) {
const gymId = await getGymIdFromMembershipId(oldAssignmentData.membershipId);
if (gymId) gymIds.add(gymId);
}
for (const gymId of gymIds) {
await rebuildGymCachee(gymId);
}
if (gymIds.size > 0) {
logger.info(`Updated trainer assignment cache for ${gymIds.size} gym(s)`);
}
} catch (error) {
logger.error('Error updating trainer assignment cache:', error);
}
}
);
export const getCachedMembers = onCall(
{
region: "#{SERVICES_RGN}#",
},
async (request) => {
try {
const { gymId, forceRefresh } = request.data;
if (!gymId) {
throw new HttpsError('invalid-argument', 'gymId is required');
}
const fileName = `${CACHE_FOLDER}/${gymId}.json`;
const file = app.storage().bucket().file(fileName);
let fileExists = false;
let fileAge = 0;
try {
const [metadata] = await file.getMetadata();
fileExists = true;
fileAge = Date.now() - new Date(metadata.timeCreated!).getTime();
} catch (error) {
fileExists = false;
}
const fiveMinutes = 5 * 60 * 1000;
if (forceRefresh || !fileExists || fileAge > fiveMinutes) {
logger.info(`Rebuilding cache for gym ${gymId} - forceRefresh: ${forceRefresh}, fileExists: ${fileExists}, fileAge: ${fileAge}ms`);
await rebuildGymCachee(gymId);
}
try {
const [fileBuffer] = await file.download();
const jsonData: JsonCacheData = JSON.parse(fileBuffer.toString());
logger.info(`Retrieved ${jsonData.members?.length || 0} members from cache for gym ${gymId}`);
return jsonData.members || [];
} catch (error) {
logger.error(`Error reading cache file for gym ${gymId}:`, error);
await rebuildGymCachee(gymId);
const [fileBuffer] = await file.download();
const jsonData: JsonCacheData = JSON.parse(fileBuffer.toString());
return jsonData.members || [];
}
} catch (error) {
logger.error('Error getting cached members:', error);
throw new HttpsError('internal', 'Error retrieving cached data');
}
}
);
export const rebuildGymCache = onCall(
{
region: "#{SERVICES_RGN}#",
},
async (request) => {
const { gymId } = request.data;
if (!gymId) {
throw new HttpsError('invalid-argument', 'gymId is required');
}
await rebuildGymCachee(gymId);
return { success: true, message: `JSON cache rebuilt for gym ${gymId}` };
}
);
export const batchRebuildCaches = onCall(
{
region: "#{SERVICES_RGN}#",
},
async (request) => {
try {
const { gymIds } = request.data;
if (!gymIds || !Array.isArray(gymIds)) {
throw new HttpsError('invalid-argument', 'gymIds array is required');
}
const results: Array<{ gymId: string; status: string; error?: string }> = [];
for (const gymId of gymIds) {
try {
await rebuildGymCachee(gymId);
results.push({ gymId, status: 'success' });
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
results.push({ gymId, status: 'error', error: errorMessage });
}
}
logger.info(`Batch rebuild completed for ${gymIds.length} gyms`);
return { results };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new HttpsError('internal', errorMessage);
}
}
);
async function rebuildGymCachee(gymId: string): Promise<void> {
try {
logger.info(`Starting JSON cache rebuild for gym: ${gymId}`);
const membershipsSnapshot = await app
.firestore()
.collection('memberships')
.where('gymId', '==', gymId)
.orderBy('createdAt', 'desc')
.get();
const members: CacheEntry[] = [];
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 generateCacheEntry(userId, doc.id, membershipData);
} 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 CacheEntry => member !== null);
members.push(...validResults);
}
const jsonData: JsonCacheData = {
gymId,
members,
totalMembers: members.length,
lastUpdated: new Date().toISOString(),
cacheVersion: '1.0'
};
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: members.length.toString(),
generatedAt: new Date().toISOString()
}
}
});
logger.info(`JSON cache saved for gym ${gymId} with ${members.length} members`);
} catch (error) {
logger.error(`Error rebuilding JSON cache for gym ${gymId}:`, error);
throw error;
}
}
async function generateCacheEntry(userId: string, membershipId: string, membershipData: MembershipData): Promise<CacheEntry> {
try {
let fields: { [key: string]: string } = {};
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;
fields = {
'first-name': fieldData.fields?.['first-name'] || '',
'email': fieldData.fields?.['email'] || '',
'phone-number': fieldData.fields?.['phone-number'] || '',
'alternate-contact': fieldData.fields?.['alternate-contact'] || '',
};
}
} catch (error) {
logger.error(`Error getting fields for user ${userId}:`, error);
}
let renewalDate: Date | null = null;
let daysUntilExpiry: number | null = null;
if (membershipData.subscription) {
try {
const paymentsSnapshot = await app
.firestore()
.collection('membership_payments')
.where('membershipId', '==', membershipId)
.orderBy('dateTimestamp', 'desc')
.limit(1)
.get();
if (!paymentsSnapshot.empty) {
const latestPayment = paymentsSnapshot.docs[0].data() as PaymentData;
if (latestPayment.dateTimestamp) {
const paymentDate = latestPayment.dateTimestamp.toDate();
renewalDate = calculateRenewalDateTimeFromPayment(membershipData.subscription, paymentDate);
if (renewalDate && membershipData.status === 'ACTIVE') {
const now = new Date();
const difference = Math.floor((renewalDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
daysUntilExpiry = difference > 0 ? difference : null;
}
}
}
} catch (error) {
logger.error(`Error getting renewal date for membership ${membershipId}:`, error);
}
}
const isPartial = membershipData.isPartialPayment === true;
const remaining = membershipData.remainingAmount || 0;
const hasPartialPayment = isPartial && remaining > 0;
let trainerAssignments: any[] = [];
let timeSlots: any[] = [];
if (membershipData.subscription?.hasPersonalTraining) {
try {
const assignmentsSnapshot = await app
.firestore()
.collection('personal_trainer_assignments')
.where('membershipId', '==', membershipId)
.get();
assignmentsSnapshot.docs.forEach(doc => {
const data = doc.data() as TrainerAssignment;
if (data.timeSlot && Array.isArray(data.timeSlot)) {
timeSlots.push(...data.timeSlot);
}
trainerAssignments.push({ id: doc.id });
});
} catch (error) {
logger.error(`Error getting trainer data for membership ${membershipId}:`, error);
}
}
const cacheEntry: CacheEntry = {
userId,
membershipId,
memberData: {
status: membershipData.status,
subscription: {
hasPersonalTraining: membershipData.subscription?.hasPersonalTraining,
frequency: membershipData.subscription?.frequency,
},
remainingAmount: membershipData.remainingAmount,
isPartialPayment: membershipData.isPartialPayment,
hasPartialPayment,
daysUntilExpiry,
createdAt: membershipData.createdAt ? membershipData.createdAt.toDate().toISOString() : null,
updatedAt: membershipData.updatedAt ? membershipData.updatedAt.toDate().toISOString() : null
},
fields,
renewalDate: renewalDate ? renewalDate.toISOString() : null,
trainerAssignments,
timeSlots,
lastUpdated: new Date().toISOString(),
};
return cacheEntry;
} catch (error) {
logger.error('Error generating cache entry:', error);
throw error;
}
}
async function getGymIdFromMembershipId(membershipId: string): Promise<string | null> {
try {
const membershipDoc = await app.firestore().collection('memberships').doc(membershipId).get();
if (membershipDoc.exists) {
const data = membershipDoc.data() as MembershipData;
return data.gymId || null;
}
return null;
} catch (error) {
logger.error(`Error getting gym ID for membership ${membershipId}:`, error);
return null;
}
}
async function rebuildGymCacheFromDeletion(membershipId: string): Promise<void> {
try {
const [files] = await app.storage().bucket().getFiles({
prefix: `${CACHE_FOLDER}/`,
delimiter: '/'
});
for (const file of files) {
if (file.name.endsWith('.json')) {
try {
const [fileBuffer] = await file.download();
const jsonData: JsonCacheData = JSON.parse(fileBuffer.toString());
const memberExists = jsonData.members?.some(member =>
member.membershipId === membershipId
);
if (memberExists) {
const gymId = file.name.replace(`${CACHE_FOLDER}/`, '').replace('.json', '');
logger.info(`Rebuilding cache for gym ${gymId} after member ${membershipId} deletion`);
await rebuildGymCachee(gymId);
break;
}
} catch (error) {
logger.error(`Error checking cache file ${file.name}:`, error);
}
}
}
} catch (error) {
logger.error('Error handling membership deletion:', error);
}
}
function calculateRenewalDateTimeFromPayment(subscription: { frequency?: string }, paymentDate: Date): Date {
const frequency = subscription.frequency || 'Monthly';
switch (frequency) {
case 'Monthly':
return new Date(paymentDate.getFullYear(), paymentDate.getMonth() + 1, paymentDate.getDate());
case 'Quarterly':
return new Date(paymentDate.getFullYear(), paymentDate.getMonth() + 3, paymentDate.getDate());
case 'Half-yearly':
return new Date(paymentDate.getFullYear(), paymentDate.getMonth() + 6, paymentDate.getDate());
case 'Yearly':
return new Date(paymentDate.getFullYear() + 1, paymentDate.getMonth(), paymentDate.getDate());
default:
return new Date(Date.now() + (30 * 24 * 60 * 60 * 1000));
}
}