feature/fitlien-828 #119
@ -6,26 +6,25 @@ setGlobalOptions({
|
|||||||
timeoutSeconds: 540,
|
timeoutSeconds: 540,
|
||||||
minInstances: 0,
|
minInstances: 0,
|
||||||
maxInstances: 10,
|
maxInstances: 10,
|
||||||
concurrency: 80
|
concurrency: 80,
|
||||||
});
|
});
|
||||||
|
|
||||||
export * from './shared/config';
|
export * from "./shared/config";
|
||||||
export { sendEmailSES } from './email';
|
export { sendEmailSES } from "./email";
|
||||||
export { sendSMSMessage } from './sms';
|
export { sendSMSMessage } from "./sms";
|
||||||
export { accessFile } from './storage';
|
export { accessFile } from "./storage";
|
||||||
export { processNotificationOnCreate, checkExpiredMemberships } from './notifications';
|
|
||||||
export * from './payments';
|
|
||||||
export { getPlaceDetails, getPlacesAutocomplete } from './places';
|
|
||||||
export { registerClient } from './users';
|
|
||||||
export {
|
export {
|
||||||
esslGetUserDetails, esslUpdateUser,
|
processNotificationOnCreate,
|
||||||
esslDeleteUser, esslGetEmployeePunchLogs
|
checkExpiredMemberships,
|
||||||
} from './dooraccess';
|
} from "./notifications";
|
||||||
|
export * from "./payments";
|
||||||
|
export { getPlaceDetails, getPlacesAutocomplete } from "./places";
|
||||||
|
export { registerClient } from "./users";
|
||||||
|
export {
|
||||||
|
esslGetUserDetails,
|
||||||
|
esslUpdateUser,
|
||||||
|
esslDeleteUser,
|
||||||
|
esslGetEmployeePunchLogs,
|
||||||
|
} from "./dooraccess";
|
||||||
|
|
||||||
export {
|
export { getMemberCache, updateMemberCache } from "./memberCache";
|
||||||
generateMemberCache,
|
|
||||||
updateTrainerAssignmentCache,
|
|
||||||
getCachedMembers,
|
|
||||||
rebuildGymCache,
|
|
||||||
batchRebuildCaches
|
|
||||||
} from './memberCache';
|
|
||||||
|
|||||||
@ -1,7 +1 @@
|
|||||||
export {
|
export { getMemberCache, updateMemberCache } from "./memberCache";
|
||||||
generateMemberCache,
|
|
||||||
updateTrainerAssignmentCache,
|
|
||||||
getCachedMembers,
|
|
||||||
rebuildGymCache,
|
|
||||||
batchRebuildCaches
|
|
||||||
} from './memberCache';
|
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { onDocumentWritten } from "firebase-functions/v2/firestore";
|
import { onRequest } from "firebase-functions/v2/https";
|
||||||
import { onCall, HttpsError } from "firebase-functions/v2/https";
|
|
||||||
import { getLogger, getAdmin } from "../shared/config";
|
import { getLogger, getAdmin } from "../shared/config";
|
||||||
import * as admin from "firebase-admin";
|
import * as admin from "firebase-admin";
|
||||||
|
|
||||||
const app = getAdmin();
|
const app = getAdmin();
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
|
|
||||||
const CACHE_FOLDER = 'gym_member_cache';
|
const CACHE_FOLDER = "gym_member_cache";
|
||||||
|
|
||||||
interface MembershipData {
|
interface MembershipData {
|
||||||
gymId?: string;
|
gymId?: string;
|
||||||
@ -67,190 +66,184 @@ interface JsonCacheData {
|
|||||||
cacheVersion: string;
|
cacheVersion: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateMemberCache = onDocumentWritten(
|
export const getMemberCache = onRequest(
|
||||||
{
|
{
|
||||||
region: "#{SERVICES_RGN}#",
|
region: "#{SERVICES_RGN}#",
|
||||||
document: "memberships/{membershipId}",
|
cors: true,
|
||||||
},
|
},
|
||||||
async (event) => {
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const membershipId = event.params.membershipId;
|
if (req.method === "OPTIONS") {
|
||||||
const membershipData = event.data?.after?.exists
|
res.set("Access-Control-Allow-Origin", "*");
|
||||||
? event.data.after.data() as MembershipData
|
res.set("Access-Control-Allow-Methods", "GET, POST");
|
||||||
: null;
|
res.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||||
|
res.status(204).send("");
|
||||||
if (!membershipData) {
|
|
||||||
await rebuildGymCacheFromDeletion(membershipId);
|
|
||||||
return;
|
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) {
|
if (!gymId) {
|
||||||
logger.warn(`No gymId found for membership ${membershipId}`);
|
res.status(400).json({ error: "gymId is required" });
|
||||||
return;
|
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 fileName = `${CACHE_FOLDER}/${gymId}.json`;
|
||||||
const file = app.storage().bucket().file(fileName);
|
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 {
|
try {
|
||||||
const [fileBuffer] = await file.download();
|
const [fileBuffer] = await file.download();
|
||||||
const jsonData: JsonCacheData = JSON.parse(fileBuffer.toString());
|
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) {
|
} catch (error) {
|
||||||
logger.error(`Error reading cache file for gym ${gymId}:`, error);
|
logger.error(`Error reading cache file for gym ${gymId}:`, error);
|
||||||
|
res.status(404).json({
|
||||||
await rebuildGymCachee(gymId);
|
error: "Cache not found for this gym. Please update cache first.",
|
||||||
const [fileBuffer] = await file.download();
|
gymId,
|
||||||
const jsonData: JsonCacheData = JSON.parse(fileBuffer.toString());
|
members: [],
|
||||||
return jsonData.members || [];
|
totalMembers: 0,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
cacheVersion: "2.0",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting cached members:', error);
|
logger.error("Error getting member cache:", error);
|
||||||
throw new HttpsError('internal', 'Error retrieving cached data');
|
res.status(500).json({ error: "Error retrieving cached data" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const rebuildGymCache = onCall(
|
export const updateMemberCache = onRequest(
|
||||||
{
|
{
|
||||||
region: "#{SERVICES_RGN}#",
|
region: "#{SERVICES_RGN}#",
|
||||||
|
cors: true,
|
||||||
|
timeoutSeconds: 540,
|
||||||
},
|
},
|
||||||
async (request) => {
|
async (req, res) => {
|
||||||
const { gymId } = request.data;
|
try {
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
res.set("Access-Control-Allow-Origin", "*");
|
||||||
|
res.set("Access-Control-Allow-Methods", "POST");
|
||||||
|
res.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||||
|
res.status(204).send("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set("Access-Control-Allow-Origin", "*");
|
||||||
|
res.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||||
|
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
res.status(405).json({ error: "Method not allowed. Use POST." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { gymId, membershipIds } = req.body;
|
||||||
|
let incrementalUpdate = req.body.incrementalUpdate || false;
|
||||||
|
|
||||||
if (!gymId) {
|
if (!gymId) {
|
||||||
throw new HttpsError('invalid-argument', 'gymId is required');
|
res.status(400).json({ error: "gymId is required" });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await rebuildGymCachee(gymId);
|
logger.info(
|
||||||
return { success: true, message: `JSON cache rebuilt for gym ${gymId}` };
|
`Starting cache ${
|
||||||
}
|
incrementalUpdate ? "incremental update" : "full refresh"
|
||||||
|
} for gym: ${gymId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
export const batchRebuildCaches = onCall(
|
let members: CacheEntry[] = [];
|
||||||
{
|
let existingData: JsonCacheData | null = null;
|
||||||
region: "#{SERVICES_RGN}#",
|
|
||||||
},
|
if (incrementalUpdate) {
|
||||||
async (request) => {
|
|
||||||
try {
|
try {
|
||||||
const { gymIds } = request.data;
|
const fileName = `${CACHE_FOLDER}/${gymId}.json`;
|
||||||
|
const file = app.storage().bucket().file(fileName);
|
||||||
if (!gymIds || !Array.isArray(gymIds)) {
|
const [fileBuffer] = await file.download();
|
||||||
throw new HttpsError('invalid-argument', 'gymIds array is required');
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results: Array<{ gymId: string; status: string; error?: string }> = [];
|
|
||||||
|
|
||||||
for (const gymId of gymIds) {
|
|
||||||
try {
|
|
||||||
await rebuildGymCachee(gymId);
|
|
||||||
results.push({ gymId, status: 'success' });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.warn(
|
||||||
results.push({ gymId, status: 'error', error: errorMessage });
|
`Could not load existing cache for incremental update, performing full refresh: ${error}`
|
||||||
|
);
|
||||||
|
incrementalUpdate = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Batch rebuild completed for ${gymIds.length} gyms`);
|
if (incrementalUpdate && membershipIds && Array.isArray(membershipIds)) {
|
||||||
return { results };
|
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) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.error("Error updating member cache:", error);
|
||||||
throw new HttpsError('internal', errorMessage);
|
res.status(500).json({
|
||||||
|
error: "Error updating cache",
|
||||||
|
details: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
async function rebuildGymCachee(gymId: string): Promise<void> {
|
async function fetchAllMembers(gymId: string): Promise<CacheEntry[]> {
|
||||||
try {
|
const members: CacheEntry[] = [];
|
||||||
logger.info(`Starting JSON cache rebuild for gym: ${gymId}`);
|
|
||||||
|
|
||||||
const membershipsSnapshot = await app
|
const membershipsSnapshot = await app
|
||||||
.firestore()
|
.firestore()
|
||||||
.collection('memberships')
|
.collection("memberships")
|
||||||
.where('gymId', '==', gymId)
|
.where("gymId", "==", gymId)
|
||||||
.orderBy('createdAt', 'desc')
|
.orderBy("createdAt", "desc")
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
const members: CacheEntry[] = [];
|
|
||||||
const batchSize = 10;
|
const batchSize = 10;
|
||||||
|
|
||||||
for (let i = 0; i < membershipsSnapshot.docs.length; i += batchSize) {
|
for (let i = 0; i < membershipsSnapshot.docs.length; i += batchSize) {
|
||||||
@ -260,7 +253,9 @@ async function rebuildGymCachee(gymId: string): Promise<void> {
|
|||||||
const membershipData = doc.data() as MembershipData;
|
const membershipData = doc.data() as MembershipData;
|
||||||
const userId = membershipData.userId;
|
const userId = membershipData.userId;
|
||||||
if (!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 null;
|
||||||
}
|
}
|
||||||
return await generateCacheEntry(userId, doc.id, membershipData);
|
return await generateCacheEntry(userId, doc.id, membershipData);
|
||||||
@ -271,57 +266,126 @@ async function rebuildGymCachee(gymId: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const batchResults = await Promise.all(batchPromises);
|
const batchResults = await Promise.all(batchPromises);
|
||||||
const validResults = batchResults.filter((member): member is CacheEntry => member !== null);
|
const validResults = batchResults.filter(
|
||||||
|
(member): member is CacheEntry => member !== null
|
||||||
|
);
|
||||||
members.push(...validResults);
|
members.push(...validResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsonData: JsonCacheData = {
|
return members;
|
||||||
gymId,
|
}
|
||||||
members,
|
|
||||||
totalMembers: members.length,
|
|
||||||
lastUpdated: new Date().toISOString(),
|
|
||||||
cacheVersion: '1.0'
|
|
||||||
};
|
|
||||||
|
|
||||||
|
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 doc = await app
|
||||||
|
.firestore()
|
||||||
|
.collection("memberships")
|
||||||
|
.doc(membershipId)
|
||||||
|
.get();
|
||||||
|
return doc.exists ? { id: doc.id, data: doc.data() } : null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error fetching membership ${membershipId}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const batchPromises = membershipDocs
|
||||||
|
.filter((doc): doc is { id: string; data: any } => doc !== null)
|
||||||
|
.map(async (doc) => {
|
||||||
|
try {
|
||||||
|
const membershipData = doc.data as MembershipData;
|
||||||
|
const userId = membershipData.userId;
|
||||||
|
if (!userId) {
|
||||||
|
logger.warn(
|
||||||
|
`Skipping member with ID ${doc.id} due to missing userId`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await generateCacheEntry(userId, doc.id, membershipData);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error processing member ${doc.id}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchResults = await Promise.all(batchPromises);
|
||||||
|
|
||||||
|
batchResults.forEach((updatedMember) => {
|
||||||
|
if (updatedMember) {
|
||||||
|
const existingIndex = existingMembersMap.get(
|
||||||
|
updatedMember.membershipId
|
||||||
|
);
|
||||||
|
if (existingIndex !== undefined) {
|
||||||
|
existingMembers[existingIndex] = updatedMember;
|
||||||
|
} else {
|
||||||
|
existingMembers.push(updatedMember);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCacheToStorage(
|
||||||
|
gymId: string,
|
||||||
|
jsonData: JsonCacheData
|
||||||
|
): Promise<void> {
|
||||||
const fileName = `${CACHE_FOLDER}/${gymId}.json`;
|
const fileName = `${CACHE_FOLDER}/${gymId}.json`;
|
||||||
const file = app.storage().bucket().file(fileName);
|
const file = app.storage().bucket().file(fileName);
|
||||||
|
|
||||||
await file.save(JSON.stringify(jsonData, null, 2), {
|
await file.save(JSON.stringify(jsonData, null, 2), {
|
||||||
metadata: {
|
metadata: {
|
||||||
contentType: 'application/json',
|
contentType: "application/json",
|
||||||
metadata: {
|
metadata: {
|
||||||
gymId: gymId,
|
gymId: gymId,
|
||||||
totalMembers: members.length.toString(),
|
totalMembers: jsonData.totalMembers.toString(),
|
||||||
generatedAt: new Date().toISOString()
|
generatedAt: jsonData.lastUpdated,
|
||||||
}
|
cacheVersion: jsonData.cacheVersion,
|
||||||
}
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
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 generateCacheEntry(
|
||||||
|
userId: string,
|
||||||
|
membershipId: string,
|
||||||
|
membershipData: MembershipData
|
||||||
|
): Promise<CacheEntry> {
|
||||||
try {
|
try {
|
||||||
let fields: { [key: string]: string } = {};
|
let fields: { [key: string]: string } = {};
|
||||||
try {
|
try {
|
||||||
const clientFieldsSnapshot = await app
|
const clientFieldsSnapshot = await app
|
||||||
.firestore()
|
.firestore()
|
||||||
.collection('client_profiles')
|
.collection("client_profiles")
|
||||||
.where('uid', '==', userId)
|
.where("uid", "==", userId)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (!clientFieldsSnapshot.empty) {
|
if (!clientFieldsSnapshot.empty) {
|
||||||
const fieldDoc = clientFieldsSnapshot.docs[0];
|
const fieldDoc = clientFieldsSnapshot.docs[0];
|
||||||
const fieldData = fieldDoc.data() as ClientFields;
|
const fieldData = fieldDoc.data() as ClientFields;
|
||||||
fields = {
|
fields = {
|
||||||
'first-name': fieldData.fields?.['first-name'] || '',
|
"first-name": fieldData.fields?.["first-name"] || "",
|
||||||
'email': fieldData.fields?.['email'] || '',
|
"last-name": fieldData.fields?.["last-name"] || "",
|
||||||
'phone-number': fieldData.fields?.['phone-number'] || '',
|
"email": fieldData.fields?.["email"] || "",
|
||||||
'alternate-contact': fieldData.fields?.['alternate-contact'] || '',
|
"phone-number": fieldData.fields?.["phone-number"] || "",
|
||||||
|
"alternate-contact": fieldData.fields?.["alternate-contact"] || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -335,9 +399,9 @@ async function generateCacheEntry(userId: string, membershipId: string, membersh
|
|||||||
try {
|
try {
|
||||||
const paymentsSnapshot = await app
|
const paymentsSnapshot = await app
|
||||||
.firestore()
|
.firestore()
|
||||||
.collection('membership_payments')
|
.collection("membership_payments")
|
||||||
.where('membershipId', '==', membershipId)
|
.where("membershipId", "==", membershipId)
|
||||||
.orderBy('dateTimestamp', 'desc')
|
.orderBy("dateTimestamp", "desc")
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
@ -345,17 +409,25 @@ async function generateCacheEntry(userId: string, membershipId: string, membersh
|
|||||||
const latestPayment = paymentsSnapshot.docs[0].data() as PaymentData;
|
const latestPayment = paymentsSnapshot.docs[0].data() as PaymentData;
|
||||||
if (latestPayment.dateTimestamp) {
|
if (latestPayment.dateTimestamp) {
|
||||||
const paymentDate = latestPayment.dateTimestamp.toDate();
|
const paymentDate = latestPayment.dateTimestamp.toDate();
|
||||||
renewalDate = calculateRenewalDateTimeFromPayment(membershipData.subscription, paymentDate);
|
renewalDate = calculateRenewalDateTimeFromPayment(
|
||||||
|
membershipData.subscription,
|
||||||
|
paymentDate
|
||||||
|
);
|
||||||
|
|
||||||
if (renewalDate && membershipData.status === 'ACTIVE') {
|
if (renewalDate && membershipData.status === "ACTIVE") {
|
||||||
const now = new Date();
|
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;
|
daysUntilExpiry = difference > 0 ? difference : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error getting renewal date for membership ${membershipId}:`, error);
|
logger.error(
|
||||||
|
`Error getting renewal date for membership ${membershipId}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,19 +442,22 @@ async function generateCacheEntry(userId: string, membershipId: string, membersh
|
|||||||
try {
|
try {
|
||||||
const assignmentsSnapshot = await app
|
const assignmentsSnapshot = await app
|
||||||
.firestore()
|
.firestore()
|
||||||
.collection('personal_trainer_assignments')
|
.collection("personal_trainer_assignments")
|
||||||
.where('membershipId', '==', membershipId)
|
.where("membershipId", "==", membershipId)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
assignmentsSnapshot.docs.forEach(doc => {
|
assignmentsSnapshot.docs.forEach((doc) => {
|
||||||
const data = doc.data() as TrainerAssignment;
|
const data = doc.data() as TrainerAssignment;
|
||||||
if (data.timeSlot && Array.isArray(data.timeSlot)) {
|
if (data.timeSlot && Array.isArray(data.timeSlot)) {
|
||||||
timeSlots.push(...data.timeSlot);
|
timeSlots.push(...data.timeSlot);
|
||||||
}
|
}
|
||||||
trainerAssignments.push({ id: doc.id });
|
trainerAssignments.push({ id: doc.id, ...data });
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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,
|
userId,
|
||||||
membershipId,
|
membershipId,
|
||||||
memberData: {
|
memberData: {
|
||||||
status: membershipData.status,
|
...membershipData,
|
||||||
subscription: {
|
|
||||||
hasPersonalTraining: membershipData.subscription?.hasPersonalTraining,
|
|
||||||
frequency: membershipData.subscription?.frequency,
|
|
||||||
},
|
|
||||||
remainingAmount: membershipData.remainingAmount,
|
|
||||||
isPartialPayment: membershipData.isPartialPayment,
|
|
||||||
hasPartialPayment,
|
hasPartialPayment,
|
||||||
daysUntilExpiry,
|
daysUntilExpiry,
|
||||||
createdAt: membershipData.createdAt ? membershipData.createdAt.toDate().toISOString() : null,
|
createdAt: membershipData.createdAt
|
||||||
updatedAt: membershipData.updatedAt ? membershipData.updatedAt.toDate().toISOString() : null
|
? membershipData.createdAt.toDate().toISOString()
|
||||||
|
: null,
|
||||||
|
updatedAt: membershipData.updatedAt
|
||||||
|
? membershipData.updatedAt.toDate().toISOString()
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
fields,
|
fields,
|
||||||
renewalDate: renewalDate ? renewalDate.toISOString() : null,
|
renewalDate: renewalDate ? renewalDate.toISOString() : null,
|
||||||
@ -411,71 +484,43 @@ async function generateCacheEntry(userId: string, membershipId: string, membersh
|
|||||||
|
|
||||||
return cacheEntry;
|
return cacheEntry;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error generating cache entry:', error);
|
logger.error("Error generating cache entry:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getGymIdFromMembershipId(membershipId: string): Promise<string | null> {
|
function calculateRenewalDateTimeFromPayment(
|
||||||
try {
|
subscription: { frequency?: string },
|
||||||
const membershipDoc = await app.firestore().collection('memberships').doc(membershipId).get();
|
paymentDate: Date
|
||||||
if (membershipDoc.exists) {
|
): Date {
|
||||||
const data = membershipDoc.data() as MembershipData;
|
const frequency = subscription.frequency || "Monthly";
|
||||||
return data.gymId || null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error getting gym ID for membership ${membershipId}:`, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rebuildGymCacheFromDeletion(membershipId: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
const [files] = await app.storage().bucket().getFiles({
|
|
||||||
prefix: `${CACHE_FOLDER}/`,
|
|
||||||
delimiter: '/'
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
if (file.name.endsWith('.json')) {
|
|
||||||
try {
|
|
||||||
const [fileBuffer] = await file.download();
|
|
||||||
const jsonData: JsonCacheData = JSON.parse(fileBuffer.toString());
|
|
||||||
|
|
||||||
const memberExists = jsonData.members?.some(member =>
|
|
||||||
member.membershipId === membershipId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (memberExists) {
|
|
||||||
const gymId = file.name.replace(`${CACHE_FOLDER}/`, '').replace('.json', '');
|
|
||||||
logger.info(`Rebuilding cache for gym ${gymId} after member ${membershipId} deletion`);
|
|
||||||
await rebuildGymCachee(gymId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error checking cache file ${file.name}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error handling membership deletion:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateRenewalDateTimeFromPayment(subscription: { frequency?: string }, paymentDate: Date): Date {
|
|
||||||
const frequency = subscription.frequency || 'Monthly';
|
|
||||||
|
|
||||||
switch (frequency) {
|
switch (frequency) {
|
||||||
case 'Monthly':
|
case "Monthly":
|
||||||
return new Date(paymentDate.getFullYear(), paymentDate.getMonth() + 1, paymentDate.getDate());
|
return new Date(
|
||||||
case 'Quarterly':
|
paymentDate.getFullYear(),
|
||||||
return new Date(paymentDate.getFullYear(), paymentDate.getMonth() + 3, paymentDate.getDate());
|
paymentDate.getMonth() + 1,
|
||||||
case 'Half-yearly':
|
paymentDate.getDate()
|
||||||
return new Date(paymentDate.getFullYear(), paymentDate.getMonth() + 6, paymentDate.getDate());
|
);
|
||||||
case 'Yearly':
|
case "Quarterly":
|
||||||
return new Date(paymentDate.getFullYear() + 1, paymentDate.getMonth(), paymentDate.getDate());
|
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:
|
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