notification-bug-fix #88
| @ -33,6 +33,15 @@ interface PaymentData { | ||||
|   discount?: number; | ||||
| } | ||||
| 
 | ||||
| interface PersonalTrainerAssign { | ||||
|   id: string; | ||||
|   ownerId: string; | ||||
|   trainerId?: string; | ||||
|   clientId: string; | ||||
|   membershipId: string; | ||||
|   gymId: string; | ||||
| } | ||||
| 
 | ||||
| export const checkExpiredMemberships = onSchedule( | ||||
|   { | ||||
|     schedule: "*/5 * * * *", | ||||
| @ -367,6 +376,66 @@ function calculateRenewalDateFromPayment( | ||||
|   return renewalDate; | ||||
| } | ||||
| 
 | ||||
| async function getTrainerAssignmentsForMembership( | ||||
|   membershipId: string | ||||
| ): Promise<PersonalTrainerAssign[]> { | ||||
|   try { | ||||
|     const querySnapshot = await app | ||||
|       .firestore() | ||||
|       .collection("personal_trainer_assignments") | ||||
|       .where("membershipId", "==", membershipId) | ||||
|       .get(); | ||||
| 
 | ||||
|     return querySnapshot.docs.map((doc) => ({ | ||||
|       id: doc.id, | ||||
|       ...doc.data(), | ||||
|     })) as PersonalTrainerAssign[]; | ||||
|   } catch (error) { | ||||
|     logger.error( | ||||
|       `Error getting trainer assignments for membership ${membershipId}:`, | ||||
|       error | ||||
|     ); | ||||
|     return []; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function getTrainerName(trainerId: string): Promise<string> { | ||||
|   try { | ||||
|     const doc = await app | ||||
|       .firestore() | ||||
|       .collection("trainer_profiles") | ||||
|       .doc(trainerId) | ||||
|       .get(); | ||||
|      | ||||
|     if (!doc.exists) { | ||||
|       const userDoc = await app | ||||
|         .firestore() | ||||
|         .collection("users") | ||||
|         .doc(trainerId) | ||||
|         .get(); | ||||
|        | ||||
|       if (userDoc.exists) { | ||||
|         const userData = userDoc.data(); | ||||
|         return userData?.name || userData?.displayName || "Unknown Trainer"; | ||||
|       } | ||||
|       return "Unknown Trainer"; | ||||
|     } | ||||
| 
 | ||||
|     const data = doc.data(); | ||||
|     const fields = data?.fields; | ||||
|     if (fields) { | ||||
|       const firstName = fields["first-name"] || ""; | ||||
|       const lastName = fields["last-name"] || ""; | ||||
|       return `${firstName} ${lastName}`.trim() || "Unknown Trainer"; | ||||
|     } | ||||
|      | ||||
|     return data?.name || data?.displayName || "Unknown Trainer"; | ||||
|   } catch (error) { | ||||
|     logger.error(`Error getting trainer name for ${trainerId}:`, error); | ||||
|     return "Unknown Trainer"; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function processExpiredMembership( | ||||
|   membershipId: string, | ||||
|   membershipData: MembershipData | ||||
| @ -378,7 +447,10 @@ async function processExpiredMembership( | ||||
|     }); | ||||
| 
 | ||||
|     logger.info(`Marked membership ${membershipId} as EXPIRED`); | ||||
|      | ||||
|     await sendPlanExpiredNotification(membershipId, membershipData); | ||||
|      | ||||
|     await sendTrainerNotifications(membershipId, membershipData, "expired"); | ||||
|   } catch (error) { | ||||
|     logger.error(`Error processing membership ${membershipId}:`, error); | ||||
|   } | ||||
| @ -390,7 +462,10 @@ async function processExpiringMembership( | ||||
| ): Promise<void> { | ||||
|   try { | ||||
|     logger.info(`Processing expiring membership ${membershipId}`); | ||||
|      | ||||
|     await sendPlanExpiringNotification(membershipId, membershipData); | ||||
|      | ||||
|     await sendTrainerNotifications(membershipId, membershipData, "expiring"); | ||||
|   } catch (error) { | ||||
|     logger.error( | ||||
|       `Error processing expiring membership ${membershipId}:`, | ||||
| @ -399,6 +474,121 @@ async function processExpiringMembership( | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function sendTrainerNotifications( | ||||
|   membershipId: string, | ||||
|   membershipData: MembershipData, | ||||
|   notificationType: "expired" | "expiring" | ||||
| ): Promise<void> { | ||||
|   try { | ||||
|     const trainerAssignments = await getTrainerAssignmentsForMembership(membershipId); | ||||
|      | ||||
|     if (trainerAssignments.length === 0) { | ||||
|       logger.info(`No trainer assignments found for membership ${membershipId}`); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const clientName = await getClientName(membershipId, membershipData.userId); | ||||
|     const gymName = await getGymName(membershipData.gymId); | ||||
| 
 | ||||
|     let expiryDate: Date | undefined; | ||||
|     let formattedDate = "Unknown Date"; | ||||
|     let daysUntilExpiry = 0; | ||||
| 
 | ||||
|     const payments = await getPaymentsForMembership(membershipId); | ||||
|     if (payments.length > 0) { | ||||
|       const latestPayment = payments[0]; | ||||
|       expiryDate = calculateRenewalDateFromPayment( | ||||
|         membershipData.subscription, | ||||
|         latestPayment.dateTimestamp | ||||
|       ); | ||||
|       formattedDate = expiryDate.toLocaleDateString("en-US", { | ||||
|         year: "numeric", | ||||
|         month: "long", | ||||
|         day: "numeric", | ||||
|       }); | ||||
| 
 | ||||
|       if (notificationType === "expiring") { | ||||
|         const now = new Date(); | ||||
|         const timeDiff = expiryDate.getTime() - now.getTime(); | ||||
|         daysUntilExpiry = Math.ceil(timeDiff / (1000 * 3600 * 24)); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     for (const assignment of trainerAssignments) { | ||||
|       if (!assignment.trainerId) continue; | ||||
| 
 | ||||
|       try { | ||||
|         const trainerName = await getTrainerName(assignment.trainerId); | ||||
|          | ||||
|         const notifType = notificationType === "expired" ? "trainer_client_plan_expired" : "trainer_client_plan_expiring"; | ||||
|         const existing = await app | ||||
|           .firestore() | ||||
|           .collection("notifications") | ||||
|           .where("type", "==", notifType) | ||||
|           .where("recipientId", "==", assignment.trainerId) | ||||
|           .where("data.membershipId", "==", membershipId) | ||||
|           .where( | ||||
|             "data.expiryDate", | ||||
|             "==", | ||||
|             expiryDate | ||||
|               ? admin.firestore.Timestamp.fromDate(expiryDate) | ||||
|               : admin.firestore.Timestamp.fromDate(new Date()) | ||||
|           ) | ||||
|           .limit(1) | ||||
|           .get(); | ||||
| 
 | ||||
|         if (!existing.empty) { | ||||
|           logger.info( | ||||
|             `${notificationType} notification already sent to trainer ${assignment.trainerId} for membership ${membershipId}, skipping...` | ||||
|           ); | ||||
|           continue; | ||||
|         } | ||||
| 
 | ||||
|         const notificationData: any = { | ||||
|           senderId: "system", | ||||
|           recipientId: assignment.trainerId, | ||||
|           type: notifType, | ||||
|           notificationSent: false, | ||||
|           timestamp: admin.firestore.FieldValue.serverTimestamp(), | ||||
|           read: false, | ||||
|           readBy: [], | ||||
|           data: { | ||||
|             planName: membershipData.subscription?.name || "Unknown Plan", | ||||
|             clientName, | ||||
|             membershipId, | ||||
|             gymName, | ||||
|             assignmentId: assignment.id, | ||||
|             formattedExpiryDate: formattedDate, | ||||
|             expiryDate: expiryDate | ||||
|               ? admin.firestore.Timestamp.fromDate(expiryDate) | ||||
|               : admin.firestore.Timestamp.fromDate(new Date()), | ||||
|           }, | ||||
|         }; | ||||
| 
 | ||||
|         if (notificationType === "expiring") { | ||||
|           notificationData.data.daysUntilExpiry = daysUntilExpiry; | ||||
|         } | ||||
| 
 | ||||
|         await app.firestore().collection("notifications").add(notificationData); | ||||
| 
 | ||||
|         logger.info( | ||||
|           `${notificationType} notification sent to trainer ${assignment.trainerId} (${trainerName}) for client ${clientName}'s membership ${membershipId}` | ||||
|         ); | ||||
|       } catch (trainerError) { | ||||
|         logger.error( | ||||
|           `Error sending notification to trainer ${assignment.trainerId} for membership ${membershipId}:`, | ||||
|           trainerError | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|   } catch (error) { | ||||
|     logger.error( | ||||
|       `Error sending trainer notifications for membership ${membershipId}:`, | ||||
|       error | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function sendPlanExpiredNotification( | ||||
|   membershipId: string, | ||||
|   membershipData: MembershipData | ||||
| @ -609,4 +799,4 @@ async function getGymName(gymId: string): Promise<string> { | ||||
|     logger.error(`Error getting gym name for gym ${gymId}:`, error); | ||||
|     return "Unknown Gym"; | ||||
|   } | ||||
| } | ||||
| } | ||||
| @ -180,7 +180,7 @@ function prepareNotificationMessage( | ||||
|     case "trainer_response": | ||||
|       title = | ||||
|         notification.data?.title || | ||||
|         (notification.data?.status === "Accepted" | ||||
|         (notification.data?.status === "accepted" | ||||
|           ? "Trainer Request Accepted" | ||||
|           : "Trainer Request Update"); | ||||
|       body = | ||||
| @ -240,14 +240,28 @@ function prepareNotificationMessage( | ||||
|       title = notification.data?.title || "Plan Expired"; | ||||
|       body = | ||||
|         notification.data?.message || | ||||
|         `${notification.data?.clientName}'s membership subscription for ${notification.data?.planName} has expired.`; | ||||
|         `${notification.data?.clientName}'s membership  for ${notification.data?.planName} has expired.`; | ||||
|       break; | ||||
| 
 | ||||
|     case "plan_expiring_soon": | ||||
|       title = notification.data?.title || "Plan Expiring Soon"; | ||||
|       body = | ||||
|         notification.data?.message || | ||||
|         `${notification.data?.clientName}'s membership subscription for ${notification.data?.planName} will expire on ${notification.data?.formattedExpiryDate}.`; | ||||
|         `${notification.data?.clientName}'s membership  for ${notification.data?.planName} will expire on ${notification.data?.formattedExpiryDate}.`; | ||||
|       break; | ||||
| 
 | ||||
|     case "trainer_client_plan_expired": | ||||
|       title = notification.data?.title || "Client Plan Expired"; | ||||
|       body = | ||||
|         notification.data?.message || | ||||
|         `${notification.data?.clientName}'s membership for ${notification.data?.planName} has expired.`; | ||||
|       break; | ||||
| 
 | ||||
|     case "trainer_client_plan_expiring": | ||||
|       title = notification.data?.title || "Client Plan Expiring Soon"; | ||||
|       body = | ||||
|         notification.data?.message || | ||||
|         `${notification.data?.clientName}'s membership for ${notification.data?.planName} will expire on ${notification.data?.formattedExpiryDate}.`; | ||||
|       break; | ||||
| 
 | ||||
|     case "schedule_update": | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user