feature/fitlien-828 #118
| @ -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'; | ||||
| export { getMemberCache, updateMemberCache } from "./memberCache"; | ||||
|  | ||||
| @ -1,7 +1 @@ | ||||
| export { | ||||
|   generateMemberCache, | ||||
|   updateTrainerAssignmentCache, | ||||
|   getCachedMembers, | ||||
|   rebuildGymCache, | ||||
|   batchRebuildCaches | ||||
| } from './memberCache'; | ||||
| export { getMemberCache, updateMemberCache } from "./memberCache"; | ||||
|  | ||||
| @ -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<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 || []; | ||||
| 
 | ||||
|         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<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(); | ||||
| async function fetchAllMembers(gymId: string): Promise<CacheEntry[]> { | ||||
|   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<void> { | ||||
|   logger.info(`Updating ${membershipIds.length} specific members`); | ||||
| 
 | ||||
|   const existingMembersMap = new Map<string, number>(); | ||||
|   existingMembers.forEach((member, index) => { | ||||
|     existingMembersMap.set(member.membershipId, index); | ||||
|   }); | ||||
| 
 | ||||
|   const batchSize = 10; | ||||
|   for (let i = 0; i < membershipIds.length; i += batchSize) { | ||||
|     const batch = membershipIds.slice(i, i + batchSize); | ||||
| 
 | ||||
|     const membershipDocs = await Promise.all( | ||||
|       batch.map(async (membershipId) => { | ||||
|         try { | ||||
|           const 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<void> { | ||||
|           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<CacheEntry> { | ||||
| async function saveCacheToStorage( | ||||
|   gymId: string, | ||||
|   jsonData: JsonCacheData | ||||
| ): Promise<void> { | ||||
|   const fileName = `${CACHE_FOLDER}/${gymId}.json`; | ||||
|   const file = app.storage().bucket().file(fileName); | ||||
| 
 | ||||
|   await file.save(JSON.stringify(jsonData, null, 2), { | ||||
|     metadata: { | ||||
|       contentType: "application/json", | ||||
|       metadata: { | ||||
|         gymId: gymId, | ||||
|         totalMembers: jsonData.totalMembers.toString(), | ||||
|         generatedAt: jsonData.lastUpdated, | ||||
|         cacheVersion: jsonData.cacheVersion, | ||||
|       }, | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function 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) | ||||
|         .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<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; | ||||
|   } | ||||
| } | ||||
| function calculateRenewalDateTimeFromPayment( | ||||
|   subscription: { frequency?: string }, | ||||
|   paymentDate: Date | ||||
| ): Date { | ||||
|   const frequency = subscription.frequency || "Monthly"; | ||||
| 
 | ||||
| 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()); | ||||
|     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); | ||||
|   } | ||||
| } | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user