@blackcode_sa/metaestetics-api 1.7.3 → 1.7.5

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.
@@ -2600,23 +2600,81 @@ var CalendarAdminService = class {
2600
2600
  );
2601
2601
  const batch = this.db.batch();
2602
2602
  const serverTimestamp = admin8.firestore.FieldValue.serverTimestamp();
2603
- const practitionerCalendarEventPath = `${PRACTITIONERS_COLLECTION}/${appointment.practitionerId}/${CALENDAR_COLLECTION}/${appointment.calendarEventId}`;
2604
- if (appointment.calendarEventId) {
2605
- const practitionerEventRef = this.db.doc(practitionerCalendarEventPath);
2603
+ let updatesAdded = 0;
2604
+ const calendarEventId = appointment.calendarEventId;
2605
+ if (!calendarEventId) {
2606
+ Logger.warn(
2607
+ `[CalendarAdminService] Missing calendar event ID for appointment ${appointment.id}`
2608
+ );
2609
+ return;
2610
+ }
2611
+ if (appointment.practitionerId) {
2612
+ const practitionerEventRef = this.db.doc(
2613
+ `${PRACTITIONERS_COLLECTION}/${appointment.practitionerId}/${CALENDAR_COLLECTION}/${calendarEventId}`
2614
+ );
2606
2615
  batch.update(practitionerEventRef, {
2607
2616
  status: newStatus,
2608
2617
  updatedAt: serverTimestamp
2609
2618
  });
2619
+ updatesAdded++;
2620
+ Logger.debug(
2621
+ `[CalendarAdminService] Added practitioner calendar event status update to batch`
2622
+ );
2623
+ } else {
2624
+ Logger.warn(
2625
+ `[CalendarAdminService] Missing practitioner ID for appointment ${appointment.id}`
2626
+ );
2610
2627
  }
2611
- try {
2612
- await batch.commit();
2613
- Logger.info(
2614
- `[CalendarAdminService] Successfully updated calendar event statuses for appointment ${appointment.id}.`
2628
+ if (appointment.patientId) {
2629
+ const patientEventRef = this.db.doc(
2630
+ `${PATIENTS_COLLECTION}/${appointment.patientId}/${CALENDAR_COLLECTION}/${calendarEventId}`
2615
2631
  );
2616
- } catch (error) {
2617
- Logger.error(
2618
- `[CalendarAdminService] Error updating calendar event statuses for appointment ${appointment.id}:`,
2619
- error
2632
+ batch.update(patientEventRef, {
2633
+ status: newStatus,
2634
+ updatedAt: serverTimestamp
2635
+ });
2636
+ updatesAdded++;
2637
+ Logger.debug(
2638
+ `[CalendarAdminService] Added patient calendar event status update to batch`
2639
+ );
2640
+ } else {
2641
+ Logger.warn(
2642
+ `[CalendarAdminService] Missing patient ID for appointment ${appointment.id}`
2643
+ );
2644
+ }
2645
+ if (appointment.clinicBranchId) {
2646
+ const clinicEventRef = this.db.doc(
2647
+ `${CLINICS_COLLECTION}/${appointment.clinicBranchId}/${CALENDAR_COLLECTION}/${calendarEventId}`
2648
+ );
2649
+ batch.update(clinicEventRef, {
2650
+ status: newStatus,
2651
+ updatedAt: serverTimestamp
2652
+ });
2653
+ updatesAdded++;
2654
+ Logger.debug(
2655
+ `[CalendarAdminService] Added clinic calendar event status update to batch`
2656
+ );
2657
+ } else {
2658
+ Logger.warn(
2659
+ `[CalendarAdminService] Missing clinic ID for appointment ${appointment.id}`
2660
+ );
2661
+ }
2662
+ if (updatesAdded > 0) {
2663
+ try {
2664
+ await batch.commit();
2665
+ Logger.info(
2666
+ `[CalendarAdminService] Successfully updated ${updatesAdded} calendar event statuses for appointment ${appointment.id}.`
2667
+ );
2668
+ } catch (error) {
2669
+ Logger.error(
2670
+ `[CalendarAdminService] Error updating calendar event statuses for appointment ${appointment.id}:`,
2671
+ error
2672
+ );
2673
+ throw error;
2674
+ }
2675
+ } else {
2676
+ Logger.warn(
2677
+ `[CalendarAdminService] No calendar event status updates were made for appointment ${appointment.id}`
2620
2678
  );
2621
2679
  }
2622
2680
  }
@@ -2625,7 +2683,7 @@ var CalendarAdminService = class {
2625
2683
  * associated with a given appointment.
2626
2684
  *
2627
2685
  * @param appointment - The appointment object.
2628
- * @param newEventTime - The new CalendarEventTime object (using FirebaseClientTimestamp).
2686
+ * @param newEventTime - The new CalendarEventTime object (using admin.firestore.Timestamp).
2629
2687
  * @returns {Promise<void>} A promise that resolves when all updates are attempted.
2630
2688
  */
2631
2689
  async updateAppointmentCalendarEventsTime(appointment, newEventTime) {
@@ -2634,28 +2692,85 @@ var CalendarAdminService = class {
2634
2692
  );
2635
2693
  const batch = this.db.batch();
2636
2694
  const serverTimestamp = admin8.firestore.FieldValue.serverTimestamp();
2695
+ let updatesAdded = 0;
2696
+ const calendarEventId = appointment.calendarEventId;
2697
+ if (!calendarEventId) {
2698
+ Logger.warn(
2699
+ `[CalendarAdminService] Missing calendar event ID for appointment ${appointment.id}`
2700
+ );
2701
+ return;
2702
+ }
2637
2703
  const firestoreCompatibleEventTime = {
2638
- start: TimestampUtils.clientToAdmin(newEventTime.start) || admin8.firestore.Timestamp.now(),
2639
- end: TimestampUtils.clientToAdmin(newEventTime.end) || admin8.firestore.Timestamp.now()
2704
+ start: newEventTime.start,
2705
+ end: newEventTime.end
2640
2706
  };
2641
- if (appointment.calendarEventId) {
2707
+ if (appointment.practitionerId) {
2642
2708
  const practitionerEventRef = this.db.doc(
2643
- `${PRACTITIONERS_COLLECTION}/${appointment.practitionerId}/${CALENDAR_COLLECTION}/${appointment.calendarEventId}`
2709
+ `${PRACTITIONERS_COLLECTION}/${appointment.practitionerId}/${CALENDAR_COLLECTION}/${calendarEventId}`
2644
2710
  );
2645
2711
  batch.update(practitionerEventRef, {
2646
2712
  eventTime: firestoreCompatibleEventTime,
2647
2713
  updatedAt: serverTimestamp
2648
2714
  });
2715
+ updatesAdded++;
2716
+ Logger.debug(
2717
+ `[CalendarAdminService] Added practitioner calendar event time update to batch`
2718
+ );
2719
+ } else {
2720
+ Logger.warn(
2721
+ `[CalendarAdminService] Missing practitioner ID for appointment ${appointment.id}`
2722
+ );
2649
2723
  }
2650
- try {
2651
- await batch.commit();
2652
- Logger.info(
2653
- `[CalendarAdminService] Successfully updated calendar event times for appointment ${appointment.id}.`
2724
+ if (appointment.patientId) {
2725
+ const patientEventRef = this.db.doc(
2726
+ `${PATIENTS_COLLECTION}/${appointment.patientId}/${CALENDAR_COLLECTION}/${calendarEventId}`
2654
2727
  );
2655
- } catch (error) {
2656
- Logger.error(
2657
- `[CalendarAdminService] Error updating calendar event times for appointment ${appointment.id}:`,
2658
- error
2728
+ batch.update(patientEventRef, {
2729
+ eventTime: firestoreCompatibleEventTime,
2730
+ updatedAt: serverTimestamp
2731
+ });
2732
+ updatesAdded++;
2733
+ Logger.debug(
2734
+ `[CalendarAdminService] Added patient calendar event time update to batch`
2735
+ );
2736
+ } else {
2737
+ Logger.warn(
2738
+ `[CalendarAdminService] Missing patient ID for appointment ${appointment.id}`
2739
+ );
2740
+ }
2741
+ if (appointment.clinicBranchId) {
2742
+ const clinicEventRef = this.db.doc(
2743
+ `${CLINICS_COLLECTION}/${appointment.clinicBranchId}/${CALENDAR_COLLECTION}/${calendarEventId}`
2744
+ );
2745
+ batch.update(clinicEventRef, {
2746
+ eventTime: firestoreCompatibleEventTime,
2747
+ updatedAt: serverTimestamp
2748
+ });
2749
+ updatesAdded++;
2750
+ Logger.debug(
2751
+ `[CalendarAdminService] Added clinic calendar event time update to batch`
2752
+ );
2753
+ } else {
2754
+ Logger.warn(
2755
+ `[CalendarAdminService] Missing clinic ID for appointment ${appointment.id}`
2756
+ );
2757
+ }
2758
+ if (updatesAdded > 0) {
2759
+ try {
2760
+ await batch.commit();
2761
+ Logger.info(
2762
+ `[CalendarAdminService] Successfully updated ${updatesAdded} calendar event times for appointment ${appointment.id}.`
2763
+ );
2764
+ } catch (error) {
2765
+ Logger.error(
2766
+ `[CalendarAdminService] Error updating calendar event times for appointment ${appointment.id}:`,
2767
+ error
2768
+ );
2769
+ throw error;
2770
+ }
2771
+ } else {
2772
+ Logger.warn(
2773
+ `[CalendarAdminService] No calendar event time updates were made for appointment ${appointment.id}`
2659
2774
  );
2660
2775
  }
2661
2776
  }
@@ -2671,21 +2786,72 @@ var CalendarAdminService = class {
2671
2786
  `[CalendarAdminService] Deleting calendar events for appointment ${appointment.id}`
2672
2787
  );
2673
2788
  const batch = this.db.batch();
2674
- if (appointment.calendarEventId) {
2789
+ let deletesAdded = 0;
2790
+ const calendarEventId = appointment.calendarEventId;
2791
+ if (!calendarEventId) {
2792
+ Logger.warn(
2793
+ `[CalendarAdminService] Missing calendar event ID for appointment ${appointment.id}`
2794
+ );
2795
+ return;
2796
+ }
2797
+ if (appointment.practitionerId) {
2675
2798
  const practitionerEventRef = this.db.doc(
2676
- `${PRACTITIONERS_COLLECTION}/${appointment.practitionerId}/${CALENDAR_COLLECTION}/${appointment.calendarEventId}`
2799
+ `${PRACTITIONERS_COLLECTION}/${appointment.practitionerId}/${CALENDAR_COLLECTION}/${calendarEventId}`
2677
2800
  );
2678
2801
  batch.delete(practitionerEventRef);
2802
+ deletesAdded++;
2803
+ Logger.debug(
2804
+ `[CalendarAdminService] Added practitioner calendar event deletion to batch`
2805
+ );
2806
+ } else {
2807
+ Logger.warn(
2808
+ `[CalendarAdminService] Missing practitioner ID for appointment ${appointment.id}`
2809
+ );
2679
2810
  }
2680
- try {
2681
- await batch.commit();
2682
- Logger.info(
2683
- `[CalendarAdminService] Successfully deleted calendar events for appointment ${appointment.id}.`
2811
+ if (appointment.patientId) {
2812
+ const patientEventRef = this.db.doc(
2813
+ `${PATIENTS_COLLECTION}/${appointment.patientId}/${CALENDAR_COLLECTION}/${calendarEventId}`
2684
2814
  );
2685
- } catch (error) {
2686
- Logger.error(
2687
- `[CalendarAdminService] Error deleting calendar events for appointment ${appointment.id}:`,
2688
- error
2815
+ batch.delete(patientEventRef);
2816
+ deletesAdded++;
2817
+ Logger.debug(
2818
+ `[CalendarAdminService] Added patient calendar event deletion to batch`
2819
+ );
2820
+ } else {
2821
+ Logger.warn(
2822
+ `[CalendarAdminService] Missing patient ID for appointment ${appointment.id}`
2823
+ );
2824
+ }
2825
+ if (appointment.clinicBranchId) {
2826
+ const clinicEventRef = this.db.doc(
2827
+ `${CLINICS_COLLECTION}/${appointment.clinicBranchId}/${CALENDAR_COLLECTION}/${calendarEventId}`
2828
+ );
2829
+ batch.delete(clinicEventRef);
2830
+ deletesAdded++;
2831
+ Logger.debug(
2832
+ `[CalendarAdminService] Added clinic calendar event deletion to batch`
2833
+ );
2834
+ } else {
2835
+ Logger.warn(
2836
+ `[CalendarAdminService] Missing clinic ID for appointment ${appointment.id}`
2837
+ );
2838
+ }
2839
+ if (deletesAdded > 0) {
2840
+ try {
2841
+ await batch.commit();
2842
+ Logger.info(
2843
+ `[CalendarAdminService] Successfully deleted ${deletesAdded} calendar events for appointment ${appointment.id}.`
2844
+ );
2845
+ } catch (error) {
2846
+ Logger.error(
2847
+ `[CalendarAdminService] Error deleting calendar events for appointment ${appointment.id}:`,
2848
+ error
2849
+ );
2850
+ throw error;
2851
+ }
2852
+ } else {
2853
+ Logger.warn(
2854
+ `[CalendarAdminService] No calendar event deletions were made for appointment ${appointment.id}`
2689
2855
  );
2690
2856
  }
2691
2857
  }
@@ -3022,7 +3188,6 @@ var AppointmentAggregationService = class {
3022
3188
  `[AggService] Handling CREATE for appointment: ${appointment.id}, patient: ${appointment.patientId}, status: ${appointment.status}`
3023
3189
  );
3024
3190
  try {
3025
- await this.managePatientClinicPractitionerLinks(appointment, "create");
3026
3191
  const [
3027
3192
  patientProfile,
3028
3193
  patientSensitiveInfo,
@@ -3036,6 +3201,14 @@ var AppointmentAggregationService = class {
3036
3201
  this.fetchClinicInfo(appointment.clinicBranchId)
3037
3202
  // Needed for clinic admin notifications
3038
3203
  ]);
3204
+ if (patientProfile) {
3205
+ await this.managePatientClinicPractitionerLinks(
3206
+ patientProfile,
3207
+ appointment.practitionerId,
3208
+ appointment.clinicBranchId,
3209
+ "create"
3210
+ );
3211
+ }
3039
3212
  if (appointment.status === "confirmed" /* CONFIRMED */) {
3040
3213
  Logger.info(
3041
3214
  `[AggService] Appt ${appointment.id} created as CONFIRMED.`
@@ -3125,7 +3298,7 @@ var AppointmentAggregationService = class {
3125
3298
  * @returns {Promise<void>}
3126
3299
  */
3127
3300
  async handleAppointmentUpdate(before, after) {
3128
- var _a, _b;
3301
+ var _a, _b, _c;
3129
3302
  Logger.info(
3130
3303
  `[AggService] Handling UPDATE for appointment: ${after.id}. Status ${before.status} -> ${after.status}`
3131
3304
  );
@@ -3150,6 +3323,10 @@ var AppointmentAggregationService = class {
3150
3323
  if (before.status === "pending" /* PENDING */ && after.status === "confirmed" /* CONFIRMED */) {
3151
3324
  Logger.info(`[AggService] Appt ${after.id} PENDING -> CONFIRMED.`);
3152
3325
  await this.createPreAppointmentRequirementInstances(after);
3326
+ await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
3327
+ after,
3328
+ "confirmed" /* CONFIRMED */
3329
+ );
3153
3330
  if ((patientSensitiveInfo == null ? void 0 : patientSensitiveInfo.email) && patientProfile) {
3154
3331
  Logger.info(
3155
3332
  `[AggService] Sending appointment confirmed email to patient ${patientSensitiveInfo.email}`
@@ -3189,7 +3366,53 @@ var AppointmentAggregationService = class {
3189
3366
  };
3190
3367
  await this.appointmentMailingService.sendAppointmentConfirmedEmail(
3191
3368
  practitionerEmailData
3192
- // TODO: Properly import PractitionerProfileInfo and ensure type compatibility
3369
+ );
3370
+ }
3371
+ } else if (before.status === "rescheduled_by_clinic" /* RESCHEDULED_BY_CLINIC */ && after.status === "confirmed" /* CONFIRMED */) {
3372
+ Logger.info(
3373
+ `[AggService] Appt ${after.id} RESCHEDULED_BY_CLINIC -> CONFIRMED.`
3374
+ );
3375
+ await this.updateRelatedPatientRequirementInstances(
3376
+ before,
3377
+ "supersededReschedule" /* SUPERSEDED_RESCHEDULE */
3378
+ );
3379
+ await this.createPreAppointmentRequirementInstances(after);
3380
+ await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
3381
+ after,
3382
+ "confirmed" /* CONFIRMED */
3383
+ );
3384
+ if ((patientSensitiveInfo == null ? void 0 : patientSensitiveInfo.email) && patientProfile) {
3385
+ Logger.info(
3386
+ `[AggService] Sending appointment confirmed email to patient ${patientSensitiveInfo.email}`
3387
+ );
3388
+ const emailData = {
3389
+ appointment: after,
3390
+ recipientProfile: after.patientInfo,
3391
+ recipientRole: "patient"
3392
+ };
3393
+ await this.appointmentMailingService.sendAppointmentConfirmedEmail(
3394
+ emailData
3395
+ );
3396
+ }
3397
+ if ((patientProfile == null ? void 0 : patientProfile.expoTokens) && patientProfile.expoTokens.length > 0) {
3398
+ await this.notificationsAdmin.sendAppointmentConfirmedPush(
3399
+ after,
3400
+ after.patientId,
3401
+ patientProfile.expoTokens,
3402
+ "patient" /* PATIENT */
3403
+ );
3404
+ }
3405
+ if ((_b = practitionerProfile == null ? void 0 : practitionerProfile.basicInfo) == null ? void 0 : _b.email) {
3406
+ Logger.info(
3407
+ `[AggService] Sending appointment confirmation email to practitioner ${practitionerProfile.basicInfo.email}`
3408
+ );
3409
+ const practitionerEmailData = {
3410
+ appointment: after,
3411
+ recipientProfile: after.practitionerInfo,
3412
+ recipientRole: "practitioner"
3413
+ };
3414
+ await this.appointmentMailingService.sendAppointmentConfirmedEmail(
3415
+ practitionerEmailData
3193
3416
  );
3194
3417
  }
3195
3418
  } else if (after.status === "canceled_clinic" /* CANCELED_CLINIC */ || after.status === "canceled_patient" /* CANCELED_PATIENT */ || after.status === "canceled_patient_rescheduled" /* CANCELED_PATIENT_RESCHEDULED */ || after.status === "no_show" /* NO_SHOW */) {
@@ -3200,7 +3423,33 @@ var AppointmentAggregationService = class {
3200
3423
  after,
3201
3424
  "cancelledAppointment" /* CANCELLED_APPOINTMENT */
3202
3425
  );
3203
- await this.managePatientClinicPractitionerLinks(after, "cancel");
3426
+ if (patientProfile) {
3427
+ await this.managePatientClinicPractitionerLinks(
3428
+ patientProfile,
3429
+ after.practitionerId,
3430
+ after.clinicBranchId,
3431
+ "cancel",
3432
+ after.status
3433
+ );
3434
+ }
3435
+ const calendarStatus = (status) => {
3436
+ switch (status) {
3437
+ case "no_show" /* NO_SHOW */:
3438
+ return "no_show" /* NO_SHOW */;
3439
+ case "canceled_clinic" /* CANCELED_CLINIC */:
3440
+ return "rejected" /* REJECTED */;
3441
+ case "canceled_patient" /* CANCELED_PATIENT */:
3442
+ return "canceled" /* CANCELED */;
3443
+ case "canceled_patient_rescheduled" /* CANCELED_PATIENT_RESCHEDULED */:
3444
+ return "rejected" /* REJECTED */;
3445
+ default:
3446
+ return "canceled" /* CANCELED */;
3447
+ }
3448
+ };
3449
+ await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
3450
+ after,
3451
+ calendarStatus(after.status)
3452
+ );
3204
3453
  if ((patientSensitiveInfo == null ? void 0 : patientSensitiveInfo.email) && patientProfile) {
3205
3454
  Logger.info(
3206
3455
  `[AggService] Sending appointment cancellation email to patient ${patientSensitiveInfo.email}`
@@ -3216,7 +3465,7 @@ var AppointmentAggregationService = class {
3216
3465
  // TODO: Properly import types
3217
3466
  );
3218
3467
  }
3219
- if ((_b = practitionerProfile == null ? void 0 : practitionerProfile.basicInfo) == null ? void 0 : _b.email) {
3468
+ if ((_c = practitionerProfile == null ? void 0 : practitionerProfile.basicInfo) == null ? void 0 : _c.email) {
3220
3469
  Logger.info(
3221
3470
  `[AggService] Sending appointment cancellation email to practitioner ${practitionerProfile.basicInfo.email}`
3222
3471
  );
@@ -3234,6 +3483,10 @@ var AppointmentAggregationService = class {
3234
3483
  } else if (after.status === "completed" /* COMPLETED */) {
3235
3484
  Logger.info(`[AggService] Appt ${after.id} status -> COMPLETED.`);
3236
3485
  await this.createPostAppointmentRequirementInstances(after);
3486
+ await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
3487
+ after,
3488
+ "completed" /* COMPLETED */
3489
+ );
3237
3490
  if ((patientSensitiveInfo == null ? void 0 : patientSensitiveInfo.email) && patientProfile) {
3238
3491
  Logger.info(
3239
3492
  `[AggService] Sending review request email to patient ${patientSensitiveInfo.email}`
@@ -3258,8 +3511,16 @@ var AppointmentAggregationService = class {
3258
3511
  // Pass the 'before' state for old requirements
3259
3512
  "supersededReschedule" /* SUPERSEDED_RESCHEDULE */
3260
3513
  );
3261
- Logger.info(
3262
- `[AggService] TODO: Handle RESCHEDULE logic for requirements carefully based on confirmation flow.`
3514
+ await this.calendarAdminService.updateAppointmentCalendarEventsTime(
3515
+ after,
3516
+ {
3517
+ start: after.appointmentStartTime,
3518
+ end: after.appointmentEndTime
3519
+ }
3520
+ );
3521
+ await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
3522
+ after,
3523
+ "pending" /* PENDING */
3263
3524
  );
3264
3525
  if ((patientSensitiveInfo == null ? void 0 : patientSensitiveInfo.email) && patientProfile) {
3265
3526
  Logger.info(
@@ -3284,9 +3545,24 @@ var AppointmentAggregationService = class {
3284
3545
  }
3285
3546
  if (timeChanged && !statusChanged) {
3286
3547
  Logger.info(`[AggService] Appointment ${after.id} time changed.`);
3287
- Logger.warn(
3288
- `[AggService] Independent time change detected for ${after.id}. Review implications for requirements and calendar.`
3289
- );
3548
+ if (after.status === "confirmed" /* CONFIRMED */) {
3549
+ await this.updateRelatedPatientRequirementInstances(
3550
+ before,
3551
+ "supersededReschedule" /* SUPERSEDED_RESCHEDULE */
3552
+ );
3553
+ await this.createPreAppointmentRequirementInstances(after);
3554
+ await this.calendarAdminService.updateAppointmentCalendarEventsTime(
3555
+ after,
3556
+ {
3557
+ start: after.appointmentStartTime,
3558
+ end: after.appointmentEndTime
3559
+ }
3560
+ );
3561
+ } else {
3562
+ Logger.warn(
3563
+ `[AggService] Independent time change detected for ${after.id} with status ${after.status}. Review implications for requirements and calendar.`
3564
+ );
3565
+ }
3290
3566
  }
3291
3567
  Logger.info(
3292
3568
  `[AggService] Successfully processed UPDATE for appointment: ${after.id}`
@@ -3311,9 +3587,19 @@ var AppointmentAggregationService = class {
3311
3587
  deletedAppointment,
3312
3588
  "cancelledAppointment" /* CANCELLED_APPOINTMENT */
3313
3589
  );
3314
- await this.managePatientClinicPractitionerLinks(
3315
- deletedAppointment,
3316
- "cancel"
3590
+ const patientProfile = await this.fetchPatientProfile(
3591
+ deletedAppointment.patientId
3592
+ );
3593
+ if (patientProfile) {
3594
+ await this.managePatientClinicPractitionerLinks(
3595
+ patientProfile,
3596
+ deletedAppointment.practitionerId,
3597
+ deletedAppointment.clinicBranchId,
3598
+ "cancel"
3599
+ );
3600
+ }
3601
+ await this.calendarAdminService.deleteAppointmentCalendarEvents(
3602
+ deletedAppointment
3317
3603
  );
3318
3604
  }
3319
3605
  // --- Helper Methods for Aggregation Logic ---
@@ -3594,18 +3880,47 @@ var AppointmentAggregationService = class {
3594
3880
  try {
3595
3881
  const batch = this.db.batch();
3596
3882
  let instancesCreatedCount = 0;
3883
+ let createdInstances = [];
3884
+ Logger.info(
3885
+ `[AggService] Found ${appointment.postProcedureRequirements.length} post-requirements to process: ${JSON.stringify(
3886
+ appointment.postProcedureRequirements.map((r) => {
3887
+ var _a2, _b;
3888
+ return {
3889
+ id: r.id,
3890
+ name: r.name,
3891
+ type: r.type,
3892
+ isActive: r.isActive,
3893
+ hasTimeframe: !!r.timeframe,
3894
+ notifyAtLength: ((_b = (_a2 = r.timeframe) == null ? void 0 : _a2.notifyAt) == null ? void 0 : _b.length) || 0
3895
+ };
3896
+ })
3897
+ )}`
3898
+ );
3597
3899
  for (const template of appointment.postProcedureRequirements) {
3598
- if (!template) continue;
3900
+ if (!template) {
3901
+ Logger.warn(
3902
+ `[AggService] Found null/undefined template in postProcedureRequirements array`
3903
+ );
3904
+ continue;
3905
+ }
3599
3906
  if (template.type !== "post" /* POST */ || !template.isActive) {
3600
3907
  Logger.debug(
3601
3908
  `[AggService] Skipping template ${template.id} (${template.name}): not an active POST requirement.`
3602
3909
  );
3603
3910
  continue;
3604
3911
  }
3912
+ if (!template.timeframe || !template.timeframe.notifyAt || template.timeframe.notifyAt.length === 0) {
3913
+ Logger.warn(
3914
+ `[AggService] Template ${template.id} (${template.name}) has no timeframe.notifyAt values. Creating with empty instructions.`
3915
+ );
3916
+ }
3605
3917
  Logger.debug(
3606
- `[AggService] Processing POST template ${template.id} (${template.name}) for appt ${appointment.id}`
3918
+ `[AggService] Processing template ${template.id} (${template.name}) for appt ${appointment.id}`
3607
3919
  );
3608
3920
  const newInstanceRef = this.db.collection(PATIENTS_COLLECTION).doc(appointment.patientId).collection(PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME).doc();
3921
+ Logger.debug(
3922
+ `[AggService] Created doc reference: ${newInstanceRef.path}`
3923
+ );
3609
3924
  const instructions = (((_a = template.timeframe) == null ? void 0 : _a.notifyAt) || []).map((notifyAtValue) => {
3610
3925
  let dueTime = appointment.appointmentEndTime;
3611
3926
  if (template.timeframe && typeof notifyAtValue === "number") {
@@ -3620,7 +3935,7 @@ var AppointmentAggregationService = class {
3620
3935
  dueTime = admin10.firestore.Timestamp.fromDate(dueDateTime);
3621
3936
  }
3622
3937
  const actionableWindowHours = template.importance === "high" ? 1 : template.importance === "medium" ? 4 : 15;
3623
- return {
3938
+ const instructionObject = {
3624
3939
  instructionId: `${template.id}_${notifyAtValue}_${newInstanceRef.id}`.replace(
3625
3940
  /[^a-zA-Z0-9_]/g,
3626
3941
  "_"
@@ -3632,36 +3947,142 @@ var AppointmentAggregationService = class {
3632
3947
  originalNotifyAtValue: notifyAtValue,
3633
3948
  originalTimeframeUnit: template.timeframe.unit,
3634
3949
  updatedAt: admin10.firestore.Timestamp.now(),
3635
- // Use current server timestamp
3636
3950
  notificationId: void 0,
3637
3951
  actionTakenAt: void 0
3638
3952
  };
3953
+ return instructionObject;
3639
3954
  });
3640
3955
  const newInstanceData = {
3956
+ id: newInstanceRef.id,
3641
3957
  patientId: appointment.patientId,
3642
3958
  appointmentId: appointment.id,
3643
3959
  originalRequirementId: template.id,
3644
3960
  requirementName: template.name,
3645
3961
  requirementDescription: template.description,
3646
3962
  requirementType: template.type,
3647
- // Should be RequirementType.POST
3648
3963
  requirementImportance: template.importance,
3649
3964
  overallStatus: "active" /* ACTIVE */,
3650
3965
  instructions,
3651
3966
  createdAt: admin10.firestore.FieldValue.serverTimestamp(),
3652
3967
  updatedAt: admin10.firestore.FieldValue.serverTimestamp()
3653
3968
  };
3969
+ Logger.debug(
3970
+ `[AggService] Setting data for requirement: ${JSON.stringify({
3971
+ id: newInstanceRef.id,
3972
+ patientId: newInstanceData.patientId,
3973
+ appointmentId: newInstanceData.appointmentId,
3974
+ requirementName: newInstanceData.requirementName,
3975
+ instructionsCount: newInstanceData.instructions.length
3976
+ })}`
3977
+ );
3654
3978
  batch.set(newInstanceRef, newInstanceData);
3979
+ createdInstances.push({
3980
+ ref: newInstanceRef,
3981
+ data: newInstanceData
3982
+ });
3655
3983
  instancesCreatedCount++;
3656
3984
  Logger.debug(
3657
- `[AggService] Added POST PatientRequirementInstance ${newInstanceRef.id} to batch for template ${template.id}.`
3985
+ `[AggService] Added PatientRequirementInstance ${newInstanceRef.id} to batch for template ${template.id}.`
3658
3986
  );
3659
3987
  }
3660
3988
  if (instancesCreatedCount > 0) {
3661
- await batch.commit();
3662
- Logger.info(
3663
- `[AggService] Successfully created ${instancesCreatedCount} POST_APPOINTMENT requirement instances for appointment ${appointment.id}.`
3664
- );
3989
+ try {
3990
+ await batch.commit();
3991
+ Logger.info(
3992
+ `[AggService] Successfully created ${instancesCreatedCount} POST_APPOINTMENT requirement instances for appointment ${appointment.id}.`
3993
+ );
3994
+ try {
3995
+ const verifySnapshot = await this.db.collection(PATIENTS_COLLECTION).doc(appointment.patientId).collection(PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME).where("appointmentId", "==", appointment.id).get();
3996
+ if (verifySnapshot.empty) {
3997
+ Logger.warn(
3998
+ `[AggService] Batch commit reported success but documents not found! Attempting direct creation as fallback...`
3999
+ );
4000
+ const fallbackPromises = createdInstances.map(
4001
+ async ({ ref, data }) => {
4002
+ try {
4003
+ await ref.set(data);
4004
+ Logger.info(
4005
+ `[AggService] Fallback direct creation success for ${ref.id}`
4006
+ );
4007
+ return true;
4008
+ } catch (fallbackError) {
4009
+ Logger.error(
4010
+ `[AggService] Fallback direct creation failed for ${ref.id}:`,
4011
+ fallbackError
4012
+ );
4013
+ return false;
4014
+ }
4015
+ }
4016
+ );
4017
+ const fallbackResults = await Promise.allSettled(
4018
+ fallbackPromises
4019
+ );
4020
+ const successCount = fallbackResults.filter(
4021
+ (r) => r.status === "fulfilled" && r.value === true
4022
+ ).length;
4023
+ if (successCount > 0) {
4024
+ Logger.info(
4025
+ `[AggService] Fallback mechanism created ${successCount} out of ${createdInstances.length} requirements`
4026
+ );
4027
+ } else {
4028
+ Logger.error(
4029
+ `[AggService] Both batch and fallback mechanisms failed to create requirements`
4030
+ );
4031
+ throw new Error(
4032
+ "Failed to create patient requirements through both batch and direct methods"
4033
+ );
4034
+ }
4035
+ } else {
4036
+ Logger.info(
4037
+ `[AggService] Verification confirmed ${verifySnapshot.size} requirement documents created`
4038
+ );
4039
+ }
4040
+ } catch (verifyError) {
4041
+ Logger.error(
4042
+ `[AggService] Error during verification of created requirements:`,
4043
+ verifyError
4044
+ );
4045
+ }
4046
+ } catch (commitError) {
4047
+ Logger.error(
4048
+ `[AggService] Error committing batch for POST_APPOINTMENT requirement instances for appointment ${appointment.id}:`,
4049
+ commitError
4050
+ );
4051
+ Logger.info(`[AggService] Attempting direct creation as fallback...`);
4052
+ const fallbackPromises = createdInstances.map(
4053
+ async ({ ref, data }) => {
4054
+ try {
4055
+ await ref.set(data);
4056
+ Logger.info(
4057
+ `[AggService] Fallback direct creation success for ${ref.id}`
4058
+ );
4059
+ return true;
4060
+ } catch (fallbackError) {
4061
+ Logger.error(
4062
+ `[AggService] Fallback direct creation failed for ${ref.id}:`,
4063
+ fallbackError
4064
+ );
4065
+ return false;
4066
+ }
4067
+ }
4068
+ );
4069
+ const fallbackResults = await Promise.allSettled(fallbackPromises);
4070
+ const successCount = fallbackResults.filter(
4071
+ (r) => r.status === "fulfilled" && r.value === true
4072
+ ).length;
4073
+ if (successCount > 0) {
4074
+ Logger.info(
4075
+ `[AggService] Fallback mechanism created ${successCount} out of ${createdInstances.length} requirements`
4076
+ );
4077
+ } else {
4078
+ Logger.error(
4079
+ `[AggService] Both batch and fallback mechanisms failed to create requirements`
4080
+ );
4081
+ throw new Error(
4082
+ "Failed to create patient requirements through both batch and direct methods"
4083
+ );
4084
+ }
4085
+ }
3665
4086
  } else {
3666
4087
  Logger.info(
3667
4088
  `[AggService] No new POST_APPOINTMENT requirement instances were prepared for batch commit for appointment ${appointment.id}.`
@@ -3672,6 +4093,7 @@ var AppointmentAggregationService = class {
3672
4093
  `[AggService] Error creating POST_APPOINTMENT requirement instances for appointment ${appointment.id}:`,
3673
4094
  error
3674
4095
  );
4096
+ throw error;
3675
4097
  }
3676
4098
  }
3677
4099
  /**
@@ -3737,75 +4159,165 @@ var AppointmentAggregationService = class {
3737
4159
  }
3738
4160
  }
3739
4161
  /**
3740
- * Manages denormalized links between a patient and the clinic/practitioner associated with an appointment.
3741
- * Adds patientId to arrays on clinic and practitioner documents on appointment creation.
3742
- * Removes patientId on appointment cancellation (basic removal, can be enhanced).
4162
+ * Manages relationships between a patient and clinics/practitioners.
4163
+ * Only updates the patient profile with doctorIds and clinicIds.
3743
4164
  *
3744
- * @param {Appointment} appointment - The appointment object.
3745
- * @param {"create" | "cancel"} action - 'create' to add links, 'cancel' to remove them.
4165
+ * @param {PatientProfile} patientProfile - The patient profile to update
4166
+ * @param {string} practitionerId - The practitioner ID
4167
+ * @param {string} clinicId - The clinic ID
4168
+ * @param {"create" | "cancel"} action - 'create' to add IDs, 'cancel' to potentially remove them
4169
+ * @param {AppointmentStatus} [cancelStatus] - The appointment status if action is 'cancel'
3746
4170
  * @returns {Promise<void>} A promise that resolves when the operation is complete.
3747
4171
  */
3748
- async managePatientClinicPractitionerLinks(appointment, action) {
4172
+ async managePatientClinicPractitionerLinks(patientProfile, practitionerId, clinicId, action, cancelStatus) {
3749
4173
  Logger.info(
3750
- `[AggService] Managing patient-clinic-practitioner links for appt ${appointment.id} (patient: ${appointment.patientId}), action: ${action}`
4174
+ `[AggService] Managing patient-clinic-practitioner links for patient ${patientProfile.id}, action: ${action}`
3751
4175
  );
3752
- if (!appointment.patientId || !appointment.practitionerId || !appointment.clinicBranchId) {
4176
+ try {
4177
+ if (action === "create") {
4178
+ await this.addPatientLinks(patientProfile, practitionerId, clinicId);
4179
+ } else if (action === "cancel") {
4180
+ await this.removePatientLinksIfNoActiveAppointments(
4181
+ patientProfile,
4182
+ practitionerId,
4183
+ clinicId
4184
+ );
4185
+ }
4186
+ } catch (error) {
3753
4187
  Logger.error(
3754
- "[AggService] managePatientClinicPractitionerLinks called with missing IDs.",
3755
- {
3756
- patientId: appointment.patientId,
3757
- practitionerId: appointment.practitionerId,
3758
- clinicBranchId: appointment.clinicBranchId
3759
- }
4188
+ `[AggService] Error managing patient-clinic-practitioner links for patient ${patientProfile.id}:`,
4189
+ error
3760
4190
  );
3761
- return;
3762
4191
  }
3763
- const batch = this.db.batch();
3764
- const patientId = appointment.patientId;
3765
- const practitionerRef = this.db.collection(PRACTITIONERS_COLLECTION).doc(appointment.practitionerId);
3766
- const clinicRef = this.db.collection(CLINICS_COLLECTION).doc(appointment.clinicBranchId);
3767
- if (action === "create") {
3768
- batch.update(practitionerRef, {
3769
- patientIds: admin10.firestore.FieldValue.arrayUnion(patientId),
3770
- // Optionally, update a count or last interaction timestamp
3771
- updatedAt: admin10.firestore.FieldValue.serverTimestamp()
3772
- });
3773
- Logger.debug(
3774
- `[AggService] Adding patient ${patientId} to practitioner ${appointment.practitionerId} patientIds.`
3775
- );
3776
- batch.update(clinicRef, {
3777
- patientIds: admin10.firestore.FieldValue.arrayUnion(patientId),
3778
- updatedAt: admin10.firestore.FieldValue.serverTimestamp()
3779
- });
3780
- Logger.debug(
3781
- `[AggService] Adding patient ${patientId} to clinic ${appointment.clinicBranchId} patientIds.`
3782
- );
3783
- } else if (action === "cancel") {
3784
- batch.update(practitionerRef, {
3785
- patientIds: admin10.firestore.FieldValue.arrayRemove(patientId),
3786
- updatedAt: admin10.firestore.FieldValue.serverTimestamp()
3787
- });
3788
- Logger.debug(
3789
- `[AggService] Removing patient ${patientId} from practitioner ${appointment.practitionerId} patientIds.`
3790
- );
3791
- batch.update(clinicRef, {
3792
- patientIds: admin10.firestore.FieldValue.arrayRemove(patientId),
3793
- updatedAt: admin10.firestore.FieldValue.serverTimestamp()
3794
- });
3795
- Logger.debug(
3796
- `[AggService] Removing patient ${patientId} from clinic ${appointment.clinicBranchId} patientIds.`
4192
+ }
4193
+ /**
4194
+ * Adds practitioner and clinic IDs to the patient profile.
4195
+ *
4196
+ * @param {PatientProfile} patientProfile - The patient profile to update
4197
+ * @param {string} practitionerId - The practitioner ID to add
4198
+ * @param {string} clinicId - The clinic ID to add
4199
+ * @returns {Promise<void>} A promise that resolves when the operation is complete.
4200
+ */
4201
+ async addPatientLinks(patientProfile, practitionerId, clinicId) {
4202
+ var _a, _b;
4203
+ try {
4204
+ const hasDoctor = ((_a = patientProfile.doctorIds) == null ? void 0 : _a.includes(practitionerId)) || false;
4205
+ const hasClinic = ((_b = patientProfile.clinicIds) == null ? void 0 : _b.includes(clinicId)) || false;
4206
+ if (!hasDoctor || !hasClinic) {
4207
+ const patientRef = this.db.collection(PATIENTS_COLLECTION).doc(patientProfile.id);
4208
+ const updateData = {
4209
+ updatedAt: admin10.firestore.FieldValue.serverTimestamp()
4210
+ };
4211
+ if (!hasDoctor) {
4212
+ Logger.debug(
4213
+ `[AggService] Adding practitioner ${practitionerId} to patient ${patientProfile.id}`
4214
+ );
4215
+ updateData.doctorIds = admin10.firestore.FieldValue.arrayUnion(practitionerId);
4216
+ }
4217
+ if (!hasClinic) {
4218
+ Logger.debug(
4219
+ `[AggService] Adding clinic ${clinicId} to patient ${patientProfile.id}`
4220
+ );
4221
+ updateData.clinicIds = admin10.firestore.FieldValue.arrayUnion(clinicId);
4222
+ }
4223
+ await patientRef.update(updateData);
4224
+ Logger.info(
4225
+ `[AggService] Successfully updated patient ${patientProfile.id} with new links.`
4226
+ );
4227
+ } else {
4228
+ Logger.info(
4229
+ `[AggService] Patient ${patientProfile.id} already has links to both practitioner ${practitionerId} and clinic ${clinicId}.`
4230
+ );
4231
+ }
4232
+ } catch (error) {
4233
+ Logger.error(
4234
+ `[AggService] Error updating patient ${patientProfile.id} with new links:`,
4235
+ error
3797
4236
  );
4237
+ throw error;
3798
4238
  }
4239
+ }
4240
+ /**
4241
+ * Removes practitioner and clinic IDs from the patient profile if there are no more active appointments.
4242
+ *
4243
+ * @param {PatientProfile} patientProfile - The patient profile to update
4244
+ * @param {string} practitionerId - The practitioner ID to remove
4245
+ * @param {string} clinicId - The clinic ID to remove
4246
+ * @returns {Promise<void>} A promise that resolves when the operation is complete.
4247
+ */
4248
+ async removePatientLinksIfNoActiveAppointments(patientProfile, practitionerId, clinicId) {
4249
+ var _a, _b;
3799
4250
  try {
3800
- await batch.commit();
4251
+ const activePractitionerAppointments = await this.checkActiveAppointments(
4252
+ patientProfile.id,
4253
+ "practitionerId",
4254
+ practitionerId
4255
+ );
4256
+ const activeClinicAppointments = await this.checkActiveAppointments(
4257
+ patientProfile.id,
4258
+ "clinicBranchId",
4259
+ clinicId
4260
+ );
3801
4261
  Logger.info(
3802
- `[AggService] Successfully updated patient-clinic-practitioner links for appointment ${appointment.id}, action: ${action}.`
4262
+ `[AggService] Active appointment count for patient ${patientProfile.id}: With practitioner ${practitionerId}: ${activePractitionerAppointments}, With clinic ${clinicId}: ${activeClinicAppointments}`
3803
4263
  );
4264
+ const patientRef = this.db.collection(PATIENTS_COLLECTION).doc(patientProfile.id);
4265
+ const updateData = {};
4266
+ let updateNeeded = false;
4267
+ if (activePractitionerAppointments === 0 && ((_a = patientProfile.doctorIds) == null ? void 0 : _a.includes(practitionerId))) {
4268
+ Logger.debug(
4269
+ `[AggService] Removing practitioner ${practitionerId} from patient ${patientProfile.id}`
4270
+ );
4271
+ updateData.doctorIds = admin10.firestore.FieldValue.arrayRemove(practitionerId);
4272
+ updateNeeded = true;
4273
+ }
4274
+ if (activeClinicAppointments === 0 && ((_b = patientProfile.clinicIds) == null ? void 0 : _b.includes(clinicId))) {
4275
+ Logger.debug(
4276
+ `[AggService] Removing clinic ${clinicId} from patient ${patientProfile.id}`
4277
+ );
4278
+ updateData.clinicIds = admin10.firestore.FieldValue.arrayRemove(clinicId);
4279
+ updateNeeded = true;
4280
+ }
4281
+ if (updateNeeded) {
4282
+ updateData.updatedAt = admin10.firestore.FieldValue.serverTimestamp();
4283
+ await patientRef.update(updateData);
4284
+ Logger.info(
4285
+ `[AggService] Successfully removed links from patient ${patientProfile.id}`
4286
+ );
4287
+ } else {
4288
+ Logger.info(
4289
+ `[AggService] No links need to be removed from patient ${patientProfile.id}`
4290
+ );
4291
+ }
3804
4292
  } catch (error) {
3805
4293
  Logger.error(
3806
- `[AggService] Error managing patient-clinic-practitioner links for appointment ${appointment.id}:`,
4294
+ `[AggService] Error removing links from patient profile:`,
3807
4295
  error
3808
4296
  );
4297
+ throw error;
4298
+ }
4299
+ }
4300
+ /**
4301
+ * Checks if there are active appointments between a patient and another entity (practitioner or clinic).
4302
+ *
4303
+ * @param {string} patientId - The patient ID.
4304
+ * @param {"practitionerId" | "clinicBranchId"} entityField - The field to check for the entity ID.
4305
+ * @param {string} entityId - The entity ID (practitioner or clinic).
4306
+ * @returns {Promise<number>} The number of active appointments found.
4307
+ */
4308
+ async checkActiveAppointments(patientId, entityField, entityId) {
4309
+ try {
4310
+ const inactiveStatuses = [
4311
+ "canceled_clinic" /* CANCELED_CLINIC */,
4312
+ "canceled_patient" /* CANCELED_PATIENT */,
4313
+ "canceled_patient_rescheduled" /* CANCELED_PATIENT_RESCHEDULED */,
4314
+ "no_show" /* NO_SHOW */
4315
+ ];
4316
+ const snapshot = await this.db.collection("appointments").where("patientId", "==", patientId).where(entityField, "==", entityId).where("status", "not-in", inactiveStatuses).get();
4317
+ return snapshot.size;
4318
+ } catch (error) {
4319
+ Logger.error(`[AggService] Error checking active appointments:`, error);
4320
+ throw error;
3809
4321
  }
3810
4322
  }
3811
4323
  // --- Data Fetching Helpers (Consider moving to a data access layer or using existing services if available) ---