From 2ab220d1e0ebf0c8aa4b1340c8e12261b7544487 Mon Sep 17 00:00:00 2001 From: Sharon Dcruz Date: Thu, 18 Sep 2025 14:05:21 +0530 Subject: [PATCH 1/5] Changes Updated --- functions/src/index.ts | 12 +- functions/src/memberCache/index.ts | 8 + functions/src/memberCache/memberCache.ts | 525 +++++++++++++++++++++++ 3 files changed, 544 insertions(+), 1 deletion(-) create mode 100644 functions/src/memberCache/index.ts create mode 100644 functions/src/memberCache/memberCache.ts diff --git a/functions/src/index.ts b/functions/src/index.ts index 625c6d7..e03316d 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -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,3 +21,13 @@ export { esslGetUserDetails, esslUpdateUser, esslDeleteUser, esslGetEmployeePunchLogs } from './dooraccess'; + +// Add member cache functions +export { + generateMemberCache, + updateTrainerAssignmentCache, + updateTimeSlotCache, + getCachedMembers, + rebuildGymCachee, + batchRebuildCaches +} from './memberCache'; \ No newline at end of file diff --git a/functions/src/memberCache/index.ts b/functions/src/memberCache/index.ts new file mode 100644 index 0000000..15ba89b --- /dev/null +++ b/functions/src/memberCache/index.ts @@ -0,0 +1,8 @@ +export { + generateMemberCache, + updateTrainerAssignmentCache, + updateTimeSlotCache, + getCachedMembers, + rebuildGymCachee, + batchRebuildCaches +} from './memberCache'; \ No newline at end of file diff --git a/functions/src/memberCache/memberCache.ts b/functions/src/memberCache/memberCache.ts new file mode 100644 index 0000000..9a1d5bd --- /dev/null +++ b/functions/src/memberCache/memberCache.ts @@ -0,0 +1,525 @@ +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; + 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; + createdAt?: admin.firestore.Timestamp; + [key: string]: any; +} + +interface TimeSlot { + id?: string; + membershipId?: string; + startTime?: admin.firestore.Timestamp; + endTime?: admin.firestore.Timestamp; + createdAt?: admin.firestore.Timestamp; + [key: string]: any; +} + +interface CacheEntry { + 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 rebuildGymCache(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(); + + 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 rebuildGymCache(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 updateTimeSlotCache = onDocumentWritten( + { + region: "#{SERVICES_RGN}#", + document: "scheduled_trainings/{slotId}", + }, + async (event) => { + try { + const slotData = event.data?.after?.exists + ? event.data.after.data() as TimeSlot + : null; + const oldSlotData = event.data?.before?.exists + ? event.data.before.data() as TimeSlot + : null; + + const gymIds = new Set(); + + if (slotData?.membershipId) { + const gymId = await getGymIdFromMembershipId(slotData.membershipId); + if (gymId) gymIds.add(gymId); + } + + if (oldSlotData?.membershipId) { + const gymId = await getGymIdFromMembershipId(oldSlotData.membershipId); + if (gymId) gymIds.add(gymId); + } + + for (const gymId of gymIds) { + await rebuildGymCache(gymId); + } + + if (gymIds.size > 0) { + logger.info(`Updated time slot cache for ${gymIds.size} gym(s)`); + } + } catch (error) { + logger.error('Error updating time slot 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 rebuildGymCache(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 rebuildGymCache(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 rebuildGymCachee = onCall( + { + region: "#{SERVICES_RGN}#", + }, + async (request) => { + const { gymId } = request.data; + + if (!gymId) { + throw new HttpsError('invalid-argument', 'gymId is required'); + } + + await rebuildGymCache(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 rebuildGymCache(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 rebuildGymCache(gymId: string): Promise { + 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 { + return await generateCacheEntry(doc.id, doc.data() as 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(membershipId: string, membershipData: MembershipData): Promise { + try { + let fields: { [key: string]: string } = {}; + try { + const clientFieldsSnapshot = await app + .firestore() + .collection('client_profiles') + .where('membershipId', '==', membershipId) + .get(); + + if (!clientFieldsSnapshot.empty) { + const fieldDoc = clientFieldsSnapshot.docs[0]; + const fieldData = fieldDoc.data() as ClientFields; + fields = fieldData.fields || {}; + } + } catch (error) { + logger.error(`Error getting fields for ${membershipId}:`, 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 ${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(); + + trainerAssignments = assignmentsSnapshot.docs.map(doc => { + const data = doc.data() as TrainerAssignment; + return { + id: doc.id, + ...data, + createdAt: data.createdAt ? data.createdAt.toDate().toISOString() : null + }; + }); + + const timeSlotsSnapshot = await app + .firestore() + .collection('scheduled_trainings') + .where('membershipId', '==', membershipId) + .get(); + + timeSlots = timeSlotsSnapshot.docs.map(doc => { + const data = doc.data() as TimeSlot; + return { + id: doc.id, + ...data, + startTime: data.startTime ? data.startTime.toDate().toISOString() : null, + endTime: data.endTime ? data.endTime.toDate().toISOString() : null, + createdAt: data.createdAt ? data.createdAt.toDate().toISOString() : null + }; + }); + } catch (error) { + logger.error(`Error getting trainer data for ${membershipId}:`, error); + } + } + + const cacheEntry: CacheEntry = { + membershipId, + memberData: { + ...membershipData, + daysUntilExpiry, + hasPartialPayment, + 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 { + 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 { + 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 rebuildGymCache(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)); + } +} \ No newline at end of file -- 2.43.0 From b0b71930037376b7873269fc95b3697a39dd9ed7 Mon Sep 17 00:00:00 2001 From: Sharon Dcruz Date: Thu, 18 Sep 2025 16:01:28 +0530 Subject: [PATCH 2/5] Changes Updated --- functions/src/index.ts | 1 - functions/src/memberCache/index.ts | 1 - functions/src/memberCache/memberCache.ts | 120 +++++++---------------- 3 files changed, 38 insertions(+), 84 deletions(-) diff --git a/functions/src/index.ts b/functions/src/index.ts index e03316d..b3ee3bf 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -26,7 +26,6 @@ export { export { generateMemberCache, updateTrainerAssignmentCache, - updateTimeSlotCache, getCachedMembers, rebuildGymCachee, batchRebuildCaches diff --git a/functions/src/memberCache/index.ts b/functions/src/memberCache/index.ts index 15ba89b..10c068d 100644 --- a/functions/src/memberCache/index.ts +++ b/functions/src/memberCache/index.ts @@ -1,7 +1,6 @@ export { generateMemberCache, updateTrainerAssignmentCache, - updateTimeSlotCache, getCachedMembers, rebuildGymCachee, batchRebuildCaches diff --git a/functions/src/memberCache/memberCache.ts b/functions/src/memberCache/memberCache.ts index 9a1d5bd..fb3ae5d 100644 --- a/functions/src/memberCache/memberCache.ts +++ b/functions/src/memberCache/memberCache.ts @@ -10,6 +10,7 @@ const CACHE_FOLDER = 'gym_member_cache'; interface MembershipData { gymId?: string; + userId?: string; status?: string; subscription?: { hasPersonalTraining?: boolean; @@ -36,21 +37,14 @@ interface PaymentData { interface TrainerAssignment { id?: string; membershipId?: string; - createdAt?: admin.firestore.Timestamp; - [key: string]: any; -} - -interface TimeSlot { - id?: string; - membershipId?: string; - startTime?: admin.firestore.Timestamp; - endTime?: admin.firestore.Timestamp; + timeSlot?: any[]; createdAt?: admin.firestore.Timestamp; [key: string]: any; } interface CacheEntry { - membershipId: string; + userId: string; + membershipId: string; memberData: { daysUntilExpiry?: number | null; hasPartialPayment: boolean; @@ -81,7 +75,7 @@ export const generateMemberCache = onDocumentWritten( async (event) => { try { const membershipId = event.params.membershipId; - const membershipData = event.data?.after?.exists + const membershipData = event.data?.after?.exists ? event.data.after.data() as MembershipData : null; @@ -144,45 +138,6 @@ export const updateTrainerAssignmentCache = onDocumentWritten( } ); -export const updateTimeSlotCache = onDocumentWritten( - { - region: "#{SERVICES_RGN}#", - document: "scheduled_trainings/{slotId}", - }, - async (event) => { - try { - const slotData = event.data?.after?.exists - ? event.data.after.data() as TimeSlot - : null; - const oldSlotData = event.data?.before?.exists - ? event.data.before.data() as TimeSlot - : null; - - const gymIds = new Set(); - - if (slotData?.membershipId) { - const gymId = await getGymIdFromMembershipId(slotData.membershipId); - if (gymId) gymIds.add(gymId); - } - - if (oldSlotData?.membershipId) { - const gymId = await getGymIdFromMembershipId(oldSlotData.membershipId); - if (gymId) gymIds.add(gymId); - } - - for (const gymId of gymIds) { - await rebuildGymCache(gymId); - } - - if (gymIds.size > 0) { - logger.info(`Updated time slot cache for ${gymIds.size} gym(s)`); - } - } catch (error) { - logger.error('Error updating time slot cache:', error); - } - } -); - export const getCachedMembers = onCall( { region: "#{SERVICES_RGN}#", @@ -302,7 +257,13 @@ async function rebuildGymCache(gymId: string): Promise { const batch = membershipsSnapshot.docs.slice(i, i + batchSize); const batchPromises = batch.map(async (doc) => { try { - return await generateCacheEntry(doc.id, doc.data() as MembershipData); + 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; @@ -343,24 +304,30 @@ async function rebuildGymCache(gymId: string): Promise { } } -async function generateCacheEntry(membershipId: string, membershipData: MembershipData): Promise { +async function generateCacheEntry(userId: string, membershipId: string, membershipData: MembershipData): Promise { try { let fields: { [key: string]: string } = {}; try { const clientFieldsSnapshot = await app .firestore() .collection('client_profiles') - .where('membershipId', '==', membershipId) + .where('uid', '==', userId) .get(); if (!clientFieldsSnapshot.empty) { const fieldDoc = clientFieldsSnapshot.docs[0]; const fieldData = fieldDoc.data() as ClientFields; - fields = fieldData.fields || {}; + 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 ${membershipId}:`, error); + logger.error(`Error getting fields for user ${userId}:`, error); } + let renewalDate: Date | null = null; let daysUntilExpiry: number | null = null; @@ -388,7 +355,7 @@ async function generateCacheEntry(membershipId: string, membershipData: Membersh } } } catch (error) { - logger.error(`Error getting renewal date for ${membershipId}:`, error); + logger.error(`Error getting renewal date for membership ${membershipId}:`, error); } } @@ -407,42 +374,31 @@ async function generateCacheEntry(membershipId: string, membershipData: Membersh .where('membershipId', '==', membershipId) .get(); - trainerAssignments = assignmentsSnapshot.docs.map(doc => { + assignmentsSnapshot.docs.forEach(doc => { const data = doc.data() as TrainerAssignment; - return { - id: doc.id, - ...data, - createdAt: data.createdAt ? data.createdAt.toDate().toISOString() : null - }; - }); - - const timeSlotsSnapshot = await app - .firestore() - .collection('scheduled_trainings') - .where('membershipId', '==', membershipId) - .get(); - - timeSlots = timeSlotsSnapshot.docs.map(doc => { - const data = doc.data() as TimeSlot; - return { - id: doc.id, - ...data, - startTime: data.startTime ? data.startTime.toDate().toISOString() : null, - endTime: data.endTime ? data.endTime.toDate().toISOString() : null, - createdAt: data.createdAt ? data.createdAt.toDate().toISOString() : null - }; + 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 ${membershipId}:`, error); + logger.error(`Error getting trainer data for membership ${membershipId}:`, error); } } const cacheEntry: CacheEntry = { + userId, membershipId, memberData: { - ...membershipData, - daysUntilExpiry, + 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 }, -- 2.43.0 From f56132ddddf3bd92df1fc879fff9c75fd7f8f427 Mon Sep 17 00:00:00 2001 From: Sharon Dcruz Date: Thu, 18 Sep 2025 17:49:54 +0530 Subject: [PATCH 3/5] Name Updated --- functions/src/index.ts | 3 +-- functions/src/memberCache/index.ts | 2 +- functions/src/memberCache/memberCache.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/functions/src/index.ts b/functions/src/index.ts index b3ee3bf..6163325 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -22,11 +22,10 @@ export { esslDeleteUser, esslGetEmployeePunchLogs } from './dooraccess'; -// Add member cache functions export { generateMemberCache, updateTrainerAssignmentCache, getCachedMembers, - rebuildGymCachee, + rebuildGymCache, batchRebuildCaches } from './memberCache'; \ No newline at end of file diff --git a/functions/src/memberCache/index.ts b/functions/src/memberCache/index.ts index 10c068d..fb4b6bb 100644 --- a/functions/src/memberCache/index.ts +++ b/functions/src/memberCache/index.ts @@ -2,6 +2,6 @@ export { generateMemberCache, updateTrainerAssignmentCache, getCachedMembers, - rebuildGymCachee, + rebuildGymCache, batchRebuildCaches } from './memberCache'; \ No newline at end of file diff --git a/functions/src/memberCache/memberCache.ts b/functions/src/memberCache/memberCache.ts index fb3ae5d..fa3a54c 100644 --- a/functions/src/memberCache/memberCache.ts +++ b/functions/src/memberCache/memberCache.ts @@ -90,7 +90,7 @@ export const generateMemberCache = onDocumentWritten( return; } - await rebuildGymCache(gymId); + await rebuildGymCachee(gymId); logger.info(`JSON cache updated for gym ${gymId} after member ${membershipId} change`); } catch (error) { @@ -126,7 +126,7 @@ export const updateTrainerAssignmentCache = onDocumentWritten( } for (const gymId of gymIds) { - await rebuildGymCache(gymId); + await rebuildGymCachee(gymId); } if (gymIds.size > 0) { @@ -167,7 +167,7 @@ export const getCachedMembers = onCall( 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 rebuildGymCache(gymId); + await rebuildGymCachee(gymId); } try { @@ -178,7 +178,7 @@ export const getCachedMembers = onCall( } catch (error) { logger.error(`Error reading cache file for gym ${gymId}:`, error); - await rebuildGymCache(gymId); + await rebuildGymCachee(gymId); const [fileBuffer] = await file.download(); const jsonData: JsonCacheData = JSON.parse(fileBuffer.toString()); return jsonData.members || []; @@ -190,7 +190,7 @@ export const getCachedMembers = onCall( } ); -export const rebuildGymCachee = onCall( +export const rebuildGymCache = onCall( { region: "#{SERVICES_RGN}#", }, @@ -201,7 +201,7 @@ export const rebuildGymCachee = onCall( throw new HttpsError('invalid-argument', 'gymId is required'); } - await rebuildGymCache(gymId); + await rebuildGymCachee(gymId); return { success: true, message: `JSON cache rebuilt for gym ${gymId}` }; } ); @@ -222,7 +222,7 @@ export const batchRebuildCaches = onCall( for (const gymId of gymIds) { try { - await rebuildGymCache(gymId); + await rebuildGymCachee(gymId); results.push({ gymId, status: 'success' }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -239,7 +239,7 @@ export const batchRebuildCaches = onCall( } ); -async function rebuildGymCache(gymId: string): Promise { +async function rebuildGymCachee(gymId: string): Promise { try { logger.info(`Starting JSON cache rebuild for gym: ${gymId}`); @@ -450,7 +450,7 @@ async function rebuildGymCacheFromDeletion(membershipId: string): Promise if (memberExists) { const gymId = file.name.replace(`${CACHE_FOLDER}/`, '').replace('.json', ''); logger.info(`Rebuilding cache for gym ${gymId} after member ${membershipId} deletion`); - await rebuildGymCache(gymId); + await rebuildGymCachee(gymId); break; } } catch (error) { -- 2.43.0 From 74e45dffb9286df985012403e14a8365230bae76 Mon Sep 17 00:00:00 2001 From: Sharon Dcruz Date: Fri, 19 Sep 2025 12:21:04 +0530 Subject: [PATCH 4/5] Changes Updated for Loading --- functions/src/index.ts | 39 +- functions/src/memberCache/index.ts | 8 +- functions/src/memberCache/memberCache.ts | 597 ++++++++++++----------- 3 files changed, 341 insertions(+), 303 deletions(-) diff --git a/functions/src/index.ts b/functions/src/index.ts index 6163325..a5d136c 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -6,26 +6,25 @@ setGlobalOptions({ timeoutSeconds: 540, minInstances: 0, maxInstances: 10, - concurrency: 80 -}); + concurrency: 80, +}); -export * from './shared/config'; -export { sendEmailSES } from './email'; -export { sendSMSMessage } from './sms'; -export { accessFile } from './storage'; -export { processNotificationOnCreate, checkExpiredMemberships } from './notifications'; -export * from './payments'; -export { getPlaceDetails, getPlacesAutocomplete } from './places'; -export { registerClient } from './users'; +export * from "./shared/config"; +export { sendEmailSES } from "./email"; +export { sendSMSMessage } from "./sms"; +export { accessFile } from "./storage"; export { - esslGetUserDetails, esslUpdateUser, - esslDeleteUser, esslGetEmployeePunchLogs -} from './dooraccess'; + processNotificationOnCreate, + checkExpiredMemberships, +} from "./notifications"; +export * from "./payments"; +export { getPlaceDetails, getPlacesAutocomplete } from "./places"; +export { registerClient } from "./users"; +export { + esslGetUserDetails, + esslUpdateUser, + esslDeleteUser, + esslGetEmployeePunchLogs, +} from "./dooraccess"; -export { - generateMemberCache, - updateTrainerAssignmentCache, - getCachedMembers, - rebuildGymCache, - batchRebuildCaches -} from './memberCache'; \ No newline at end of file +export { getMemberCache, updateMemberCache } from "./memberCache"; diff --git a/functions/src/memberCache/index.ts b/functions/src/memberCache/index.ts index fb4b6bb..c2e1d70 100644 --- a/functions/src/memberCache/index.ts +++ b/functions/src/memberCache/index.ts @@ -1,7 +1 @@ -export { - generateMemberCache, - updateTrainerAssignmentCache, - getCachedMembers, - rebuildGymCache, - batchRebuildCaches -} from './memberCache'; \ No newline at end of file +export { getMemberCache, updateMemberCache } from "./memberCache"; diff --git a/functions/src/memberCache/memberCache.ts b/functions/src/memberCache/memberCache.ts index fa3a54c..b2ee547 100644 --- a/functions/src/memberCache/memberCache.ts +++ b/functions/src/memberCache/memberCache.ts @@ -1,16 +1,15 @@ -import { onDocumentWritten } from "firebase-functions/v2/firestore"; -import { onCall, HttpsError } from "firebase-functions/v2/https"; +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'; +const CACHE_FOLDER = "gym_member_cache"; interface MembershipData { gymId?: string; - userId?: string; + userId?: string; status?: string; subscription?: { hasPersonalTraining?: boolean; @@ -37,14 +36,14 @@ interface PaymentData { interface TrainerAssignment { id?: string; membershipId?: string; - timeSlot?: any[]; + timeSlot?: any[]; createdAt?: admin.firestore.Timestamp; [key: string]: any; } interface CacheEntry { - userId: string; - membershipId: string; + userId: string; + membershipId: string; memberData: { daysUntilExpiry?: number | null; hasPartialPayment: boolean; @@ -67,200 +66,257 @@ interface JsonCacheData { cacheVersion: string; } -export const generateMemberCache = onDocumentWritten( +export const getMemberCache = onRequest( { region: "#{SERVICES_RGN}#", - document: "memberships/{membershipId}", + cors: true, }, - async (event) => { + async (req, res) => { try { - const membershipId = event.params.membershipId; - const membershipData = event.data?.after?.exists - ? event.data.after.data() as MembershipData - : null; - - if (!membershipData) { - await rebuildGymCacheFromDeletion(membershipId); + 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; } - const gymId = membershipData.gymId; + 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) { - logger.warn(`No gymId found for membership ${membershipId}`); + res.status(400).json({ error: "gymId is required" }); 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(); - - 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 || []; + + 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); - - await rebuildGymCachee(gymId); - const [fileBuffer] = await file.download(); - const jsonData: JsonCacheData = JSON.parse(fileBuffer.toString()); - return jsonData.members || []; + res.status(404).json({ + error: "Cache not found for this gym. Please update cache first.", + gymId, + members: [], + totalMembers: 0, + lastUpdated: new Date().toISOString(), + cacheVersion: "2.0", + }); } } catch (error) { - logger.error('Error getting cached members:', error); - throw new HttpsError('internal', 'Error retrieving cached data'); + logger.error("Error getting member cache:", error); + res.status(500).json({ error: "Error retrieving cached data" }); } } ); -export const rebuildGymCache = onCall( +export const updateMemberCache = onRequest( { region: "#{SERVICES_RGN}#", + cors: true, + timeoutSeconds: 540, }, - 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) => { + async (req, res) => { try { - const { gymIds } = request.data; - - if (!gymIds || !Array.isArray(gymIds)) { - throw new HttpsError('invalid-argument', 'gymIds array is required'); + 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; } - const results: Array<{ gymId: string; status: string; error?: string }> = []; - - for (const gymId of gymIds) { + 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: CacheEntry[] = []; + let existingData: JsonCacheData | null = null; + + if (incrementalUpdate) { try { - await rebuildGymCachee(gymId); - results.push({ gymId, status: 'success' }); + 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) { - const errorMessage = error instanceof Error ? error.message : String(error); - results.push({ gymId, status: 'error', error: errorMessage }); + logger.warn( + `Could not load existing cache for incremental update, performing full refresh: ${error}` + ); + incrementalUpdate = false; } } - logger.info(`Batch rebuild completed for ${gymIds.length} gyms`); - return { results }; + if (incrementalUpdate && membershipIds && Array.isArray(membershipIds)) { + await updateSpecificMembers(gymId, membershipIds, members); + } else { + members = await fetchAllMembers(gymId); + } + + const jsonData: JsonCacheData = { + gymId, + members, + totalMembers: members.length, + lastUpdated: new Date().toISOString(), + cacheVersion: "2.0", + }; + + 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) { - const errorMessage = error instanceof Error ? error.message : String(error); - throw new HttpsError('internal', errorMessage); + 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 rebuildGymCachee(gymId: string): Promise { - try { - logger.info(`Starting JSON cache rebuild for gym: ${gymId}`); - - const membershipsSnapshot = await app - .firestore() - .collection('memberships') - .where('gymId', '==', gymId) - .orderBy('createdAt', 'desc') - .get(); +async function fetchAllMembers(gymId: string): Promise { + const members: CacheEntry[] = []; - const members: CacheEntry[] = []; - const batchSize = 10; + const membershipsSnapshot = await app + .firestore() + .collection("memberships") + .where("gymId", "==", gymId) + .orderBy("createdAt", "desc") + .get(); - for (let i = 0; i < membershipsSnapshot.docs.length; i += batchSize) { - const batch = membershipsSnapshot.docs.slice(i, i + batchSize); - const batchPromises = batch.map(async (doc) => { + 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); + } + + return members; +} + +async function updateSpecificMembers( + gymId: string, + membershipIds: string[], + existingMembers: CacheEntry[] +): Promise { + logger.info(`Updating ${membershipIds.length} specific members`); + + const existingMembersMap = new Map(); + 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 membershipData = doc.data() as MembershipData; + 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`); + logger.warn( + `Skipping member with ID ${doc.id} due to missing userId` + ); return null; } return await generateCacheEntry(userId, doc.id, membershipData); @@ -269,59 +325,67 @@ async function rebuildGymCachee(gymId: string): Promise { 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 batchResults = await Promise.all(batchPromises); - 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() + batchResults.forEach((updatedMember) => { + if (updatedMember) { + const existingIndex = existingMembersMap.get( + updatedMember.membershipId + ); + if (existingIndex !== undefined) { + existingMembers[existingIndex] = updatedMember; + } else { + existingMembers.push(updatedMember); } } }); - - 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 { +async function saveCacheToStorage( + gymId: string, + jsonData: JsonCacheData +): Promise { + 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 generateCacheEntry( + userId: string, + membershipId: string, + membershipData: MembershipData +): Promise { try { let fields: { [key: string]: string } = {}; try { const clientFieldsSnapshot = await app .firestore() - .collection('client_profiles') - .where('uid', '==', userId) + .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'] || '', + "first-name": fieldData.fields?.["first-name"] || "", + "last-name": fieldData.fields?.["last-name"] || "", + "email": fieldData.fields?.["email"] || "", + "phone-number": fieldData.fields?.["phone-number"] || "", + "alternate-contact": fieldData.fields?.["alternate-contact"] || "", }; } } catch (error) { @@ -330,32 +394,40 @@ async function generateCacheEntry(userId: string, membershipId: string, membersh 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') + .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') { + 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)); + 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); + logger.error( + `Error getting renewal date for membership ${membershipId}:`, + error + ); } } @@ -365,24 +437,27 @@ async function generateCacheEntry(userId: string, membershipId: string, membersh let trainerAssignments: any[] = []; let timeSlots: any[] = []; - + if (membershipData.subscription?.hasPersonalTraining) { try { const assignmentsSnapshot = await app .firestore() - .collection('personal_trainer_assignments') - .where('membershipId', '==', membershipId) + .collection("personal_trainer_assignments") + .where("membershipId", "==", membershipId) .get(); - - assignmentsSnapshot.docs.forEach(doc => { + + 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 }); + trainerAssignments.push({ id: doc.id, ...data }); }); } catch (error) { - logger.error(`Error getting trainer data for membership ${membershipId}:`, error); + logger.error( + `Error getting trainer data for membership ${membershipId}:`, + error + ); } } @@ -390,17 +465,15 @@ async function generateCacheEntry(userId: string, membershipId: string, membersh userId, membershipId, memberData: { - status: membershipData.status, - subscription: { - hasPersonalTraining: membershipData.subscription?.hasPersonalTraining, - frequency: membershipData.subscription?.frequency, - }, - remainingAmount: membershipData.remainingAmount, - isPartialPayment: membershipData.isPartialPayment, + ...membershipData, hasPartialPayment, daysUntilExpiry, - createdAt: membershipData.createdAt ? membershipData.createdAt.toDate().toISOString() : null, - updatedAt: membershipData.updatedAt ? membershipData.updatedAt.toDate().toISOString() : null + createdAt: membershipData.createdAt + ? membershipData.createdAt.toDate().toISOString() + : null, + updatedAt: membershipData.updatedAt + ? membershipData.updatedAt.toDate().toISOString() + : null, }, fields, renewalDate: renewalDate ? renewalDate.toISOString() : null, @@ -411,71 +484,43 @@ async function generateCacheEntry(userId: string, membershipId: string, membersh return cacheEntry; } catch (error) { - logger.error('Error generating cache entry:', error); + logger.error("Error generating cache entry:", error); throw error; } } -async function getGymIdFromMembershipId(membershipId: string): Promise { - 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; - } -} +function calculateRenewalDateTimeFromPayment( + subscription: { frequency?: string }, + paymentDate: Date +): Date { + const frequency = subscription.frequency || "Monthly"; -async function rebuildGymCacheFromDeletion(membershipId: string): Promise { - 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()); + 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)); + return new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); } -} \ No newline at end of file +} -- 2.43.0 From 21664f8ea6de30612e80910cfc7e44215638e338 Mon Sep 17 00:00:00 2001 From: Sharon Dcruz Date: Fri, 19 Sep 2025 16:33:37 +0530 Subject: [PATCH 5/5] Updated Fields --- functions/src/memberCache/memberCache.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/functions/src/memberCache/memberCache.ts b/functions/src/memberCache/memberCache.ts index b2ee547..71e24b9 100644 --- a/functions/src/memberCache/memberCache.ts +++ b/functions/src/memberCache/memberCache.ts @@ -24,7 +24,7 @@ interface MembershipData { } interface ClientFields { - fields?: { [key: string]: string }; + fields?: { [key: string]: any }; [key: string]: any; } @@ -51,7 +51,7 @@ interface CacheEntry { updatedAt?: string | null; [key: string]: any; }; - fields: { [key: string]: string }; + fields: { [key: string]: any }; renewalDate?: string | null; trainerAssignments: any[]; timeSlots: any[]; @@ -380,13 +380,13 @@ async function generateCacheEntry( if (!clientFieldsSnapshot.empty) { const fieldDoc = clientFieldsSnapshot.docs[0]; const fieldData = fieldDoc.data() as ClientFields; - fields = { - "first-name": fieldData.fields?.["first-name"] || "", - "last-name": fieldData.fields?.["last-name"] || "", - "email": fieldData.fields?.["email"] || "", - "phone-number": fieldData.fields?.["phone-number"] || "", - "alternate-contact": fieldData.fields?.["alternate-contact"] || "", - }; + + fields = { ...fieldData.fields }; + Object.keys(fieldData).forEach((key) => { + if (key !== "fields" && !fields[key]) { + fields[key] = fieldData[key]; + } + }); } } catch (error) { logger.error(`Error getting fields for user ${userId}:`, error); -- 2.43.0