@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.
@@ -148,9 +148,9 @@ var Logger = class {
148
148
 
149
149
  // src/admin/notifications/notifications.admin.ts
150
150
  var NotificationsAdmin = class {
151
- constructor(firestore13) {
151
+ constructor(firestore15) {
152
152
  this.expo = new Expo();
153
- this.db = firestore13 || admin.firestore();
153
+ this.db = firestore15 || admin.firestore();
154
154
  }
155
155
  /**
156
156
  * Dohvata notifikaciju po ID-u
@@ -869,8 +869,8 @@ var ClinicAggregationService = class {
869
869
  * Constructor for ClinicAggregationService.
870
870
  * @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
871
871
  */
872
- constructor(firestore13) {
873
- this.db = firestore13 || admin3.firestore();
872
+ constructor(firestore15) {
873
+ this.db = firestore15 || admin3.firestore();
874
874
  }
875
875
  /**
876
876
  * Adds clinic information to a clinic group when a new clinic is created
@@ -1344,8 +1344,8 @@ var ClinicAggregationService = class {
1344
1344
  import * as admin4 from "firebase-admin";
1345
1345
  var CALENDAR_SUBCOLLECTION_ID2 = "calendar";
1346
1346
  var PractitionerAggregationService = class {
1347
- constructor(firestore13) {
1348
- this.db = firestore13 || admin4.firestore();
1347
+ constructor(firestore15) {
1348
+ this.db = firestore15 || admin4.firestore();
1349
1349
  }
1350
1350
  /**
1351
1351
  * Adds practitioner information to a clinic when a new practitioner is created
@@ -1680,8 +1680,8 @@ var PractitionerAggregationService = class {
1680
1680
  import * as admin5 from "firebase-admin";
1681
1681
  var CALENDAR_SUBCOLLECTION_ID3 = "calendar";
1682
1682
  var ProcedureAggregationService = class {
1683
- constructor(firestore13) {
1684
- this.db = firestore13 || admin5.firestore();
1683
+ constructor(firestore15) {
1684
+ this.db = firestore15 || admin5.firestore();
1685
1685
  }
1686
1686
  /**
1687
1687
  * Adds procedure information to a practitioner when a new procedure is created
@@ -2065,8 +2065,8 @@ var ProcedureAggregationService = class {
2065
2065
  import * as admin6 from "firebase-admin";
2066
2066
  var CALENDAR_SUBCOLLECTION_ID4 = "calendar";
2067
2067
  var PatientAggregationService = class {
2068
- constructor(firestore13) {
2069
- this.db = firestore13 || admin6.firestore();
2068
+ constructor(firestore15) {
2069
+ this.db = firestore15 || admin6.firestore();
2070
2070
  }
2071
2071
  // --- Methods for Patient Creation --- >
2072
2072
  // No specific aggregations defined for patient creation in the plan.
@@ -2198,8 +2198,8 @@ var PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME = "patientRequirements";
2198
2198
  // src/admin/requirements/patient-requirements.admin.service.ts
2199
2199
  import * as admin7 from "firebase-admin";
2200
2200
  var PatientRequirementsAdminService = class {
2201
- constructor(firestore13) {
2202
- this.db = firestore13 || admin7.firestore();
2201
+ constructor(firestore15) {
2202
+ this.db = firestore15 || admin7.firestore();
2203
2203
  this.notificationsAdmin = new NotificationsAdmin(this.db);
2204
2204
  }
2205
2205
  /**
@@ -2527,8 +2527,8 @@ var PatientRequirementsAdminService = class {
2527
2527
  // src/admin/calendar/calendar.admin.service.ts
2528
2528
  import * as admin8 from "firebase-admin";
2529
2529
  var CalendarAdminService = class {
2530
- constructor(firestore13) {
2531
- this.db = firestore13 || admin8.firestore();
2530
+ constructor(firestore15) {
2531
+ this.db = firestore15 || admin8.firestore();
2532
2532
  Logger.info("[CalendarAdminService] Initialized.");
2533
2533
  }
2534
2534
  /**
@@ -2811,9 +2811,9 @@ var BaseMailingService = class {
2811
2811
  * @param firestore Firestore instance provided by the caller
2812
2812
  * @param mailgunClient Mailgun client instance (mailgun.js v10+) provided by the caller
2813
2813
  */
2814
- constructor(firestore13, mailgunClient) {
2814
+ constructor(firestore15, mailgunClient) {
2815
2815
  var _a;
2816
- this.db = firestore13;
2816
+ this.db = firestore15;
2817
2817
  this.mailgunClient = mailgunClient;
2818
2818
  if (!this.db) {
2819
2819
  Logger.error("[BaseMailingService] No Firestore instance provided");
@@ -2957,8 +2957,8 @@ var BaseMailingService = class {
2957
2957
  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>";
2958
2958
  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>";
2959
2959
  var AppointmentMailingService = class extends BaseMailingService {
2960
- constructor(firestore13, mailgunClient) {
2961
- super(firestore13, mailgunClient);
2960
+ constructor(firestore15, mailgunClient) {
2961
+ super(firestore15, mailgunClient);
2962
2962
  this.DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
2963
2963
  Logger.info("[AppointmentMailingService] Initialized.");
2964
2964
  }
@@ -3107,8 +3107,8 @@ var AppointmentAggregationService = class {
3107
3107
  * @param mailgunClient - An initialized Mailgun client instance.
3108
3108
  * @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
3109
3109
  */
3110
- constructor(mailgunClient, firestore13) {
3111
- this.db = firestore13 || admin10.firestore();
3110
+ constructor(mailgunClient, firestore15) {
3111
+ this.db = firestore15 || admin10.firestore();
3112
3112
  this.appointmentMailingService = new AppointmentMailingService(
3113
3113
  this.db,
3114
3114
  mailgunClient
@@ -4357,6 +4357,658 @@ var AppointmentAggregationService = class {
4357
4357
  }
4358
4358
  };
4359
4359
 
4360
+ // src/admin/aggregation/forms/filled-forms.aggregation.service.ts
4361
+ import * as admin11 from "firebase-admin";
4362
+ var FilledFormsAggregationService = class {
4363
+ /**
4364
+ * Constructor for FilledFormsAggregationService.
4365
+ * @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
4366
+ */
4367
+ constructor(firestore15) {
4368
+ this.db = firestore15 || admin11.firestore();
4369
+ Logger.info("[FilledFormsAggregationService] Initialized");
4370
+ }
4371
+ /**
4372
+ * Handles side effects when a filled form is created or updated.
4373
+ * This function would typically be called by a Firestore onCreate or onUpdate trigger.
4374
+ * @param filledDocument The filled document that was created or updated.
4375
+ * @returns {Promise<void>}
4376
+ */
4377
+ async handleFilledFormCreateOrUpdate(filledDocument) {
4378
+ Logger.info(
4379
+ `[FilledFormsAggregationService] Handling CREATE/UPDATE for filled form: ${filledDocument.id}, appointment: ${filledDocument.appointmentId}`
4380
+ );
4381
+ try {
4382
+ const appointmentRef = this.db.collection(APPOINTMENTS_COLLECTION).doc(filledDocument.appointmentId);
4383
+ const appointmentDoc = await appointmentRef.get();
4384
+ if (!appointmentDoc.exists) {
4385
+ Logger.error(
4386
+ `[FilledFormsAggregationService] Appointment ${filledDocument.appointmentId} not found.`
4387
+ );
4388
+ return;
4389
+ }
4390
+ const appointment = appointmentDoc.data();
4391
+ const formSubcollection = filledDocument.isUserForm ? USER_FORMS_SUBCOLLECTION : DOCTOR_FORMS_SUBCOLLECTION;
4392
+ const linkedFormInfo = {
4393
+ formId: filledDocument.id,
4394
+ templateId: filledDocument.templateId,
4395
+ templateVersion: filledDocument.templateVersion,
4396
+ title: filledDocument.isUserForm ? "User Form" : "Doctor Form",
4397
+ // Default title if not available
4398
+ isUserForm: filledDocument.isUserForm,
4399
+ isRequired: filledDocument.isRequired,
4400
+ status: filledDocument.status,
4401
+ path: `${APPOINTMENTS_COLLECTION}/${filledDocument.appointmentId}/${formSubcollection}/${filledDocument.id}`
4402
+ };
4403
+ if (filledDocument.updatedAt) {
4404
+ linkedFormInfo.submittedAt = admin11.firestore.Timestamp.fromMillis(
4405
+ filledDocument.updatedAt
4406
+ );
4407
+ }
4408
+ if (filledDocument.status === "completed" /* COMPLETED */ || filledDocument.status === "signed" /* SIGNED */) {
4409
+ linkedFormInfo.completedAt = admin11.firestore.Timestamp.fromMillis(
4410
+ filledDocument.updatedAt
4411
+ );
4412
+ }
4413
+ let updateData = {};
4414
+ let existingFormIndex = -1;
4415
+ if (appointment.linkedForms && appointment.linkedForms.length > 0) {
4416
+ existingFormIndex = appointment.linkedForms.findIndex(
4417
+ (form) => form.formId === filledDocument.id
4418
+ );
4419
+ }
4420
+ if (existingFormIndex >= 0 && appointment.linkedForms) {
4421
+ updateData = {
4422
+ linkedForms: admin11.firestore.FieldValue.arrayRemove(
4423
+ appointment.linkedForms[existingFormIndex]
4424
+ ),
4425
+ updatedAt: admin11.firestore.FieldValue.serverTimestamp()
4426
+ };
4427
+ await appointmentRef.update(updateData);
4428
+ updateData = {
4429
+ linkedForms: admin11.firestore.FieldValue.arrayUnion(linkedFormInfo),
4430
+ updatedAt: admin11.firestore.FieldValue.serverTimestamp()
4431
+ };
4432
+ } else {
4433
+ updateData = {
4434
+ linkedForms: appointment.linkedForms ? admin11.firestore.FieldValue.arrayUnion(linkedFormInfo) : [linkedFormInfo],
4435
+ // If linkedForms doesn't exist, create a new array
4436
+ linkedFormIds: appointment.linkedFormIds ? admin11.firestore.FieldValue.arrayUnion(filledDocument.id) : [filledDocument.id],
4437
+ // If linkedFormIds doesn't exist, create a new array
4438
+ updatedAt: admin11.firestore.FieldValue.serverTimestamp()
4439
+ };
4440
+ }
4441
+ if (filledDocument.isUserForm && filledDocument.isRequired && (filledDocument.status === "completed" /* COMPLETED */ || filledDocument.status === "signed" /* SIGNED */)) {
4442
+ if (appointment.pendingUserFormsIds && appointment.pendingUserFormsIds.includes(filledDocument.id)) {
4443
+ updateData.pendingUserFormsIds = admin11.firestore.FieldValue.arrayRemove(filledDocument.id);
4444
+ Logger.info(
4445
+ `[FilledFormsAggregationService] Removing form ${filledDocument.id} from pendingUserFormsIds`
4446
+ );
4447
+ }
4448
+ }
4449
+ await appointmentRef.update(updateData);
4450
+ Logger.info(
4451
+ `[FilledFormsAggregationService] Successfully updated appointment ${filledDocument.appointmentId} with form info for ${filledDocument.id}`
4452
+ );
4453
+ } catch (error) {
4454
+ Logger.error(
4455
+ `[FilledFormsAggregationService] Error updating appointment for filled form ${filledDocument.id}:`,
4456
+ error
4457
+ );
4458
+ throw error;
4459
+ }
4460
+ }
4461
+ /**
4462
+ * Handles side effects when a filled form is deleted.
4463
+ * This function would typically be called by a Firestore onDelete trigger.
4464
+ * @param filledDocument The filled document that was deleted.
4465
+ * @returns {Promise<void>}
4466
+ */
4467
+ async handleFilledFormDelete(filledDocument) {
4468
+ Logger.info(
4469
+ `[FilledFormsAggregationService] Handling DELETE for filled form: ${filledDocument.id}, appointment: ${filledDocument.appointmentId}`
4470
+ );
4471
+ try {
4472
+ const appointmentRef = this.db.collection(APPOINTMENTS_COLLECTION).doc(filledDocument.appointmentId);
4473
+ const appointmentDoc = await appointmentRef.get();
4474
+ if (!appointmentDoc.exists) {
4475
+ Logger.error(
4476
+ `[FilledFormsAggregationService] Appointment ${filledDocument.appointmentId} not found.`
4477
+ );
4478
+ return;
4479
+ }
4480
+ const appointment = appointmentDoc.data();
4481
+ let updateData = {};
4482
+ if (appointment.linkedForms && appointment.linkedForms.length > 0) {
4483
+ const formToRemove = appointment.linkedForms.find(
4484
+ (form) => form.formId === filledDocument.id
4485
+ );
4486
+ if (formToRemove) {
4487
+ updateData.linkedForms = admin11.firestore.FieldValue.arrayRemove(formToRemove);
4488
+ }
4489
+ }
4490
+ if (appointment.linkedFormIds && appointment.linkedFormIds.includes(filledDocument.id)) {
4491
+ updateData.linkedFormIds = admin11.firestore.FieldValue.arrayRemove(
4492
+ filledDocument.id
4493
+ );
4494
+ }
4495
+ if (filledDocument.isUserForm && filledDocument.isRequired) {
4496
+ if (filledDocument.status !== "completed" /* COMPLETED */ && filledDocument.status !== "signed" /* SIGNED */) {
4497
+ if (!appointment.pendingUserFormsIds || !appointment.pendingUserFormsIds.includes(filledDocument.id)) {
4498
+ updateData.pendingUserFormsIds = appointment.pendingUserFormsIds ? admin11.firestore.FieldValue.arrayUnion(filledDocument.id) : [filledDocument.id];
4499
+ }
4500
+ }
4501
+ }
4502
+ if (Object.keys(updateData).length > 0) {
4503
+ updateData.updatedAt = admin11.firestore.FieldValue.serverTimestamp();
4504
+ await appointmentRef.update(updateData);
4505
+ Logger.info(
4506
+ `[FilledFormsAggregationService] Successfully updated appointment ${filledDocument.appointmentId} after form deletion`
4507
+ );
4508
+ } else {
4509
+ Logger.info(
4510
+ `[FilledFormsAggregationService] No updates needed for appointment ${filledDocument.appointmentId}`
4511
+ );
4512
+ }
4513
+ } catch (error) {
4514
+ Logger.error(
4515
+ `[FilledFormsAggregationService] Error updating appointment after form deletion for ${filledDocument.id}:`,
4516
+ error
4517
+ );
4518
+ throw error;
4519
+ }
4520
+ }
4521
+ /**
4522
+ * Updates the appointment's pendingUserFormsIds for a new required user form.
4523
+ * This should be called when a required user form is first created.
4524
+ * @param filledDocument The newly created filled document.
4525
+ * @returns {Promise<void>}
4526
+ */
4527
+ async handleRequiredUserFormCreate(filledDocument) {
4528
+ if (!filledDocument.isUserForm || !filledDocument.isRequired) {
4529
+ return;
4530
+ }
4531
+ Logger.info(
4532
+ `[FilledFormsAggregationService] Handling new required user form creation: ${filledDocument.id}`
4533
+ );
4534
+ try {
4535
+ const appointmentRef = this.db.collection(APPOINTMENTS_COLLECTION).doc(filledDocument.appointmentId);
4536
+ if (filledDocument.status === "completed" /* COMPLETED */ || filledDocument.status === "signed" /* SIGNED */) {
4537
+ Logger.info(
4538
+ `[FilledFormsAggregationService] Form ${filledDocument.id} is already completed/signed, not adding to pendingUserFormsIds`
4539
+ );
4540
+ return;
4541
+ }
4542
+ await appointmentRef.update({
4543
+ pendingUserFormsIds: admin11.firestore.FieldValue.arrayUnion(
4544
+ filledDocument.id
4545
+ ),
4546
+ updatedAt: admin11.firestore.FieldValue.serverTimestamp()
4547
+ });
4548
+ Logger.info(
4549
+ `[FilledFormsAggregationService] Successfully added form ${filledDocument.id} to pendingUserFormsIds for appointment ${filledDocument.appointmentId}`
4550
+ );
4551
+ } catch (error) {
4552
+ Logger.error(
4553
+ `[FilledFormsAggregationService] Error handling required user form creation for ${filledDocument.id}:`,
4554
+ error
4555
+ );
4556
+ throw error;
4557
+ }
4558
+ }
4559
+ };
4560
+
4561
+ // src/admin/aggregation/reviews/reviews.aggregation.service.ts
4562
+ import * as admin12 from "firebase-admin";
4563
+
4564
+ // src/types/reviews/index.ts
4565
+ var REVIEWS_COLLECTION = "reviews";
4566
+
4567
+ // src/admin/aggregation/reviews/reviews.aggregation.service.ts
4568
+ var ReviewsAggregationService = class {
4569
+ /**
4570
+ * Constructor for ReviewsAggregationService.
4571
+ * @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
4572
+ */
4573
+ constructor(firestore15) {
4574
+ this.db = firestore15 || admin12.firestore();
4575
+ }
4576
+ /**
4577
+ * Process a newly created review and update all related entities
4578
+ * @param review The newly created review
4579
+ * @returns Promise resolving when all updates are complete
4580
+ */
4581
+ async processNewReview(review) {
4582
+ console.log(
4583
+ `[ReviewsAggregationService] Processing new review: ${review.id}`
4584
+ );
4585
+ const updatePromises = [];
4586
+ if (review.clinicReview) {
4587
+ updatePromises.push(
4588
+ this.updateClinicReviewInfo(review.clinicReview.clinicId)
4589
+ );
4590
+ }
4591
+ if (review.practitionerReview) {
4592
+ updatePromises.push(
4593
+ this.updatePractitionerReviewInfo(
4594
+ review.practitionerReview.practitionerId
4595
+ )
4596
+ );
4597
+ }
4598
+ if (review.procedureReview) {
4599
+ updatePromises.push(
4600
+ this.updateProcedureReviewInfo(review.procedureReview.procedureId)
4601
+ );
4602
+ }
4603
+ await Promise.all(updatePromises);
4604
+ console.log(
4605
+ `[ReviewsAggregationService] Successfully processed review: ${review.id}`
4606
+ );
4607
+ }
4608
+ /**
4609
+ * Process a deleted review and update all related entities
4610
+ * @param review The deleted review
4611
+ * @returns Promise resolving when all updates are complete
4612
+ */
4613
+ async processDeletedReview(review) {
4614
+ console.log(
4615
+ `[ReviewsAggregationService] Processing deleted review: ${review.id}`
4616
+ );
4617
+ const updatePromises = [];
4618
+ if (review.clinicReview) {
4619
+ updatePromises.push(
4620
+ this.updateClinicReviewInfo(
4621
+ review.clinicReview.clinicId,
4622
+ review.clinicReview,
4623
+ true
4624
+ )
4625
+ );
4626
+ }
4627
+ if (review.practitionerReview) {
4628
+ updatePromises.push(
4629
+ this.updatePractitionerReviewInfo(
4630
+ review.practitionerReview.practitionerId,
4631
+ review.practitionerReview,
4632
+ true
4633
+ )
4634
+ );
4635
+ }
4636
+ if (review.procedureReview) {
4637
+ updatePromises.push(
4638
+ this.updateProcedureReviewInfo(
4639
+ review.procedureReview.procedureId,
4640
+ review.procedureReview,
4641
+ true
4642
+ )
4643
+ );
4644
+ }
4645
+ await Promise.all(updatePromises);
4646
+ console.log(
4647
+ `[ReviewsAggregationService] Successfully processed deleted review: ${review.id}`
4648
+ );
4649
+ }
4650
+ /**
4651
+ * Updates the review info for a clinic
4652
+ * @param clinicId The ID of the clinic to update
4653
+ * @param removedReview Optional review being removed
4654
+ * @param isRemoval Whether this update is for a review removal
4655
+ * @returns The updated clinic review info
4656
+ */
4657
+ async updateClinicReviewInfo(clinicId, removedReview, isRemoval = false) {
4658
+ console.log(
4659
+ `[ReviewsAggregationService] Updating review info for clinic: ${clinicId}`
4660
+ );
4661
+ const clinicDoc = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
4662
+ if (!clinicDoc.exists) {
4663
+ console.error(
4664
+ `[ReviewsAggregationService] Clinic with ID ${clinicId} not found`
4665
+ );
4666
+ throw new Error(`Clinic with ID ${clinicId} not found`);
4667
+ }
4668
+ const clinicData = clinicDoc.data();
4669
+ const currentReviewInfo = (clinicData == null ? void 0 : clinicData.reviewInfo) || {
4670
+ totalReviews: 0,
4671
+ averageRating: 0,
4672
+ cleanliness: 0,
4673
+ facilities: 0,
4674
+ staffFriendliness: 0,
4675
+ waitingTime: 0,
4676
+ accessibility: 0,
4677
+ recommendationPercentage: 0
4678
+ };
4679
+ const reviewsQuery = await this.db.collection(REVIEWS_COLLECTION).where("clinicReview.clinicId", "==", clinicId).get();
4680
+ if (isRemoval && reviewsQuery.size <= 1 || reviewsQuery.empty) {
4681
+ const updatedReviewInfo2 = {
4682
+ totalReviews: 0,
4683
+ averageRating: 0,
4684
+ cleanliness: 0,
4685
+ facilities: 0,
4686
+ staffFriendliness: 0,
4687
+ waitingTime: 0,
4688
+ accessibility: 0,
4689
+ recommendationPercentage: 0
4690
+ };
4691
+ await this.db.collection(CLINICS_COLLECTION).doc(clinicId).update({
4692
+ reviewInfo: updatedReviewInfo2,
4693
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
4694
+ });
4695
+ console.log(
4696
+ `[ReviewsAggregationService] Reset review info for clinic: ${clinicId}`
4697
+ );
4698
+ return updatedReviewInfo2;
4699
+ }
4700
+ const reviews = reviewsQuery.docs.map((doc) => doc.data());
4701
+ const clinicReviews = reviews.map((review) => review.clinicReview).filter((review) => review !== void 0);
4702
+ let totalRating = 0;
4703
+ let totalCleanliness = 0;
4704
+ let totalFacilities = 0;
4705
+ let totalStaffFriendliness = 0;
4706
+ let totalWaitingTime = 0;
4707
+ let totalAccessibility = 0;
4708
+ let totalRecommendations = 0;
4709
+ clinicReviews.forEach((review) => {
4710
+ totalRating += review.overallRating;
4711
+ totalCleanliness += review.cleanliness;
4712
+ totalFacilities += review.facilities;
4713
+ totalStaffFriendliness += review.staffFriendliness;
4714
+ totalWaitingTime += review.waitingTime;
4715
+ totalAccessibility += review.accessibility;
4716
+ if (review.wouldRecommend) totalRecommendations++;
4717
+ });
4718
+ const count = clinicReviews.length;
4719
+ const roundToOneDecimal = (value) => Math.round(value / count * 10) / 10;
4720
+ const updatedReviewInfo = {
4721
+ totalReviews: count,
4722
+ averageRating: roundToOneDecimal(totalRating),
4723
+ cleanliness: roundToOneDecimal(totalCleanliness),
4724
+ facilities: roundToOneDecimal(totalFacilities),
4725
+ staffFriendliness: roundToOneDecimal(totalStaffFriendliness),
4726
+ waitingTime: roundToOneDecimal(totalWaitingTime),
4727
+ accessibility: roundToOneDecimal(totalAccessibility),
4728
+ recommendationPercentage: Math.round(totalRecommendations / count * 1e3) / 10
4729
+ };
4730
+ await this.db.collection(CLINICS_COLLECTION).doc(clinicId).update({
4731
+ reviewInfo: updatedReviewInfo,
4732
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
4733
+ });
4734
+ console.log(
4735
+ `[ReviewsAggregationService] Updated review info for clinic: ${clinicId}`
4736
+ );
4737
+ return updatedReviewInfo;
4738
+ }
4739
+ /**
4740
+ * Updates the review info for a practitioner
4741
+ * @param practitionerId The ID of the practitioner to update
4742
+ * @param removedReview Optional review being removed
4743
+ * @param isRemoval Whether this update is for a review removal
4744
+ * @returns The updated practitioner review info
4745
+ */
4746
+ async updatePractitionerReviewInfo(practitionerId, removedReview, isRemoval = false) {
4747
+ console.log(
4748
+ `[ReviewsAggregationService] Updating review info for practitioner: ${practitionerId}`
4749
+ );
4750
+ const practitionerDoc = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
4751
+ if (!practitionerDoc.exists) {
4752
+ console.error(
4753
+ `[ReviewsAggregationService] Practitioner with ID ${practitionerId} not found`
4754
+ );
4755
+ throw new Error(`Practitioner with ID ${practitionerId} not found`);
4756
+ }
4757
+ const practitionerData = practitionerDoc.data();
4758
+ const currentReviewInfo = (practitionerData == null ? void 0 : practitionerData.reviewInfo) || {
4759
+ totalReviews: 0,
4760
+ averageRating: 0,
4761
+ knowledgeAndExpertise: 0,
4762
+ communicationSkills: 0,
4763
+ bedSideManner: 0,
4764
+ thoroughness: 0,
4765
+ trustworthiness: 0,
4766
+ recommendationPercentage: 0
4767
+ };
4768
+ const reviewsQuery = await this.db.collection(REVIEWS_COLLECTION).where("practitionerReview.practitionerId", "==", practitionerId).get();
4769
+ if (isRemoval && reviewsQuery.size <= 1 || reviewsQuery.empty) {
4770
+ const updatedReviewInfo2 = {
4771
+ totalReviews: 0,
4772
+ averageRating: 0,
4773
+ knowledgeAndExpertise: 0,
4774
+ communicationSkills: 0,
4775
+ bedSideManner: 0,
4776
+ thoroughness: 0,
4777
+ trustworthiness: 0,
4778
+ recommendationPercentage: 0
4779
+ };
4780
+ await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).update({
4781
+ reviewInfo: updatedReviewInfo2,
4782
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
4783
+ });
4784
+ await this.updateDoctorInfoInProcedures(practitionerId, 0);
4785
+ console.log(
4786
+ `[ReviewsAggregationService] Reset review info for practitioner: ${practitionerId}`
4787
+ );
4788
+ return updatedReviewInfo2;
4789
+ }
4790
+ const reviews = reviewsQuery.docs.map((doc) => doc.data());
4791
+ const practitionerReviews = reviews.map((review) => review.practitionerReview).filter((review) => review !== void 0);
4792
+ let totalRating = 0;
4793
+ let totalKnowledgeAndExpertise = 0;
4794
+ let totalCommunicationSkills = 0;
4795
+ let totalBedSideManner = 0;
4796
+ let totalThoroughness = 0;
4797
+ let totalTrustworthiness = 0;
4798
+ let totalRecommendations = 0;
4799
+ practitionerReviews.forEach((review) => {
4800
+ totalRating += review.overallRating;
4801
+ totalKnowledgeAndExpertise += review.knowledgeAndExpertise;
4802
+ totalCommunicationSkills += review.communicationSkills;
4803
+ totalBedSideManner += review.bedSideManner;
4804
+ totalThoroughness += review.thoroughness;
4805
+ totalTrustworthiness += review.trustworthiness;
4806
+ if (review.wouldRecommend) totalRecommendations++;
4807
+ });
4808
+ const count = practitionerReviews.length;
4809
+ const roundToOneDecimal = (value) => Math.round(value / count * 10) / 10;
4810
+ const updatedReviewInfo = {
4811
+ totalReviews: count,
4812
+ averageRating: roundToOneDecimal(totalRating),
4813
+ knowledgeAndExpertise: roundToOneDecimal(totalKnowledgeAndExpertise),
4814
+ communicationSkills: roundToOneDecimal(totalCommunicationSkills),
4815
+ bedSideManner: roundToOneDecimal(totalBedSideManner),
4816
+ thoroughness: roundToOneDecimal(totalThoroughness),
4817
+ trustworthiness: roundToOneDecimal(totalTrustworthiness),
4818
+ recommendationPercentage: Math.round(totalRecommendations / count * 1e3) / 10
4819
+ };
4820
+ await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).update({
4821
+ reviewInfo: updatedReviewInfo,
4822
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
4823
+ });
4824
+ await this.updateDoctorInfoInProcedures(
4825
+ practitionerId,
4826
+ updatedReviewInfo.averageRating
4827
+ );
4828
+ console.log(
4829
+ `[ReviewsAggregationService] Updated review info for practitioner: ${practitionerId}`
4830
+ );
4831
+ return updatedReviewInfo;
4832
+ }
4833
+ /**
4834
+ * Updates the review info for a procedure
4835
+ * @param procedureId The ID of the procedure to update
4836
+ * @param removedReview Optional review being removed
4837
+ * @param isRemoval Whether this update is for a review removal
4838
+ * @returns The updated procedure review info
4839
+ */
4840
+ async updateProcedureReviewInfo(procedureId, removedReview, isRemoval = false) {
4841
+ console.log(
4842
+ `[ReviewsAggregationService] Updating review info for procedure: ${procedureId}`
4843
+ );
4844
+ const procedureDoc = await this.db.collection(PROCEDURES_COLLECTION).doc(procedureId).get();
4845
+ if (!procedureDoc.exists) {
4846
+ console.error(
4847
+ `[ReviewsAggregationService] Procedure with ID ${procedureId} not found`
4848
+ );
4849
+ throw new Error(`Procedure with ID ${procedureId} not found`);
4850
+ }
4851
+ const procedureData = procedureDoc.data();
4852
+ const currentReviewInfo = (procedureData == null ? void 0 : procedureData.reviewInfo) || {
4853
+ totalReviews: 0,
4854
+ averageRating: 0,
4855
+ effectivenessOfTreatment: 0,
4856
+ outcomeExplanation: 0,
4857
+ painManagement: 0,
4858
+ followUpCare: 0,
4859
+ valueForMoney: 0,
4860
+ recommendationPercentage: 0
4861
+ };
4862
+ const reviewsQuery = await this.db.collection(REVIEWS_COLLECTION).where("procedureReview.procedureId", "==", procedureId).get();
4863
+ if (isRemoval && reviewsQuery.size <= 1 || reviewsQuery.empty) {
4864
+ const updatedReviewInfo2 = {
4865
+ totalReviews: 0,
4866
+ averageRating: 0,
4867
+ effectivenessOfTreatment: 0,
4868
+ outcomeExplanation: 0,
4869
+ painManagement: 0,
4870
+ followUpCare: 0,
4871
+ valueForMoney: 0,
4872
+ recommendationPercentage: 0
4873
+ };
4874
+ await this.db.collection(PROCEDURES_COLLECTION).doc(procedureId).update({
4875
+ reviewInfo: updatedReviewInfo2,
4876
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
4877
+ });
4878
+ console.log(
4879
+ `[ReviewsAggregationService] Reset review info for procedure: ${procedureId}`
4880
+ );
4881
+ return updatedReviewInfo2;
4882
+ }
4883
+ const reviews = reviewsQuery.docs.map((doc) => doc.data());
4884
+ const procedureReviews = reviews.map((review) => review.procedureReview).filter((review) => review !== void 0);
4885
+ let totalRating = 0;
4886
+ let totalEffectivenessOfTreatment = 0;
4887
+ let totalOutcomeExplanation = 0;
4888
+ let totalPainManagement = 0;
4889
+ let totalFollowUpCare = 0;
4890
+ let totalValueForMoney = 0;
4891
+ let totalRecommendations = 0;
4892
+ procedureReviews.forEach((review) => {
4893
+ totalRating += review.overallRating;
4894
+ totalEffectivenessOfTreatment += review.effectivenessOfTreatment;
4895
+ totalOutcomeExplanation += review.outcomeExplanation;
4896
+ totalPainManagement += review.painManagement;
4897
+ totalFollowUpCare += review.followUpCare;
4898
+ totalValueForMoney += review.valueForMoney;
4899
+ if (review.wouldRecommend) totalRecommendations++;
4900
+ });
4901
+ const count = procedureReviews.length;
4902
+ const roundToOneDecimal = (value) => Math.round(value / count * 10) / 10;
4903
+ const updatedReviewInfo = {
4904
+ totalReviews: count,
4905
+ averageRating: roundToOneDecimal(totalRating),
4906
+ effectivenessOfTreatment: roundToOneDecimal(
4907
+ totalEffectivenessOfTreatment
4908
+ ),
4909
+ outcomeExplanation: roundToOneDecimal(totalOutcomeExplanation),
4910
+ painManagement: roundToOneDecimal(totalPainManagement),
4911
+ followUpCare: roundToOneDecimal(totalFollowUpCare),
4912
+ valueForMoney: roundToOneDecimal(totalValueForMoney),
4913
+ recommendationPercentage: Math.round(totalRecommendations / count * 1e3) / 10
4914
+ };
4915
+ await this.db.collection(PROCEDURES_COLLECTION).doc(procedureId).update({
4916
+ reviewInfo: updatedReviewInfo,
4917
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
4918
+ });
4919
+ console.log(
4920
+ `[ReviewsAggregationService] Updated review info for procedure: ${procedureId}`
4921
+ );
4922
+ return updatedReviewInfo;
4923
+ }
4924
+ /**
4925
+ * Updates doctorInfo rating in all procedures for a practitioner
4926
+ * @param practitionerId The ID of the practitioner
4927
+ * @param rating The new rating to set
4928
+ */
4929
+ async updateDoctorInfoInProcedures(practitionerId, rating) {
4930
+ console.log(
4931
+ `[ReviewsAggregationService] Updating doctor info in procedures for practitioner: ${practitionerId}`
4932
+ );
4933
+ const proceduresQuery = await this.db.collection(PROCEDURES_COLLECTION).where("practitionerId", "==", practitionerId).get();
4934
+ if (proceduresQuery.empty) {
4935
+ console.log(
4936
+ `[ReviewsAggregationService] No procedures found for practitioner: ${practitionerId}`
4937
+ );
4938
+ return;
4939
+ }
4940
+ const batch = this.db.batch();
4941
+ proceduresQuery.docs.forEach((docSnapshot) => {
4942
+ const procedureRef = this.db.collection(PROCEDURES_COLLECTION).doc(docSnapshot.id);
4943
+ batch.update(procedureRef, {
4944
+ "doctorInfo.rating": rating,
4945
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
4946
+ });
4947
+ });
4948
+ await batch.commit();
4949
+ console.log(
4950
+ `[ReviewsAggregationService] Updated doctor info in ${proceduresQuery.size} procedures for practitioner: ${practitionerId}`
4951
+ );
4952
+ }
4953
+ /**
4954
+ * Verifies a review as checked by admin/staff
4955
+ * @param reviewId The ID of the review to verify
4956
+ */
4957
+ async verifyReview(reviewId) {
4958
+ console.log(`[ReviewsAggregationService] Verifying review: ${reviewId}`);
4959
+ const reviewDoc = await this.db.collection(REVIEWS_COLLECTION).doc(reviewId).get();
4960
+ if (!reviewDoc.exists) {
4961
+ console.error(
4962
+ `[ReviewsAggregationService] Review with ID ${reviewId} not found`
4963
+ );
4964
+ throw new Error(`Review with ID ${reviewId} not found`);
4965
+ }
4966
+ const review = reviewDoc.data();
4967
+ const batch = this.db.batch();
4968
+ const reviewRef = this.db.collection(REVIEWS_COLLECTION).doc(reviewId);
4969
+ if (review.clinicReview) {
4970
+ review.clinicReview.isVerified = true;
4971
+ }
4972
+ if (review.practitionerReview) {
4973
+ review.practitionerReview.isVerified = true;
4974
+ }
4975
+ if (review.procedureReview) {
4976
+ review.procedureReview.isVerified = true;
4977
+ }
4978
+ batch.update(reviewRef, {
4979
+ clinicReview: review.clinicReview,
4980
+ practitionerReview: review.practitionerReview,
4981
+ procedureReview: review.procedureReview,
4982
+ updatedAt: admin12.firestore.FieldValue.serverTimestamp()
4983
+ });
4984
+ await batch.commit();
4985
+ console.log(
4986
+ `[ReviewsAggregationService] Successfully verified review: ${reviewId}`
4987
+ );
4988
+ }
4989
+ /**
4990
+ * Calculate the average of all reviews for an entity
4991
+ * @param entityId The entity ID
4992
+ * @param entityType The type of entity ('clinic', 'practitioner', or 'procedure')
4993
+ * @returns Promise that resolves to the calculated review info
4994
+ */
4995
+ async calculateEntityReviewInfo(entityId, entityType) {
4996
+ console.log(
4997
+ `[ReviewsAggregationService] Calculating review info for ${entityType}: ${entityId}`
4998
+ );
4999
+ switch (entityType) {
5000
+ case "clinic":
5001
+ return this.updateClinicReviewInfo(entityId);
5002
+ case "practitioner":
5003
+ return this.updatePractitionerReviewInfo(entityId);
5004
+ case "procedure":
5005
+ return this.updateProcedureReviewInfo(entityId);
5006
+ default:
5007
+ throw new Error(`Invalid entity type: ${entityType}`);
5008
+ }
5009
+ }
5010
+ };
5011
+
4360
5012
  // src/admin/mailing/practitionerInvite/templates/invitation.template.ts
4361
5013
  var practitionerInvitationTemplate = `
4362
5014
  <!DOCTYPE html>
@@ -4464,8 +5116,8 @@ var PractitionerInviteMailingService = class extends BaseMailingService {
4464
5116
  * @param firestore Firestore instance provided by the caller
4465
5117
  * @param mailgunClient Mailgun client instance (mailgun.js v10+) provided by the caller
4466
5118
  */
4467
- constructor(firestore13, mailgunClient) {
4468
- super(firestore13, mailgunClient);
5119
+ constructor(firestore15, mailgunClient) {
5120
+ super(firestore15, mailgunClient);
4469
5121
  this.DEFAULT_REGISTRATION_URL = "https://metaesthetics.net/register";
4470
5122
  this.DEFAULT_SUBJECT = "You've Been Invited to Join as a Practitioner";
4471
5123
  this.DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
@@ -4703,7 +5355,7 @@ var PractitionerInviteMailingService = class extends BaseMailingService {
4703
5355
  };
4704
5356
 
4705
5357
  // src/admin/booking/booking.admin.ts
4706
- import * as admin12 from "firebase-admin";
5358
+ import * as admin14 from "firebase-admin";
4707
5359
 
4708
5360
  // src/admin/booking/booking.calculator.ts
4709
5361
  import { Timestamp } from "firebase/firestore";
@@ -5137,10 +5789,10 @@ var BookingAvailabilityCalculator = class {
5137
5789
  BookingAvailabilityCalculator.DEFAULT_INTERVAL_MINUTES = 15;
5138
5790
 
5139
5791
  // src/admin/documentation-templates/document-manager.admin.ts
5140
- import * as admin11 from "firebase-admin";
5792
+ import * as admin13 from "firebase-admin";
5141
5793
  var DocumentManagerAdminService = class {
5142
- constructor(firestore13) {
5143
- this.db = firestore13;
5794
+ constructor(firestore15) {
5795
+ this.db = firestore15;
5144
5796
  }
5145
5797
  /**
5146
5798
  * Adds operations to a Firestore batch to initialize all linked forms for a new appointment
@@ -5243,7 +5895,7 @@ var DocumentManagerAdminService = class {
5243
5895
  };
5244
5896
  }
5245
5897
  const templateIds = technologyTemplates.map((t) => t.templateId);
5246
- const templatesSnapshot = await this.db.collection(DOCUMENTATION_TEMPLATES_COLLECTION).where(admin11.firestore.FieldPath.documentId(), "in", templateIds).get();
5898
+ const templatesSnapshot = await this.db.collection(DOCUMENTATION_TEMPLATES_COLLECTION).where(admin13.firestore.FieldPath.documentId(), "in", templateIds).get();
5247
5899
  const templatesMap = /* @__PURE__ */ new Map();
5248
5900
  templatesSnapshot.forEach((doc) => {
5249
5901
  templatesMap.set(doc.id, doc.data());
@@ -5309,8 +5961,8 @@ var BookingAdmin = class {
5309
5961
  * Creates a new BookingAdmin instance
5310
5962
  * @param firestore - Firestore instance provided by the caller
5311
5963
  */
5312
- constructor(firestore13) {
5313
- this.db = firestore13 || admin12.firestore();
5964
+ constructor(firestore15) {
5965
+ this.db = firestore15 || admin14.firestore();
5314
5966
  this.documentManagerAdmin = new DocumentManagerAdminService(this.db);
5315
5967
  }
5316
5968
  /**
@@ -5327,8 +5979,8 @@ var BookingAdmin = class {
5327
5979
  console.log(
5328
5980
  `[BookingAdmin] Getting available slots for clinic ${clinicId}, practitioner ${practitionerId}, procedure ${procedureId}`
5329
5981
  );
5330
- const start = timeframe.start instanceof Date ? admin12.firestore.Timestamp.fromDate(timeframe.start) : timeframe.start;
5331
- const end = timeframe.end instanceof Date ? admin12.firestore.Timestamp.fromDate(timeframe.end) : timeframe.end;
5982
+ const start = timeframe.start instanceof Date ? admin14.firestore.Timestamp.fromDate(timeframe.start) : timeframe.start;
5983
+ const end = timeframe.end instanceof Date ? admin14.firestore.Timestamp.fromDate(timeframe.end) : timeframe.end;
5332
5984
  const clinicDoc = await this.db.collection("clinics").doc(clinicId).get();
5333
5985
  if (!clinicDoc.exists) {
5334
5986
  throw new Error(`Clinic ${clinicId} not found`);
@@ -5367,7 +6019,7 @@ var BookingAdmin = class {
5367
6019
  const result = BookingAvailabilityCalculator.calculateSlots(request);
5368
6020
  return {
5369
6021
  availableSlots: result.availableSlots.map((slot) => ({
5370
- start: admin12.firestore.Timestamp.fromMillis(slot.start.toMillis())
6022
+ start: admin14.firestore.Timestamp.fromMillis(slot.start.toMillis())
5371
6023
  }))
5372
6024
  };
5373
6025
  } catch (error) {
@@ -5469,8 +6121,8 @@ var BookingAdmin = class {
5469
6121
  `[BookingAdmin] Orchestrating appointment creation for patient ${data.patientId} by user ${authenticatedUserId}`
5470
6122
  );
5471
6123
  const batch = this.db.batch();
5472
- const adminTsNow = admin12.firestore.Timestamp.now();
5473
- const serverTimestampValue = admin12.firestore.FieldValue.serverTimestamp();
6124
+ const adminTsNow = admin14.firestore.Timestamp.now();
6125
+ const serverTimestampValue = admin14.firestore.FieldValue.serverTimestamp();
5474
6126
  try {
5475
6127
  if (!data.patientId || !data.procedureId || !data.appointmentStartTime || !data.appointmentEndTime) {
5476
6128
  return {
@@ -5567,7 +6219,7 @@ var BookingAdmin = class {
5567
6219
  fullName: `${(patientSensitiveData == null ? void 0 : patientSensitiveData.firstName) || ""} ${(patientSensitiveData == null ? void 0 : patientSensitiveData.lastName) || ""}`.trim() || patientProfileData.displayName,
5568
6220
  email: (patientSensitiveData == null ? void 0 : patientSensitiveData.email) || "",
5569
6221
  phone: (patientSensitiveData == null ? void 0 : patientSensitiveData.phoneNumber) || patientProfileData.phoneNumber || null,
5570
- dateOfBirth: (patientSensitiveData == null ? void 0 : patientSensitiveData.dateOfBirth) || patientProfileData.dateOfBirth || admin12.firestore.Timestamp.now(),
6222
+ dateOfBirth: (patientSensitiveData == null ? void 0 : patientSensitiveData.dateOfBirth) || patientProfileData.dateOfBirth || admin14.firestore.Timestamp.now(),
5571
6223
  gender: (patientSensitiveData == null ? void 0 : patientSensitiveData.gender) || "other" /* OTHER */
5572
6224
  };
5573
6225
  const newAppointmentId = this.db.collection(APPOINTMENTS_COLLECTION).doc().id;
@@ -5817,6 +6469,7 @@ export {
5817
6469
  CalendarAdminService,
5818
6470
  ClinicAggregationService,
5819
6471
  DocumentManagerAdminService,
6472
+ FilledFormsAggregationService,
5820
6473
  Logger,
5821
6474
  MediaType,
5822
6475
  NOTIFICATIONS_COLLECTION,
@@ -5833,5 +6486,6 @@ export {
5833
6486
  PractitionerInviteMailingService,
5834
6487
  PractitionerTokenStatus,
5835
6488
  ProcedureAggregationService,
6489
+ ReviewsAggregationService,
5836
6490
  UserRole
5837
6491
  };