feature/fitlien-828 #120
| @ -13,7 +13,7 @@ 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 { processNotificationOnCreate, checkExpiredMemberships } from './notifications'; | ||||||
| export * from './payments'; | export * from './payments'; | ||||||
| export { getPlaceDetails, getPlacesAutocomplete } from './places'; | export { getPlaceDetails, getPlacesAutocomplete } from './places'; | ||||||
| export { registerClient } from './users'; | export { registerClient } from './users'; | ||||||
| @ -21,3 +21,13 @@ export { | |||||||
|   esslGetUserDetails, esslUpdateUser, |   esslGetUserDetails, esslUpdateUser, | ||||||
|   esslDeleteUser, esslGetEmployeePunchLogs |   esslDeleteUser, esslGetEmployeePunchLogs | ||||||
| } from './dooraccess'; | } from './dooraccess'; | ||||||
|  | 
 | ||||||
|  | // Add member cache functions
 | ||||||
|  | export { | ||||||
|  |   generateMemberCache, | ||||||
|  |   updateTrainerAssignmentCache, | ||||||
|  |   updateTimeSlotCache, | ||||||
|  |   getCachedMembers, | ||||||
|  |   rebuildGymCachee, | ||||||
|  |   batchRebuildCaches | ||||||
|  | } from './memberCache'; | ||||||
							
								
								
									
										8
									
								
								functions/src/memberCache/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								functions/src/memberCache/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | export { | ||||||
|  |   generateMemberCache, | ||||||
|  |   updateTrainerAssignmentCache, | ||||||
|  |   updateTimeSlotCache, | ||||||
|  |   getCachedMembers, | ||||||
|  |   rebuildGymCachee, | ||||||
|  |   batchRebuildCaches | ||||||
|  | } from './memberCache'; | ||||||
							
								
								
									
										525
									
								
								functions/src/memberCache/memberCache.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										525
									
								
								functions/src/memberCache/memberCache.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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<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 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<string>(); | ||||||
|  |        | ||||||
|  |       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<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 { | ||||||
|  |           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<CacheEntry> { | ||||||
|  |   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<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 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)); | ||||||
|  |   } | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user