@blackcode_sa/metaestetics-api 1.7.12 → 1.7.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,6 +40,7 @@ __export(index_exports, {
40
40
  CalendarAdminService: () => CalendarAdminService,
41
41
  ClinicAggregationService: () => ClinicAggregationService,
42
42
  DocumentManagerAdminService: () => DocumentManagerAdminService,
43
+ FilledFormsAggregationService: () => FilledFormsAggregationService,
43
44
  Logger: () => Logger,
44
45
  MediaType: () => MediaType,
45
46
  NOTIFICATIONS_COLLECTION: () => NOTIFICATIONS_COLLECTION,
@@ -56,6 +57,7 @@ __export(index_exports, {
56
57
  PractitionerInviteMailingService: () => PractitionerInviteMailingService,
57
58
  PractitionerTokenStatus: () => PractitionerTokenStatus,
58
59
  ProcedureAggregationService: () => ProcedureAggregationService,
60
+ ReviewsAggregationService: () => ReviewsAggregationService,
59
61
  UserRole: () => UserRole
60
62
  });
61
63
  module.exports = __toCommonJS(index_exports);
@@ -203,9 +205,9 @@ var Logger = class {
203
205
 
204
206
  // src/admin/notifications/notifications.admin.ts
205
207
  var NotificationsAdmin = class {
206
- constructor(firestore13) {
208
+ constructor(firestore15) {
207
209
  this.expo = new import_expo_server_sdk.Expo();
208
- this.db = firestore13 || admin.firestore();
210
+ this.db = firestore15 || admin.firestore();
209
211
  }
210
212
  /**
211
213
  * Dohvata notifikaciju po ID-u
@@ -924,8 +926,8 @@ var ClinicAggregationService = class {
924
926
  * Constructor for ClinicAggregationService.
925
927
  * @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
926
928
  */
927
- constructor(firestore13) {
928
- this.db = firestore13 || admin3.firestore();
929
+ constructor(firestore15) {
930
+ this.db = firestore15 || admin3.firestore();
929
931
  }
930
932
  /**
931
933
  * Adds clinic information to a clinic group when a new clinic is created
@@ -1399,8 +1401,8 @@ var ClinicAggregationService = class {
1399
1401
  var admin4 = __toESM(require("firebase-admin"));
1400
1402
  var CALENDAR_SUBCOLLECTION_ID2 = "calendar";
1401
1403
  var PractitionerAggregationService = class {
1402
- constructor(firestore13) {
1403
- this.db = firestore13 || admin4.firestore();
1404
+ constructor(firestore15) {
1405
+ this.db = firestore15 || admin4.firestore();
1404
1406
  }
1405
1407
  /**
1406
1408
  * Adds practitioner information to a clinic when a new practitioner is created
@@ -1735,8 +1737,8 @@ var PractitionerAggregationService = class {
1735
1737
  var admin5 = __toESM(require("firebase-admin"));
1736
1738
  var CALENDAR_SUBCOLLECTION_ID3 = "calendar";
1737
1739
  var ProcedureAggregationService = class {
1738
- constructor(firestore13) {
1739
- this.db = firestore13 || admin5.firestore();
1740
+ constructor(firestore15) {
1741
+ this.db = firestore15 || admin5.firestore();
1740
1742
  }
1741
1743
  /**
1742
1744
  * Adds procedure information to a practitioner when a new procedure is created
@@ -2120,8 +2122,8 @@ var ProcedureAggregationService = class {
2120
2122
  var admin6 = __toESM(require("firebase-admin"));
2121
2123
  var CALENDAR_SUBCOLLECTION_ID4 = "calendar";
2122
2124
  var PatientAggregationService = class {
2123
- constructor(firestore13) {
2124
- this.db = firestore13 || admin6.firestore();
2125
+ constructor(firestore15) {
2126
+ this.db = firestore15 || admin6.firestore();
2125
2127
  }
2126
2128
  // --- Methods for Patient Creation --- >
2127
2129
  // No specific aggregations defined for patient creation in the plan.
@@ -2253,8 +2255,8 @@ var PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME = "patientRequirements";
2253
2255
  // src/admin/requirements/patient-requirements.admin.service.ts
2254
2256
  var admin7 = __toESM(require("firebase-admin"));
2255
2257
  var PatientRequirementsAdminService = class {
2256
- constructor(firestore13) {
2257
- this.db = firestore13 || admin7.firestore();
2258
+ constructor(firestore15) {
2259
+ this.db = firestore15 || admin7.firestore();
2258
2260
  this.notificationsAdmin = new NotificationsAdmin(this.db);
2259
2261
  }
2260
2262
  /**
@@ -2582,8 +2584,8 @@ var PatientRequirementsAdminService = class {
2582
2584
  // src/admin/calendar/calendar.admin.service.ts
2583
2585
  var admin8 = __toESM(require("firebase-admin"));
2584
2586
  var CalendarAdminService = class {
2585
- constructor(firestore13) {
2586
- this.db = firestore13 || admin8.firestore();
2587
+ constructor(firestore15) {
2588
+ this.db = firestore15 || admin8.firestore();
2587
2589
  Logger.info("[CalendarAdminService] Initialized.");
2588
2590
  }
2589
2591
  /**
@@ -2866,9 +2868,9 @@ var BaseMailingService = class {
2866
2868
  * @param firestore Firestore instance provided by the caller
2867
2869
  * @param mailgunClient Mailgun client instance (mailgun.js v10+) provided by the caller
2868
2870
  */
2869
- constructor(firestore13, mailgunClient) {
2871
+ constructor(firestore15, mailgunClient) {
2870
2872
  var _a;
2871
- this.db = firestore13;
2873
+ this.db = firestore15;
2872
2874
  this.mailgunClient = mailgunClient;
2873
2875
  if (!this.db) {
2874
2876
  Logger.error("[BaseMailingService] No Firestore instance provided");
@@ -3012,8 +3014,8 @@ var BaseMailingService = class {
3012
3014
  var patientAppointmentConfirmedTemplate = "<h1>Appointment Confirmed</h1><p>Dear {{patientName}},</p><p>Your appointment for {{procedureName}} on {{appointmentDate}} at {{appointmentTime}} with {{practitionerName}} at {{clinicName}} has been confirmed.</p><p>Thank you!</p>";
3013
3015
  var clinicAppointmentRequestedTemplate = "<h1>New Appointment Request</h1><p>Hello {{clinicName}} Admin,</p><p>A new appointment for {{procedureName}} has been requested by {{patientName}} for {{appointmentDate}} at {{appointmentTime}} with {{practitionerName}}.</p><p>Please review and confirm in the admin panel.</p>";
3014
3016
  var AppointmentMailingService = class extends BaseMailingService {
3015
- constructor(firestore13, mailgunClient) {
3016
- super(firestore13, mailgunClient);
3017
+ constructor(firestore15, mailgunClient) {
3018
+ super(firestore15, mailgunClient);
3017
3019
  this.DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
3018
3020
  Logger.info("[AppointmentMailingService] Initialized.");
3019
3021
  }
@@ -3162,8 +3164,8 @@ var AppointmentAggregationService = class {
3162
3164
  * @param mailgunClient - An initialized Mailgun client instance.
3163
3165
  * @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
3164
3166
  */
3165
- constructor(mailgunClient, firestore13) {
3166
- this.db = firestore13 || admin10.firestore();
3167
+ constructor(mailgunClient, firestore15) {
3168
+ this.db = firestore15 || admin10.firestore();
3167
3169
  this.appointmentMailingService = new AppointmentMailingService(
3168
3170
  this.db,
3169
3171
  mailgunClient
@@ -4412,6 +4414,658 @@ var AppointmentAggregationService = class {
4412
4414
  }
4413
4415
  };
4414
4416
 
4417
+ // src/admin/aggregation/forms/filled-forms.aggregation.service.ts
4418
+ var admin11 = __toESM(require("firebase-admin"));
4419
+ var FilledFormsAggregationService = class {
4420
+ /**
4421
+ * Constructor for FilledFormsAggregationService.
4422
+ * @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
4423
+ */
4424
+ constructor(firestore15) {
4425
+ this.db = firestore15 || admin11.firestore();
4426
+ Logger.info("[FilledFormsAggregationService] Initialized");
4427
+ }
4428
+ /**
4429
+ * Handles side effects when a filled form is created or updated.
4430
+ * This function would typically be called by a Firestore onCreate or onUpdate trigger.
4431
+ * @param filledDocument The filled document that was created or updated.
4432
+ * @returns {Promise<void>}
4433
+ */
4434
+ async handleFilledFormCreateOrUpdate(filledDocument) {
4435
+ Logger.info(
4436
+ `[FilledFormsAggregationService] Handling CREATE/UPDATE for filled form: ${filledDocument.id}, appointment: ${filledDocument.appointmentId}`
4437
+ );
4438
+ try {
4439
+ const appointmentRef = this.db.collection(APPOINTMENTS_COLLECTION).doc(filledDocument.appointmentId);
4440
+ const appointmentDoc = await appointmentRef.get();
4441
+ if (!appointmentDoc.exists) {
4442
+ Logger.error(
4443
+ `[FilledFormsAggregationService] Appointment ${filledDocument.appointmentId} not found.`
4444
+ );
4445
+ return;
4446
+ }
4447
+ const appointment = appointmentDoc.data();
4448
+ const formSubcollection = filledDocument.isUserForm ? USER_FORMS_SUBCOLLECTION : DOCTOR_FORMS_SUBCOLLECTION;
4449
+ const linkedFormInfo = {
4450
+ formId: filledDocument.id,
4451
+ templateId: filledDocument.templateId,
4452
+ templateVersion: filledDocument.templateVersion,
4453
+ title: filledDocument.isUserForm ? "User Form" : "Doctor Form",
4454
+ // Default title if not available
4455
+ isUserForm: filledDocument.isUserForm,
4456
+ isRequired: filledDocument.isRequired,
4457
+ status: filledDocument.status,
4458
+ path: `${APPOINTMENTS_COLLECTION}/${filledDocument.appointmentId}/${formSubcollection}/${filledDocument.id}`
4459
+ };
4460
+ if (filledDocument.updatedAt) {
4461
+ linkedFormInfo.submittedAt = admin11.firestore.Timestamp.fromMillis(
4462
+ filledDocument.updatedAt
4463
+ );
4464
+ }
4465
+ if (filledDocument.status === "completed" /* COMPLETED */ || filledDocument.status === "signed" /* SIGNED */) {
4466
+ linkedFormInfo.completedAt = admin11.firestore.Timestamp.fromMillis(
4467
+ filledDocument.updatedAt
4468
+ );
4469
+ }
4470
+ let updateData = {};
4471
+ let existingFormIndex = -1;
4472
+ if (appointment.linkedForms && appointment.linkedForms.length > 0) {
4473
+ existingFormIndex = appointment.linkedForms.findIndex(
4474
+ (form) => form.formId === filledDocument.id
4475
+ );
4476
+ }
4477
+ if (existingFormIndex >= 0 && appointment.linkedForms) {
4478
+ updateData = {
4479
+ linkedForms: admin11.firestore.FieldValue.arrayRemove(
4480
+ appointment.linkedForms[existingFormIndex]
4481
+ ),
4482
+ updatedAt: admin11.firestore.FieldValue.serverTimestamp()
4483
+ };
4484
+ await appointmentRef.update(updateData);
4485
+ updateData = {
4486
+ linkedForms: admin11.firestore.FieldValue.arrayUnion(linkedFormInfo),
4487
+ updatedAt: admin11.firestore.FieldValue.serverTimestamp()
4488
+ };
4489
+ } else {
4490
+ updateData = {
4491
+ linkedForms: appointment.linkedForms ? admin11.firestore.FieldValue.arrayUnion(linkedFormInfo) : [linkedFormInfo],
4492
+ // If linkedForms doesn't exist, create a new array
4493
+ linkedFormIds: appointment.linkedFormIds ? admin11.firestore.FieldValue.arrayUnion(filledDocument.id) : [filledDocument.id],
4494
+ // If linkedFormIds doesn't exist, create a new array
4495
+ updatedAt: admin11.firestore.FieldValue.serverTimestamp()
4496
+ };
4497
+ }
4498
+ if (filledDocument.isUserForm && filledDocument.isRequired && (filledDocument.status === "completed" /* COMPLETED */ || filledDocument.status === "signed" /* SIGNED */)) {
4499
+ if (appointment.pendingUserFormsIds && appointment.pendingUserFormsIds.includes(filledDocument.id)) {
4500
+ updateData.pendingUserFormsIds = admin11.firestore.FieldValue.arrayRemove(filledDocument.id);
4501
+ Logger.info(
4502
+ `[FilledFormsAggregationService] Removing form ${filledDocument.id} from pendingUserFormsIds`
4503
+ );
4504
+ }
4505
+ }
4506
+ await appointmentRef.update(updateData);
4507
+ Logger.info(
4508
+ `[FilledFormsAggregationService] Successfully updated appointment ${filledDocument.appointmentId} with form info for ${filledDocument.id}`
4509
+ );
4510
+ } catch (error) {
4511
+ Logger.error(
4512
+ `[FilledFormsAggregationService] Error updating appointment for filled form ${filledDocument.id}:`,
4513
+ error
4514
+ );
4515
+ throw error;
4516
+ }
4517
+ }
4518
+ /**
4519
+ * Handles side effects when a filled form is deleted.
4520
+ * This function would typically be called by a Firestore onDelete trigger.
4521
+ * @param filledDocument The filled document that was deleted.
4522
+ * @returns {Promise<void>}
4523
+ */
4524
+ async handleFilledFormDelete(filledDocument) {
4525
+ Logger.info(
4526
+ `[FilledFormsAggregationService] Handling DELETE for filled form: ${filledDocument.id}, appointment: ${filledDocument.appointmentId}`
4527
+ );
4528
+ try {
4529
+ const appointmentRef = this.db.collection(APPOINTMENTS_COLLECTION).doc(filledDocument.appointmentId);
4530
+ const appointmentDoc = await appointmentRef.get();
4531
+ if (!appointmentDoc.exists) {
4532
+ Logger.error(
4533
+ `[FilledFormsAggregationService] Appointment ${filledDocument.appointmentId} not found.`
4534
+ );
4535
+ return;
4536
+ }
4537
+ const appointment = appointmentDoc.data();
4538
+ let updateData = {};
4539
+ if (appointment.linkedForms && appointment.linkedForms.length > 0) {
4540
+ const formToRemove = appointment.linkedForms.find(
4541
+ (form) => form.formId === filledDocument.id
4542
+ );
4543
+ if (formToRemove) {
4544
+ updateData.linkedForms = admin11.firestore.FieldValue.arrayRemove(formToRemove);
4545
+ }
4546
+ }
4547
+ if (appointment.linkedFormIds && appointment.linkedFormIds.includes(filledDocument.id)) {
4548
+ updateData.linkedFormIds = admin11.firestore.FieldValue.arrayRemove(
4549
+ filledDocument.id
4550
+ );
4551
+ }
4552
+ if (filledDocument.isUserForm && filledDocument.isRequired) {
4553
+ if (filledDocument.status !== "completed" /* COMPLETED */ && filledDocument.status !== "signed" /* SIGNED */) {
4554
+ if (!appointment.pendingUserFormsIds || !appointment.pendingUserFormsIds.includes(filledDocument.id)) {
4555
+ updateData.pendingUserFormsIds = appointment.pendingUserFormsIds ? admin11.firestore.FieldValue.arrayUnion(filledDocument.id) : [filledDocument.id];
4556
+ }
4557
+ }
4558
+ }
4559
+ if (Object.keys(updateData).length > 0) {
4560
+ updateData.updatedAt = admin11.firestore.FieldValue.serverTimestamp();
4561
+ await appointmentRef.update(updateData);
4562
+ Logger.info(
4563
+ `[FilledFormsAggregationService] Successfully updated appointment ${filledDocument.appointmentId} after form deletion`
4564
+ );
4565
+ } else {
4566
+ Logger.info(
4567
+ `[FilledFormsAggregationService] No updates needed for appointment ${filledDocument.appointmentId}`
4568
+ );
4569
+ }
4570
+ } catch (error) {
4571
+ Logger.error(
4572
+ `[FilledFormsAggregationService] Error updating appointment after form deletion for ${filledDocument.id}:`,
4573
+ error
4574
+ );
4575
+ throw error;
4576
+ }
4577
+ }
4578
+ /**
4579
+ * Updates the appointment's pendingUserFormsIds for a new required user form.
4580
+ * This should be called when a required user form is first created.
4581
+ * @param filledDocument The newly created filled document.
4582
+ * @returns {Promise<void>}
4583
+ */
4584
+ async handleRequiredUserFormCreate(filledDocument) {
4585
+ if (!filledDocument.isUserForm || !filledDocument.isRequired) {
4586
+ return;
4587
+ }
4588
+ Logger.info(
4589
+ `[FilledFormsAggregationService] Handling new required user form creation: ${filledDocument.id}`
4590
+ );
4591
+ try {
4592
+ const appointmentRef = this.db.collection(APPOINTMENTS_COLLECTION).doc(filledDocument.appointmentId);
4593
+ if (filledDocument.status === "completed" /* COMPLETED */ || filledDocument.status === "signed" /* SIGNED */) {
4594
+ Logger.info(
4595
+ `[FilledFormsAggregationService] Form ${filledDocument.id} is already completed/signed, not adding to pendingUserFormsIds`
4596
+ );
4597
+ return;
4598
+ }
4599
+ await appointmentRef.update({
4600
+ pendingUserFormsIds: admin11.firestore.FieldValue.arrayUnion(
4601
+ filledDocument.id
4602
+ ),
4603
+ updatedAt: admin11.firestore.FieldValue.serverTimestamp()
4604
+ });
4605
+ Logger.info(
4606
+ `[FilledFormsAggregationService] Successfully added form ${filledDocument.id} to pendingUserFormsIds for appointment ${filledDocument.appointmentId}`
4607
+ );
4608
+ } catch (error) {
4609
+ Logger.error(
4610
+ `[FilledFormsAggregationService] Error handling required user form creation for ${filledDocument.id}:`,
4611
+ error
4612
+ );
4613
+ throw error;
4614
+ }
4615
+ }
4616
+ };
4617
+
4618
+ // src/admin/aggregation/reviews/reviews.aggregation.service.ts
4619
+ var admin12 = __toESM(require("firebase-admin"));
4620
+
4621
+ // src/types/reviews/index.ts
4622
+ var REVIEWS_COLLECTION = "reviews";
4623
+
4624
+ // src/admin/aggregation/reviews/reviews.aggregation.service.ts
4625
+ var ReviewsAggregationService = class {
4626
+ /**
4627
+ * Constructor for ReviewsAggregationService.
4628
+ * @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
4629
+ */
4630
+ constructor(firestore15) {
4631
+ this.db = firestore15 || admin12.firestore();
4632
+ }
4633
+ /**
4634
+ * Process a newly created review and update all related entities
4635
+ * @param review The newly created review
4636
+ * @returns Promise resolving when all updates are complete
4637
+ */
4638
+ async processNewReview(review) {
4639
+ console.log(
4640
+ `[ReviewsAggregationService] Processing new review: ${review.id}`
4641
+ );
4642
+ const updatePromises = [];
4643
+ if (review.clinicReview) {
4644
+ updatePromises.push(
4645
+ this.updateClinicReviewInfo(review.clinicReview.clinicId)
4646
+ );
4647
+ }
4648
+ if (review.practitionerReview) {
4649
+ updatePromises.push(
4650
+ this.updatePractitionerReviewInfo(
4651
+ review.practitionerReview.practitionerId
4652
+ )
4653
+ );
4654
+ }
4655
+ if (review.procedureReview) {
4656
+ updatePromises.push(
4657
+ this.updateProcedureReviewInfo(review.procedureReview.procedureId)
4658
+ );
4659
+ }
4660
+ await Promise.all(updatePromises);
4661
+ console.log(
4662
+ `[ReviewsAggregationService] Successfully processed review: ${review.id}`
4663
+ );
4664
+ }
4665
+ /**
4666
+ * Process a deleted review and update all related entities
4667
+ * @param review The deleted review
4668
+ * @returns Promise resolving when all updates are complete
4669
+ */
4670
+ async processDeletedReview(review) {
4671
+ console.log(
4672
+ `[ReviewsAggregationService] Processing deleted review: ${review.id}`
4673
+ );
4674
+ const updatePromises = [];
4675
+ if (review.clinicReview) {
4676
+ updatePromises.push(
4677
+ this.updateClinicReviewInfo(
4678
+ review.clinicReview.clinicId,
4679
+ review.clinicReview,
4680
+ true
4681
+ )
4682
+ );
4683
+ }
4684
+ if (review.practitionerReview) {
4685
+ updatePromises.push(
4686
+ this.updatePractitionerReviewInfo(
4687
+ review.practitionerReview.practitionerId,
4688
+ review.practitionerReview,
4689
+ true
4690
+ )
4691
+ );
4692
+ }
4693
+ if (review.procedureReview) {
4694
+ updatePromises.push(
4695
+ this.updateProcedureReviewInfo(
4696
+ review.procedureReview.procedureId,
4697
+ review.procedureReview,
4698
+ true
4699
+ )
4700
+ );
4701
+ }
4702
+ await Promise.all(updatePromises);
4703
+ console.log(
4704
+ `[ReviewsAggregationService] Successfully processed deleted review: ${review.id}`
4705
+ );
4706
+ }
4707
+ /**
4708
+ * Updates the review info for a clinic
4709
+ * @param clinicId The ID of the clinic to update
4710
+ * @param removedReview Optional review being removed
4711
+ * @param isRemoval Whether this update is for a review removal
4712
+ * @returns The updated clinic review info
4713
+ */
4714
+ async updateClinicReviewInfo(clinicId, removedReview, isRemoval = false) {
4715
+ console.log(
4716
+ `[ReviewsAggregationService] Updating review info for clinic: ${clinicId}`
4717
+ );
4718
+ const clinicDoc = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
4719
+ if (!clinicDoc.exists) {
4720
+ console.error(
4721
+ `[ReviewsAggregationService] Clinic with ID ${clinicId} not found`
4722
+ );
4723
+ throw new Error(`Clinic with ID ${clinicId} not found`);
4724
+ }
4725
+ const clinicData = clinicDoc.data();
4726
+ const currentReviewInfo = (clinicData == null ? void 0 : clinicData.reviewInfo) || {
4727
+ totalReviews: 0,
4728
+ averageRating: 0,
4729
+ cleanliness: 0,
4730
+ facilities: 0,
4731
+ staffFriendliness: 0,
4732
+ waitingTime: 0,
4733
+ accessibility: 0,
4734
+ recommendationPercentage: 0
4735
+ };
4736
+ const reviewsQuery = await this.db.collection(REVIEWS_COLLECTION).where("clinicReview.clinicId", "==", clinicId).get();
4737
+ if (isRemoval && reviewsQuery.size <= 1 || reviewsQuery.empty) {
4738
+ const updatedReviewInfo2 = {
4739
+ totalReviews: 0,
4740
+ averageRating: 0,
4741
+ cleanliness: 0,
4742
+ facilities: 0,
4743
+ staffFriendliness: 0,
4744
+ waitingTime: 0,
4745
+ accessibility: 0,
4746
+ recommendationPercentage: 0
4747
+ };
4748
+ await this.db.collection(CLINICS_COLLECTION).doc(clinicId).update({
4749
+ reviewInfo: updatedReviewInfo2,
4750
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
4751
+ });
4752
+ console.log(
4753
+ `[ReviewsAggregationService] Reset review info for clinic: ${clinicId}`
4754
+ );
4755
+ return updatedReviewInfo2;
4756
+ }
4757
+ const reviews = reviewsQuery.docs.map((doc) => doc.data());
4758
+ const clinicReviews = reviews.map((review) => review.clinicReview).filter((review) => review !== void 0);
4759
+ let totalRating = 0;
4760
+ let totalCleanliness = 0;
4761
+ let totalFacilities = 0;
4762
+ let totalStaffFriendliness = 0;
4763
+ let totalWaitingTime = 0;
4764
+ let totalAccessibility = 0;
4765
+ let totalRecommendations = 0;
4766
+ clinicReviews.forEach((review) => {
4767
+ totalRating += review.overallRating;
4768
+ totalCleanliness += review.cleanliness;
4769
+ totalFacilities += review.facilities;
4770
+ totalStaffFriendliness += review.staffFriendliness;
4771
+ totalWaitingTime += review.waitingTime;
4772
+ totalAccessibility += review.accessibility;
4773
+ if (review.wouldRecommend) totalRecommendations++;
4774
+ });
4775
+ const count = clinicReviews.length;
4776
+ const roundToOneDecimal = (value) => Math.round(value / count * 10) / 10;
4777
+ const updatedReviewInfo = {
4778
+ totalReviews: count,
4779
+ averageRating: roundToOneDecimal(totalRating),
4780
+ cleanliness: roundToOneDecimal(totalCleanliness),
4781
+ facilities: roundToOneDecimal(totalFacilities),
4782
+ staffFriendliness: roundToOneDecimal(totalStaffFriendliness),
4783
+ waitingTime: roundToOneDecimal(totalWaitingTime),
4784
+ accessibility: roundToOneDecimal(totalAccessibility),
4785
+ recommendationPercentage: Math.round(totalRecommendations / count * 1e3) / 10
4786
+ };
4787
+ await this.db.collection(CLINICS_COLLECTION).doc(clinicId).update({
4788
+ reviewInfo: updatedReviewInfo,
4789
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
4790
+ });
4791
+ console.log(
4792
+ `[ReviewsAggregationService] Updated review info for clinic: ${clinicId}`
4793
+ );
4794
+ return updatedReviewInfo;
4795
+ }
4796
+ /**
4797
+ * Updates the review info for a practitioner
4798
+ * @param practitionerId The ID of the practitioner to update
4799
+ * @param removedReview Optional review being removed
4800
+ * @param isRemoval Whether this update is for a review removal
4801
+ * @returns The updated practitioner review info
4802
+ */
4803
+ async updatePractitionerReviewInfo(practitionerId, removedReview, isRemoval = false) {
4804
+ console.log(
4805
+ `[ReviewsAggregationService] Updating review info for practitioner: ${practitionerId}`
4806
+ );
4807
+ const practitionerDoc = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
4808
+ if (!practitionerDoc.exists) {
4809
+ console.error(
4810
+ `[ReviewsAggregationService] Practitioner with ID ${practitionerId} not found`
4811
+ );
4812
+ throw new Error(`Practitioner with ID ${practitionerId} not found`);
4813
+ }
4814
+ const practitionerData = practitionerDoc.data();
4815
+ const currentReviewInfo = (practitionerData == null ? void 0 : practitionerData.reviewInfo) || {
4816
+ totalReviews: 0,
4817
+ averageRating: 0,
4818
+ knowledgeAndExpertise: 0,
4819
+ communicationSkills: 0,
4820
+ bedSideManner: 0,
4821
+ thoroughness: 0,
4822
+ trustworthiness: 0,
4823
+ recommendationPercentage: 0
4824
+ };
4825
+ const reviewsQuery = await this.db.collection(REVIEWS_COLLECTION).where("practitionerReview.practitionerId", "==", practitionerId).get();
4826
+ if (isRemoval && reviewsQuery.size <= 1 || reviewsQuery.empty) {
4827
+ const updatedReviewInfo2 = {
4828
+ totalReviews: 0,
4829
+ averageRating: 0,
4830
+ knowledgeAndExpertise: 0,
4831
+ communicationSkills: 0,
4832
+ bedSideManner: 0,
4833
+ thoroughness: 0,
4834
+ trustworthiness: 0,
4835
+ recommendationPercentage: 0
4836
+ };
4837
+ await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).update({
4838
+ reviewInfo: updatedReviewInfo2,
4839
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
4840
+ });
4841
+ await this.updateDoctorInfoInProcedures(practitionerId, 0);
4842
+ console.log(
4843
+ `[ReviewsAggregationService] Reset review info for practitioner: ${practitionerId}`
4844
+ );
4845
+ return updatedReviewInfo2;
4846
+ }
4847
+ const reviews = reviewsQuery.docs.map((doc) => doc.data());
4848
+ const practitionerReviews = reviews.map((review) => review.practitionerReview).filter((review) => review !== void 0);
4849
+ let totalRating = 0;
4850
+ let totalKnowledgeAndExpertise = 0;
4851
+ let totalCommunicationSkills = 0;
4852
+ let totalBedSideManner = 0;
4853
+ let totalThoroughness = 0;
4854
+ let totalTrustworthiness = 0;
4855
+ let totalRecommendations = 0;
4856
+ practitionerReviews.forEach((review) => {
4857
+ totalRating += review.overallRating;
4858
+ totalKnowledgeAndExpertise += review.knowledgeAndExpertise;
4859
+ totalCommunicationSkills += review.communicationSkills;
4860
+ totalBedSideManner += review.bedSideManner;
4861
+ totalThoroughness += review.thoroughness;
4862
+ totalTrustworthiness += review.trustworthiness;
4863
+ if (review.wouldRecommend) totalRecommendations++;
4864
+ });
4865
+ const count = practitionerReviews.length;
4866
+ const roundToOneDecimal = (value) => Math.round(value / count * 10) / 10;
4867
+ const updatedReviewInfo = {
4868
+ totalReviews: count,
4869
+ averageRating: roundToOneDecimal(totalRating),
4870
+ knowledgeAndExpertise: roundToOneDecimal(totalKnowledgeAndExpertise),
4871
+ communicationSkills: roundToOneDecimal(totalCommunicationSkills),
4872
+ bedSideManner: roundToOneDecimal(totalBedSideManner),
4873
+ thoroughness: roundToOneDecimal(totalThoroughness),
4874
+ trustworthiness: roundToOneDecimal(totalTrustworthiness),
4875
+ recommendationPercentage: Math.round(totalRecommendations / count * 1e3) / 10
4876
+ };
4877
+ await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).update({
4878
+ reviewInfo: updatedReviewInfo,
4879
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
4880
+ });
4881
+ await this.updateDoctorInfoInProcedures(
4882
+ practitionerId,
4883
+ updatedReviewInfo.averageRating
4884
+ );
4885
+ console.log(
4886
+ `[ReviewsAggregationService] Updated review info for practitioner: ${practitionerId}`
4887
+ );
4888
+ return updatedReviewInfo;
4889
+ }
4890
+ /**
4891
+ * Updates the review info for a procedure
4892
+ * @param procedureId The ID of the procedure to update
4893
+ * @param removedReview Optional review being removed
4894
+ * @param isRemoval Whether this update is for a review removal
4895
+ * @returns The updated procedure review info
4896
+ */
4897
+ async updateProcedureReviewInfo(procedureId, removedReview, isRemoval = false) {
4898
+ console.log(
4899
+ `[ReviewsAggregationService] Updating review info for procedure: ${procedureId}`
4900
+ );
4901
+ const procedureDoc = await this.db.collection(PROCEDURES_COLLECTION).doc(procedureId).get();
4902
+ if (!procedureDoc.exists) {
4903
+ console.error(
4904
+ `[ReviewsAggregationService] Procedure with ID ${procedureId} not found`
4905
+ );
4906
+ throw new Error(`Procedure with ID ${procedureId} not found`);
4907
+ }
4908
+ const procedureData = procedureDoc.data();
4909
+ const currentReviewInfo = (procedureData == null ? void 0 : procedureData.reviewInfo) || {
4910
+ totalReviews: 0,
4911
+ averageRating: 0,
4912
+ effectivenessOfTreatment: 0,
4913
+ outcomeExplanation: 0,
4914
+ painManagement: 0,
4915
+ followUpCare: 0,
4916
+ valueForMoney: 0,
4917
+ recommendationPercentage: 0
4918
+ };
4919
+ const reviewsQuery = await this.db.collection(REVIEWS_COLLECTION).where("procedureReview.procedureId", "==", procedureId).get();
4920
+ if (isRemoval && reviewsQuery.size <= 1 || reviewsQuery.empty) {
4921
+ const updatedReviewInfo2 = {
4922
+ totalReviews: 0,
4923
+ averageRating: 0,
4924
+ effectivenessOfTreatment: 0,
4925
+ outcomeExplanation: 0,
4926
+ painManagement: 0,
4927
+ followUpCare: 0,
4928
+ valueForMoney: 0,
4929
+ recommendationPercentage: 0
4930
+ };
4931
+ await this.db.collection(PROCEDURES_COLLECTION).doc(procedureId).update({
4932
+ reviewInfo: updatedReviewInfo2,
4933
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
4934
+ });
4935
+ console.log(
4936
+ `[ReviewsAggregationService] Reset review info for procedure: ${procedureId}`
4937
+ );
4938
+ return updatedReviewInfo2;
4939
+ }
4940
+ const reviews = reviewsQuery.docs.map((doc) => doc.data());
4941
+ const procedureReviews = reviews.map((review) => review.procedureReview).filter((review) => review !== void 0);
4942
+ let totalRating = 0;
4943
+ let totalEffectivenessOfTreatment = 0;
4944
+ let totalOutcomeExplanation = 0;
4945
+ let totalPainManagement = 0;
4946
+ let totalFollowUpCare = 0;
4947
+ let totalValueForMoney = 0;
4948
+ let totalRecommendations = 0;
4949
+ procedureReviews.forEach((review) => {
4950
+ totalRating += review.overallRating;
4951
+ totalEffectivenessOfTreatment += review.effectivenessOfTreatment;
4952
+ totalOutcomeExplanation += review.outcomeExplanation;
4953
+ totalPainManagement += review.painManagement;
4954
+ totalFollowUpCare += review.followUpCare;
4955
+ totalValueForMoney += review.valueForMoney;
4956
+ if (review.wouldRecommend) totalRecommendations++;
4957
+ });
4958
+ const count = procedureReviews.length;
4959
+ const roundToOneDecimal = (value) => Math.round(value / count * 10) / 10;
4960
+ const updatedReviewInfo = {
4961
+ totalReviews: count,
4962
+ averageRating: roundToOneDecimal(totalRating),
4963
+ effectivenessOfTreatment: roundToOneDecimal(
4964
+ totalEffectivenessOfTreatment
4965
+ ),
4966
+ outcomeExplanation: roundToOneDecimal(totalOutcomeExplanation),
4967
+ painManagement: roundToOneDecimal(totalPainManagement),
4968
+ followUpCare: roundToOneDecimal(totalFollowUpCare),
4969
+ valueForMoney: roundToOneDecimal(totalValueForMoney),
4970
+ recommendationPercentage: Math.round(totalRecommendations / count * 1e3) / 10
4971
+ };
4972
+ await this.db.collection(PROCEDURES_COLLECTION).doc(procedureId).update({
4973
+ reviewInfo: updatedReviewInfo,
4974
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
4975
+ });
4976
+ console.log(
4977
+ `[ReviewsAggregationService] Updated review info for procedure: ${procedureId}`
4978
+ );
4979
+ return updatedReviewInfo;
4980
+ }
4981
+ /**
4982
+ * Updates doctorInfo rating in all procedures for a practitioner
4983
+ * @param practitionerId The ID of the practitioner
4984
+ * @param rating The new rating to set
4985
+ */
4986
+ async updateDoctorInfoInProcedures(practitionerId, rating) {
4987
+ console.log(
4988
+ `[ReviewsAggregationService] Updating doctor info in procedures for practitioner: ${practitionerId}`
4989
+ );
4990
+ const proceduresQuery = await this.db.collection(PROCEDURES_COLLECTION).where("practitionerId", "==", practitionerId).get();
4991
+ if (proceduresQuery.empty) {
4992
+ console.log(
4993
+ `[ReviewsAggregationService] No procedures found for practitioner: ${practitionerId}`
4994
+ );
4995
+ return;
4996
+ }
4997
+ const batch = this.db.batch();
4998
+ proceduresQuery.docs.forEach((docSnapshot) => {
4999
+ const procedureRef = this.db.collection(PROCEDURES_COLLECTION).doc(docSnapshot.id);
5000
+ batch.update(procedureRef, {
5001
+ "doctorInfo.rating": rating,
5002
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
5003
+ });
5004
+ });
5005
+ await batch.commit();
5006
+ console.log(
5007
+ `[ReviewsAggregationService] Updated doctor info in ${proceduresQuery.size} procedures for practitioner: ${practitionerId}`
5008
+ );
5009
+ }
5010
+ /**
5011
+ * Verifies a review as checked by admin/staff
5012
+ * @param reviewId The ID of the review to verify
5013
+ */
5014
+ async verifyReview(reviewId) {
5015
+ console.log(`[ReviewsAggregationService] Verifying review: ${reviewId}`);
5016
+ const reviewDoc = await this.db.collection(REVIEWS_COLLECTION).doc(reviewId).get();
5017
+ if (!reviewDoc.exists) {
5018
+ console.error(
5019
+ `[ReviewsAggregationService] Review with ID ${reviewId} not found`
5020
+ );
5021
+ throw new Error(`Review with ID ${reviewId} not found`);
5022
+ }
5023
+ const review = reviewDoc.data();
5024
+ const batch = this.db.batch();
5025
+ const reviewRef = this.db.collection(REVIEWS_COLLECTION).doc(reviewId);
5026
+ if (review.clinicReview) {
5027
+ review.clinicReview.isVerified = true;
5028
+ }
5029
+ if (review.practitionerReview) {
5030
+ review.practitionerReview.isVerified = true;
5031
+ }
5032
+ if (review.procedureReview) {
5033
+ review.procedureReview.isVerified = true;
5034
+ }
5035
+ batch.update(reviewRef, {
5036
+ clinicReview: review.clinicReview,
5037
+ practitionerReview: review.practitionerReview,
5038
+ procedureReview: review.procedureReview,
5039
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
5040
+ });
5041
+ await batch.commit();
5042
+ console.log(
5043
+ `[ReviewsAggregationService] Successfully verified review: ${reviewId}`
5044
+ );
5045
+ }
5046
+ /**
5047
+ * Calculate the average of all reviews for an entity
5048
+ * @param entityId The entity ID
5049
+ * @param entityType The type of entity ('clinic', 'practitioner', or 'procedure')
5050
+ * @returns Promise that resolves to the calculated review info
5051
+ */
5052
+ async calculateEntityReviewInfo(entityId, entityType) {
5053
+ console.log(
5054
+ `[ReviewsAggregationService] Calculating review info for ${entityType}: ${entityId}`
5055
+ );
5056
+ switch (entityType) {
5057
+ case "clinic":
5058
+ return this.updateClinicReviewInfo(entityId);
5059
+ case "practitioner":
5060
+ return this.updatePractitionerReviewInfo(entityId);
5061
+ case "procedure":
5062
+ return this.updateProcedureReviewInfo(entityId);
5063
+ default:
5064
+ throw new Error(`Invalid entity type: ${entityType}`);
5065
+ }
5066
+ }
5067
+ };
5068
+
4415
5069
  // src/admin/mailing/practitionerInvite/templates/invitation.template.ts
4416
5070
  var practitionerInvitationTemplate = `
4417
5071
  <!DOCTYPE html>
@@ -4519,8 +5173,8 @@ var PractitionerInviteMailingService = class extends BaseMailingService {
4519
5173
  * @param firestore Firestore instance provided by the caller
4520
5174
  * @param mailgunClient Mailgun client instance (mailgun.js v10+) provided by the caller
4521
5175
  */
4522
- constructor(firestore13, mailgunClient) {
4523
- super(firestore13, mailgunClient);
5176
+ constructor(firestore15, mailgunClient) {
5177
+ super(firestore15, mailgunClient);
4524
5178
  this.DEFAULT_REGISTRATION_URL = "https://metaesthetics.net/register";
4525
5179
  this.DEFAULT_SUBJECT = "You've Been Invited to Join as a Practitioner";
4526
5180
  this.DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
@@ -4758,7 +5412,7 @@ var PractitionerInviteMailingService = class extends BaseMailingService {
4758
5412
  };
4759
5413
 
4760
5414
  // src/admin/booking/booking.admin.ts
4761
- var admin12 = __toESM(require("firebase-admin"));
5415
+ var admin14 = __toESM(require("firebase-admin"));
4762
5416
 
4763
5417
  // src/admin/booking/booking.calculator.ts
4764
5418
  var import_firestore2 = require("firebase/firestore");
@@ -5192,10 +5846,10 @@ var BookingAvailabilityCalculator = class {
5192
5846
  BookingAvailabilityCalculator.DEFAULT_INTERVAL_MINUTES = 15;
5193
5847
 
5194
5848
  // src/admin/documentation-templates/document-manager.admin.ts
5195
- var admin11 = __toESM(require("firebase-admin"));
5849
+ var admin13 = __toESM(require("firebase-admin"));
5196
5850
  var DocumentManagerAdminService = class {
5197
- constructor(firestore13) {
5198
- this.db = firestore13;
5851
+ constructor(firestore15) {
5852
+ this.db = firestore15;
5199
5853
  }
5200
5854
  /**
5201
5855
  * Adds operations to a Firestore batch to initialize all linked forms for a new appointment
@@ -5298,7 +5952,7 @@ var DocumentManagerAdminService = class {
5298
5952
  };
5299
5953
  }
5300
5954
  const templateIds = technologyTemplates.map((t) => t.templateId);
5301
- const templatesSnapshot = await this.db.collection(DOCUMENTATION_TEMPLATES_COLLECTION).where(admin11.firestore.FieldPath.documentId(), "in", templateIds).get();
5955
+ const templatesSnapshot = await this.db.collection(DOCUMENTATION_TEMPLATES_COLLECTION).where(admin13.firestore.FieldPath.documentId(), "in", templateIds).get();
5302
5956
  const templatesMap = /* @__PURE__ */ new Map();
5303
5957
  templatesSnapshot.forEach((doc) => {
5304
5958
  templatesMap.set(doc.id, doc.data());
@@ -5364,8 +6018,8 @@ var BookingAdmin = class {
5364
6018
  * Creates a new BookingAdmin instance
5365
6019
  * @param firestore - Firestore instance provided by the caller
5366
6020
  */
5367
- constructor(firestore13) {
5368
- this.db = firestore13 || admin12.firestore();
6021
+ constructor(firestore15) {
6022
+ this.db = firestore15 || admin14.firestore();
5369
6023
  this.documentManagerAdmin = new DocumentManagerAdminService(this.db);
5370
6024
  }
5371
6025
  /**
@@ -5382,8 +6036,8 @@ var BookingAdmin = class {
5382
6036
  console.log(
5383
6037
  `[BookingAdmin] Getting available slots for clinic ${clinicId}, practitioner ${practitionerId}, procedure ${procedureId}`
5384
6038
  );
5385
- const start = timeframe.start instanceof Date ? admin12.firestore.Timestamp.fromDate(timeframe.start) : timeframe.start;
5386
- const end = timeframe.end instanceof Date ? admin12.firestore.Timestamp.fromDate(timeframe.end) : timeframe.end;
6039
+ const start = timeframe.start instanceof Date ? admin14.firestore.Timestamp.fromDate(timeframe.start) : timeframe.start;
6040
+ const end = timeframe.end instanceof Date ? admin14.firestore.Timestamp.fromDate(timeframe.end) : timeframe.end;
5387
6041
  const clinicDoc = await this.db.collection("clinics").doc(clinicId).get();
5388
6042
  if (!clinicDoc.exists) {
5389
6043
  throw new Error(`Clinic ${clinicId} not found`);
@@ -5422,7 +6076,7 @@ var BookingAdmin = class {
5422
6076
  const result = BookingAvailabilityCalculator.calculateSlots(request);
5423
6077
  return {
5424
6078
  availableSlots: result.availableSlots.map((slot) => ({
5425
- start: admin12.firestore.Timestamp.fromMillis(slot.start.toMillis())
6079
+ start: admin14.firestore.Timestamp.fromMillis(slot.start.toMillis())
5426
6080
  }))
5427
6081
  };
5428
6082
  } catch (error) {
@@ -5524,8 +6178,8 @@ var BookingAdmin = class {
5524
6178
  `[BookingAdmin] Orchestrating appointment creation for patient ${data.patientId} by user ${authenticatedUserId}`
5525
6179
  );
5526
6180
  const batch = this.db.batch();
5527
- const adminTsNow = admin12.firestore.Timestamp.now();
5528
- const serverTimestampValue = admin12.firestore.FieldValue.serverTimestamp();
6181
+ const adminTsNow = admin14.firestore.Timestamp.now();
6182
+ const serverTimestampValue = admin14.firestore.FieldValue.serverTimestamp();
5529
6183
  try {
5530
6184
  if (!data.patientId || !data.procedureId || !data.appointmentStartTime || !data.appointmentEndTime) {
5531
6185
  return {
@@ -5622,7 +6276,7 @@ var BookingAdmin = class {
5622
6276
  fullName: `${(patientSensitiveData == null ? void 0 : patientSensitiveData.firstName) || ""} ${(patientSensitiveData == null ? void 0 : patientSensitiveData.lastName) || ""}`.trim() || patientProfileData.displayName,
5623
6277
  email: (patientSensitiveData == null ? void 0 : patientSensitiveData.email) || "",
5624
6278
  phone: (patientSensitiveData == null ? void 0 : patientSensitiveData.phoneNumber) || patientProfileData.phoneNumber || null,
5625
- dateOfBirth: (patientSensitiveData == null ? void 0 : patientSensitiveData.dateOfBirth) || patientProfileData.dateOfBirth || admin12.firestore.Timestamp.now(),
6279
+ dateOfBirth: (patientSensitiveData == null ? void 0 : patientSensitiveData.dateOfBirth) || patientProfileData.dateOfBirth || admin14.firestore.Timestamp.now(),
5626
6280
  gender: (patientSensitiveData == null ? void 0 : patientSensitiveData.gender) || "other" /* OTHER */
5627
6281
  };
5628
6282
  const newAppointmentId = this.db.collection(APPOINTMENTS_COLLECTION).doc().id;
@@ -5873,6 +6527,7 @@ TimestampUtils.enableServerMode();
5873
6527
  CalendarAdminService,
5874
6528
  ClinicAggregationService,
5875
6529
  DocumentManagerAdminService,
6530
+ FilledFormsAggregationService,
5876
6531
  Logger,
5877
6532
  MediaType,
5878
6533
  NOTIFICATIONS_COLLECTION,
@@ -5889,5 +6544,6 @@ TimestampUtils.enableServerMode();
5889
6544
  PractitionerInviteMailingService,
5890
6545
  PractitionerTokenStatus,
5891
6546
  ProcedureAggregationService,
6547
+ ReviewsAggregationService,
5892
6548
  UserRole
5893
6549
  });