@blackcode_sa/metaestetics-api 1.12.20 → 1.12.21

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.
@@ -1,43 +1,40 @@
1
- import * as admin from "firebase-admin";
1
+ import * as admin from 'firebase-admin';
2
2
  import {
3
3
  Appointment,
4
4
  AppointmentStatus,
5
5
  // APPOINTMENTS_COLLECTION, // Not directly used in this file after refactor
6
- } from "../../../types/appointment";
6
+ } from '../../../types/appointment';
7
7
  import {
8
8
  PatientRequirementInstance,
9
9
  PatientRequirementOverallStatus,
10
10
  PatientInstructionStatus,
11
11
  PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME,
12
12
  PatientRequirementInstruction, // Added import
13
- } from "../../../types/patient/patient-requirements";
13
+ } from '../../../types/patient/patient-requirements';
14
14
  import {
15
15
  Requirement as RequirementTemplate,
16
16
  // REQUIREMENTS_COLLECTION as REQUIREMENTS_TEMPLATES_COLLECTION, // Not used directly after refactor
17
17
  RequirementType,
18
18
  TimeUnit, // Added import
19
- } from "../../../backoffice/types/requirement.types";
19
+ } from '../../../backoffice/types/requirement.types';
20
20
  import {
21
21
  PATIENTS_COLLECTION,
22
22
  PatientProfile,
23
23
  PatientSensitiveInfo,
24
24
  PATIENT_SENSITIVE_INFO_COLLECTION,
25
- } from "../../../types/patient";
26
- import {
27
- Practitioner,
28
- PRACTITIONERS_COLLECTION,
29
- } from "../../../types/practitioner";
30
- import { Clinic, CLINICS_COLLECTION } from "../../../types/clinic";
25
+ } from '../../../types/patient';
26
+ import { Practitioner, PRACTITIONERS_COLLECTION } from '../../../types/practitioner';
27
+ import { Clinic, CLINICS_COLLECTION } from '../../../types/clinic';
31
28
  // import { UserRole } from "../../../types"; // Not directly used
32
29
 
33
30
  // Dependent Admin Services
34
- import { PatientRequirementsAdminService } from "../../requirements/patient-requirements.admin.service";
35
- import { NotificationsAdmin } from "../../notifications/notifications.admin";
36
- import { CalendarAdminService } from "../../calendar/calendar.admin.service";
37
- import { AppointmentMailingService } from "../../mailing/appointment/appointment.mailing.service";
38
- import { Logger } from "../../logger";
39
- import { UserRole } from "../../../types";
40
- import { CalendarEventStatus } from "../../../types/calendar";
31
+ import { PatientRequirementsAdminService } from '../../requirements/patient-requirements.admin.service';
32
+ import { NotificationsAdmin } from '../../notifications/notifications.admin';
33
+ import { CalendarAdminService } from '../../calendar/calendar.admin.service';
34
+ import { AppointmentMailingService } from '../../mailing/appointment/appointment.mailing.service';
35
+ import { Logger } from '../../logger';
36
+ import { UserRole } from '../../../types';
37
+ import { CalendarEventStatus } from '../../../types/calendar';
41
38
 
42
39
  // Mailgun client will be injected via constructor
43
40
 
@@ -61,19 +58,17 @@ export class AppointmentAggregationService {
61
58
  */
62
59
  constructor(
63
60
  mailgunClient: any, // Type as 'any' for now, to be provided by the calling Cloud Function
64
- firestore?: admin.firestore.Firestore
61
+ firestore?: admin.firestore.Firestore,
65
62
  ) {
66
63
  this.db = firestore || admin.firestore();
67
64
  this.appointmentMailingService = new AppointmentMailingService(
68
65
  this.db,
69
- mailgunClient // Pass the injected client
66
+ mailgunClient, // Pass the injected client
70
67
  );
71
68
  this.notificationsAdmin = new NotificationsAdmin(this.db);
72
69
  this.calendarAdminService = new CalendarAdminService(this.db);
73
- this.patientRequirementsAdminService = new PatientRequirementsAdminService(
74
- this.db
75
- );
76
- Logger.info("[AppointmentAggregationService] Initialized.");
70
+ this.patientRequirementsAdminService = new PatientRequirementsAdminService(this.db);
71
+ Logger.info('[AppointmentAggregationService] Initialized.');
77
72
  }
78
73
 
79
74
  /**
@@ -84,23 +79,19 @@ export class AppointmentAggregationService {
84
79
  */
85
80
  async handleAppointmentCreate(appointment: Appointment): Promise<void> {
86
81
  Logger.info(
87
- `[AggService] Handling CREATE for appointment: ${appointment.id}, patient: ${appointment.patientId}, status: ${appointment.status}`
82
+ `[AggService] Handling CREATE for appointment: ${appointment.id}, patient: ${appointment.patientId}, status: ${appointment.status}`,
88
83
  );
89
84
 
90
85
  try {
91
86
  // 1. Fetch necessary profiles for notifications and context
92
87
  // These can be fetched in parallel
93
- const [
94
- patientProfile,
95
- patientSensitiveInfo,
96
- practitionerProfile,
97
- clinicInfo,
98
- ] = await Promise.all([
99
- this.fetchPatientProfile(appointment.patientId),
100
- this.fetchPatientSensitiveInfo(appointment.patientId),
101
- this.fetchPractitionerProfile(appointment.practitionerId), // Needed for practitioner notifications
102
- this.fetchClinicInfo(appointment.clinicBranchId), // Needed for clinic admin notifications
103
- ]);
88
+ const [patientProfile, patientSensitiveInfo, practitionerProfile, clinicInfo] =
89
+ await Promise.all([
90
+ this.fetchPatientProfile(appointment.patientId),
91
+ this.fetchPatientSensitiveInfo(appointment.patientId),
92
+ this.fetchPractitionerProfile(appointment.practitionerId), // Needed for practitioner notifications
93
+ this.fetchClinicInfo(appointment.clinicBranchId), // Needed for clinic admin notifications
94
+ ]);
104
95
 
105
96
  // 2. Manage Patient-Clinic-Practitioner Links (moved from beginning to here)
106
97
  // Now we can pass the already fetched patient profile
@@ -109,67 +100,62 @@ export class AppointmentAggregationService {
109
100
  patientProfile,
110
101
  appointment.practitionerId,
111
102
  appointment.clinicBranchId,
112
- "create"
103
+ 'create',
113
104
  );
114
105
  }
115
106
 
116
107
  // 3. Initial State Handling based on appointment status
117
108
  if (appointment.status === AppointmentStatus.CONFIRMED) {
118
- Logger.info(
119
- `[AggService] Appt ${appointment.id} created as CONFIRMED.`
120
- );
109
+ Logger.info(`[AggService] Appt ${appointment.id} created as CONFIRMED.`);
121
110
  // Create pre-appointment requirements for confirmed appointments
122
111
  await this.createPreAppointmentRequirementInstances(appointment);
123
112
 
124
113
  // Send confirmation notifications
125
114
  if (patientSensitiveInfo?.email && patientProfile) {
126
115
  Logger.info(
127
- `[AggService] Sending appointment confirmed email to patient ${patientSensitiveInfo.email}`
116
+ `[AggService] Sending appointment confirmed email to patient ${patientSensitiveInfo.email}`,
128
117
  );
129
118
  // Construct the data object for the mailing service
130
119
  const emailData = {
131
120
  appointment: appointment,
132
121
  recipientProfile: appointment.patientInfo,
133
- recipientRole: "patient" as const, // Use 'as const' for literal type
122
+ recipientRole: 'patient' as const, // Use 'as const' for literal type
134
123
  };
135
124
  // The type cast here might still be an issue if PatientProfileInfo is not imported.
136
125
  // However, the structure should be compatible enough for the call.
137
126
  await this.appointmentMailingService.sendAppointmentConfirmedEmail(
138
- emailData as any // Using 'as any' temporarily to bypass strict type checking if PatientProfileInfo is not imported
127
+ emailData as any, // Using 'as any' temporarily to bypass strict type checking if PatientProfileInfo is not imported
139
128
  // TODO: Properly import PatientProfileInfo and ensure type compatibility
140
129
  );
141
130
  } else {
142
131
  Logger.warn(
143
- `[AggService] Cannot send confirmation email to patient ${appointment.patientId}: email missing.`
132
+ `[AggService] Cannot send confirmation email to patient ${appointment.patientId}: email missing.`,
144
133
  );
145
134
  }
146
135
 
147
- if (
148
- patientProfile?.expoTokens &&
149
- patientProfile.expoTokens.length > 0
150
- ) {
136
+ if (patientProfile?.expoTokens && patientProfile.expoTokens.length > 0) {
151
137
  Logger.info(
152
- `[AggService] TODO: Send appointment confirmed push to patient ${appointment.patientId}`
138
+ `[AggService] TODO: Send appointment confirmed push to patient ${appointment.patientId}`,
153
139
  );
154
140
  await this.notificationsAdmin.sendAppointmentConfirmedPush(
155
141
  appointment,
156
142
  appointment.patientId,
157
143
  patientProfile.expoTokens,
158
- UserRole.PATIENT
144
+ UserRole.PATIENT,
159
145
  );
160
146
  }
161
147
 
162
148
  if (practitionerProfile?.basicInfo?.email) {
163
149
  Logger.info(
164
- `[AggService] Sending appointment confirmation email to practitioner ${practitionerProfile.basicInfo.email}`
150
+ `[AggService] Sending appointment confirmation email to practitioner ${practitionerProfile.basicInfo.email}`,
165
151
  );
166
152
  const practitionerEmailData = {
167
153
  appointment: appointment,
168
154
  recipientProfile: appointment.practitionerInfo,
169
- recipientRole: "practitioner" as const,
155
+ recipientRole: 'practitioner' as const,
170
156
  };
171
157
  await this.appointmentMailingService.sendAppointmentConfirmedEmail(
172
- practitionerEmailData // TODO: Properly import PractitionerProfileInfo and ensure type compatibility
158
+ practitionerEmailData, // TODO: Properly import PractitionerProfileInfo and ensure type compatibility
173
159
  );
174
160
  }
175
161
  // TODO: Add push notification for practitioner if they have expoTokens
@@ -178,31 +164,29 @@ export class AppointmentAggregationService {
178
164
  // Notify clinic admin about the pending appointment
179
165
  if (clinicInfo?.contactInfo?.email) {
180
166
  Logger.info(
181
- `[AggService] TODO: Send pending appointment notification email to clinic admin ${clinicInfo.contactInfo.email}`
167
+ `[AggService] TODO: Send pending appointment notification email to clinic admin ${clinicInfo.contactInfo.email}`,
182
168
  );
183
169
  const clinicEmailData = {
184
170
  appointment: appointment,
185
171
  clinicProfile: appointment.clinicInfo, // clinicInfo should be compatible with ClinicInfo type
186
172
  };
187
173
  await this.appointmentMailingService.sendAppointmentRequestedEmailToClinic(
188
- clinicEmailData // TODO: Properly import ClinicInfo if stricter typing is needed here and ensure compatibility
174
+ clinicEmailData, // TODO: Properly import ClinicInfo if stricter typing is needed here and ensure compatibility
189
175
  );
190
176
  } else {
191
177
  Logger.warn(
192
- `[AggService] Cannot send pending appointment email to clinic ${appointment.clinicBranchId}: email missing.`
178
+ `[AggService] Cannot send pending appointment email to clinic ${appointment.clinicBranchId}: email missing.`,
193
179
  );
194
180
  }
195
181
  // TODO: Push notification for clinic admin if applicable (they usually don't have tokens)
196
182
  }
197
183
 
198
184
  // Calendar events are noted as being handled by BookingAdmin.orchestrateAppointmentCreation during the booking process itself.
199
- Logger.info(
200
- `[AggService] Successfully processed CREATE for appointment: ${appointment.id}`
201
- );
185
+ Logger.info(`[AggService] Successfully processed CREATE for appointment: ${appointment.id}`);
202
186
  } catch (error) {
203
187
  Logger.error(
204
188
  `[AggService] Critical error in handleAppointmentCreate for appointment ${appointment.id}:`,
205
- error
189
+ error,
206
190
  );
207
191
  // Depending on the error, you might want to re-throw or handle specific cases
208
192
  // (e.g., update appointment status to an error state if a critical part failed)
@@ -216,41 +200,33 @@ export class AppointmentAggregationService {
216
200
  * @param {Appointment} after - The Appointment object after the update.
217
201
  * @returns {Promise<void>}
218
202
  */
219
- async handleAppointmentUpdate(
220
- before: Appointment,
221
- after: Appointment
222
- ): Promise<void> {
203
+ async handleAppointmentUpdate(before: Appointment, after: Appointment): Promise<void> {
223
204
  Logger.info(
224
- `[AggService] Handling UPDATE for appointment: ${after.id}. Status ${before.status} -> ${after.status}`
205
+ `[AggService] Handling UPDATE for appointment: ${after.id}. Status ${before.status} -> ${after.status}`,
225
206
  );
226
207
 
227
208
  try {
228
209
  const statusChanged = before.status !== after.status;
229
210
  const timeChanged =
230
- before.appointmentStartTime.toMillis() !==
231
- after.appointmentStartTime.toMillis() ||
232
- before.appointmentEndTime.toMillis() !==
233
- after.appointmentEndTime.toMillis();
211
+ before.appointmentStartTime.toMillis() !== after.appointmentStartTime.toMillis() ||
212
+ before.appointmentEndTime.toMillis() !== after.appointmentEndTime.toMillis();
213
+ const zonePhotosChanged = this.hasZonePhotosChanged(before, after);
234
214
  // const paymentStatusChanged = before.paymentStatus !== after.paymentStatus; // TODO: Handle later
235
215
  // const reviewAdded = !before.reviewInfo && after.reviewInfo; // TODO: Handle later
236
216
 
237
217
  // Fetch profiles for notifications - could be conditional based on changes
238
218
  // For simplicity, fetching upfront, but optimize if performance is an issue.
239
- const [
240
- patientProfile,
241
- patientSensitiveInfo,
242
- practitionerProfile,
243
- clinicInfo,
244
- ] = await Promise.all([
245
- this.fetchPatientProfile(after.patientId),
246
- this.fetchPatientSensitiveInfo(after.patientId),
247
- this.fetchPractitionerProfile(after.practitionerId),
248
- this.fetchClinicInfo(after.clinicBranchId),
249
- ]);
219
+ const [patientProfile, patientSensitiveInfo, practitionerProfile, clinicInfo] =
220
+ await Promise.all([
221
+ this.fetchPatientProfile(after.patientId),
222
+ this.fetchPatientSensitiveInfo(after.patientId),
223
+ this.fetchPractitionerProfile(after.practitionerId),
224
+ this.fetchClinicInfo(after.clinicBranchId),
225
+ ]);
250
226
 
251
227
  if (statusChanged) {
252
228
  Logger.info(
253
- `[AggService] Status changed for ${after.id}: ${before.status} -> ${after.status}`
229
+ `[AggService] Status changed for ${after.id}: ${before.status} -> ${after.status}`,
254
230
  );
255
231
 
256
232
  // --- PENDING -> CONFIRMED ---
@@ -264,54 +240,49 @@ export class AppointmentAggregationService {
264
240
  // Update calendar events to CONFIRMED status
265
241
  await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
266
242
  after,
267
- CalendarEventStatus.CONFIRMED
243
+ CalendarEventStatus.CONFIRMED,
268
244
  );
269
245
 
270
246
  // Send confirmation notifications
271
247
  if (patientSensitiveInfo?.email && patientProfile) {
272
248
  Logger.info(
273
- `[AggService] Sending appointment confirmed email to patient ${patientSensitiveInfo.email}`
249
+ `[AggService] Sending appointment confirmed email to patient ${patientSensitiveInfo.email}`,
274
250
  );
275
251
  const emailData = {
276
252
  appointment: after,
277
253
  recipientProfile: after.patientInfo,
278
- recipientRole: "patient" as const,
254
+ recipientRole: 'patient' as const,
279
255
  };
280
- await this.appointmentMailingService.sendAppointmentConfirmedEmail(
281
- emailData as any
282
- );
256
+ await this.appointmentMailingService.sendAppointmentConfirmedEmail(emailData as any);
283
257
  } else {
284
258
  Logger.warn(
285
- `[AggService] Cannot send confirmation email to patient ${after.patientId}: email missing.`
259
+ `[AggService] Cannot send confirmation email to patient ${after.patientId}: email missing.`,
286
260
  );
287
261
  }
288
262
 
289
- if (
290
- patientProfile?.expoTokens &&
291
- patientProfile.expoTokens.length > 0
292
- ) {
263
+ if (patientProfile?.expoTokens && patientProfile.expoTokens.length > 0) {
293
264
  Logger.info(
294
- `[AggService] TODO: Send appointment confirmed push to patient ${after.patientId}`
265
+ `[AggService] TODO: Send appointment confirmed push to patient ${after.patientId}`,
295
266
  );
296
267
  await this.notificationsAdmin.sendAppointmentConfirmedPush(
297
268
  after,
298
269
  after.patientId,
299
270
  patientProfile.expoTokens,
300
- UserRole.PATIENT
271
+ UserRole.PATIENT,
301
272
  );
302
273
  }
303
274
 
304
275
  if (practitionerProfile?.basicInfo?.email) {
305
276
  Logger.info(
306
- `[AggService] Sending appointment confirmation email to practitioner ${practitionerProfile.basicInfo.email}`
277
+ `[AggService] Sending appointment confirmation email to practitioner ${practitionerProfile.basicInfo.email}`,
307
278
  );
308
279
  const practitionerEmailData = {
309
280
  appointment: after,
310
281
  recipientProfile: after.practitionerInfo,
311
- recipientRole: "practitioner" as const,
282
+ recipientRole: 'practitioner' as const,
312
283
  };
313
284
  await this.appointmentMailingService.sendAppointmentConfirmedEmail(
314
- practitionerEmailData as any
285
+ practitionerEmailData as any,
315
286
  );
316
287
  }
317
288
  }
@@ -320,61 +291,54 @@ export class AppointmentAggregationService {
320
291
  before.status === AppointmentStatus.RESCHEDULED_BY_CLINIC &&
321
292
  after.status === AppointmentStatus.CONFIRMED
322
293
  ) {
323
- Logger.info(
324
- `[AggService] Appt ${after.id} RESCHEDULED_BY_CLINIC -> CONFIRMED.`
325
- );
294
+ Logger.info(`[AggService] Appt ${after.id} RESCHEDULED_BY_CLINIC -> CONFIRMED.`);
326
295
 
327
296
  // Update existing requirements as superseded and create new ones
328
297
  await this.updateRelatedPatientRequirementInstances(
329
298
  before,
330
- PatientRequirementOverallStatus.SUPERSEDED_RESCHEDULE
299
+ PatientRequirementOverallStatus.SUPERSEDED_RESCHEDULE,
331
300
  );
332
301
  await this.createPreAppointmentRequirementInstances(after);
333
302
 
334
303
  // Update calendar events to CONFIRMED status and update times
335
304
  await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
336
305
  after,
337
- CalendarEventStatus.CONFIRMED
306
+ CalendarEventStatus.CONFIRMED,
338
307
  );
339
308
 
340
309
  // Send confirmation notifications (similar to PENDING -> CONFIRMED)
341
310
  if (patientSensitiveInfo?.email && patientProfile) {
342
311
  Logger.info(
343
- `[AggService] Sending appointment confirmed email to patient ${patientSensitiveInfo.email}`
312
+ `[AggService] Sending appointment confirmed email to patient ${patientSensitiveInfo.email}`,
344
313
  );
345
314
  const emailData = {
346
315
  appointment: after,
347
316
  recipientProfile: after.patientInfo,
348
- recipientRole: "patient" as const,
317
+ recipientRole: 'patient' as const,
349
318
  };
350
- await this.appointmentMailingService.sendAppointmentConfirmedEmail(
351
- emailData as any
352
- );
319
+ await this.appointmentMailingService.sendAppointmentConfirmedEmail(emailData as any);
353
320
  }
354
321
 
355
- if (
356
- patientProfile?.expoTokens &&
357
- patientProfile.expoTokens.length > 0
358
- ) {
322
+ if (patientProfile?.expoTokens && patientProfile.expoTokens.length > 0) {
359
323
  await this.notificationsAdmin.sendAppointmentConfirmedPush(
360
324
  after,
361
325
  after.patientId,
362
326
  patientProfile.expoTokens,
363
- UserRole.PATIENT
327
+ UserRole.PATIENT,
364
328
  );
365
329
  }
366
330
 
367
331
  if (practitionerProfile?.basicInfo?.email) {
368
332
  Logger.info(
369
- `[AggService] Sending appointment confirmation email to practitioner ${practitionerProfile.basicInfo.email}`
333
+ `[AggService] Sending appointment confirmation email to practitioner ${practitionerProfile.basicInfo.email}`,
370
334
  );
371
335
  const practitionerEmailData = {
372
336
  appointment: after,
373
337
  recipientProfile: after.practitionerInfo,
374
- recipientRole: "practitioner" as const,
338
+ recipientRole: 'practitioner' as const,
375
339
  };
376
340
  await this.appointmentMailingService.sendAppointmentConfirmedEmail(
377
- practitionerEmailData as any
341
+ practitionerEmailData as any,
378
342
  );
379
343
  }
380
344
  }
@@ -386,11 +350,11 @@ export class AppointmentAggregationService {
386
350
  after.status === AppointmentStatus.NO_SHOW
387
351
  ) {
388
352
  Logger.info(
389
- `[AggService] Appt ${after.id} status -> ${after.status}. Processing as cancellation.`
353
+ `[AggService] Appt ${after.id} status -> ${after.status}. Processing as cancellation.`,
390
354
  );
391
355
  await this.updateRelatedPatientRequirementInstances(
392
356
  after,
393
- PatientRequirementOverallStatus.CANCELLED_APPOINTMENT
357
+ PatientRequirementOverallStatus.CANCELLED_APPOINTMENT,
394
358
  );
395
359
 
396
360
  // Update patient-clinic-practitioner links if patient profile exists
@@ -399,8 +363,8 @@ export class AppointmentAggregationService {
399
363
  patientProfile,
400
364
  after.practitionerId,
401
365
  after.clinicBranchId,
402
- "cancel",
403
- after.status
366
+ 'cancel',
367
+ after.status,
404
368
  );
405
369
  }
406
370
 
@@ -421,38 +385,38 @@ export class AppointmentAggregationService {
421
385
 
422
386
  await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
423
387
  after,
424
- calendarStatus(after.status)
388
+ calendarStatus(after.status),
425
389
  );
426
390
 
427
391
  // Send cancellation email to Patient
428
392
  if (patientSensitiveInfo?.email && patientProfile) {
429
393
  Logger.info(
430
- `[AggService] Sending appointment cancellation email to patient ${patientSensitiveInfo.email}`
394
+ `[AggService] Sending appointment cancellation email to patient ${patientSensitiveInfo.email}`,
431
395
  );
432
396
  const patientCancellationData = {
433
397
  appointment: after,
434
398
  recipientProfile: after.patientInfo,
435
- recipientRole: "patient" as const,
399
+ recipientRole: 'patient' as const,
436
400
  // cancellationReason: after.cancellationReason, // TODO: Add if cancellationReason is available on 'after' Appointment
437
401
  };
438
402
  await this.appointmentMailingService.sendAppointmentCancelledEmail(
439
- patientCancellationData as any // TODO: Properly import types
403
+ patientCancellationData as any, // TODO: Properly import types
440
404
  );
441
405
  }
442
406
 
443
407
  // Send cancellation email to Practitioner
444
408
  if (practitionerProfile?.basicInfo?.email) {
445
409
  Logger.info(
446
- `[AggService] Sending appointment cancellation email to practitioner ${practitionerProfile.basicInfo.email}`
410
+ `[AggService] Sending appointment cancellation email to practitioner ${practitionerProfile.basicInfo.email}`,
447
411
  );
448
412
  const practitionerCancellationData = {
449
413
  appointment: after,
450
414
  recipientProfile: after.practitionerInfo,
451
- recipientRole: "practitioner" as const,
415
+ recipientRole: 'practitioner' as const,
452
416
  // cancellationReason: after.cancellationReason, // TODO: Add if cancellationReason is available on 'after' Appointment
453
417
  };
454
418
  await this.appointmentMailingService.sendAppointmentCancelledEmail(
455
- practitionerCancellationData as any // TODO: Properly import types
419
+ practitionerCancellationData as any, // TODO: Properly import types
456
420
  );
457
421
  }
458
422
 
@@ -467,54 +431,49 @@ export class AppointmentAggregationService {
467
431
  // Update calendar events to COMPLETED status
468
432
  await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
469
433
  after,
470
- CalendarEventStatus.COMPLETED
434
+ CalendarEventStatus.COMPLETED,
471
435
  );
472
436
 
473
437
  // Send review request email to patient
474
438
  if (patientSensitiveInfo?.email && patientProfile) {
475
439
  Logger.info(
476
- `[AggService] Sending review request email to patient ${patientSensitiveInfo.email}`
440
+ `[AggService] Sending review request email to patient ${patientSensitiveInfo.email}`,
477
441
  );
478
442
  const reviewRequestData = {
479
443
  appointment: after,
480
444
  patientProfile: after.patientInfo,
481
- reviewLink: "TODO: Generate actual review link", // Placeholder
445
+ reviewLink: 'TODO: Generate actual review link', // Placeholder
482
446
  };
483
447
  await this.appointmentMailingService.sendReviewRequestEmail(
484
- reviewRequestData as any // TODO: Properly import PatientProfileInfo and define reviewLink generation
448
+ reviewRequestData as any, // TODO: Properly import PatientProfileInfo and define reviewLink generation
485
449
  );
486
450
  }
487
451
  // TODO: Send review request push notification to patient
488
452
  }
489
453
  // --- RESCHEDULE Scenarios (e.g., PENDING/CONFIRMED -> RESCHEDULED_BY_CLINIC) ---
490
454
  else if (after.status === AppointmentStatus.RESCHEDULED_BY_CLINIC) {
491
- Logger.info(
492
- `[AggService] Appt ${after.id} status -> RESCHEDULED_BY_CLINIC.`
493
- );
455
+ Logger.info(`[AggService] Appt ${after.id} status -> RESCHEDULED_BY_CLINIC.`);
494
456
  await this.updateRelatedPatientRequirementInstances(
495
457
  before, // Pass the 'before' state for old requirements
496
- PatientRequirementOverallStatus.SUPERSEDED_RESCHEDULE
458
+ PatientRequirementOverallStatus.SUPERSEDED_RESCHEDULE,
497
459
  );
498
460
 
499
461
  // First update the calendar event times with new proposed times
500
- await this.calendarAdminService.updateAppointmentCalendarEventsTime(
501
- after,
502
- {
503
- start: after.appointmentStartTime,
504
- end: after.appointmentEndTime,
505
- }
506
- );
462
+ await this.calendarAdminService.updateAppointmentCalendarEventsTime(after, {
463
+ start: after.appointmentStartTime,
464
+ end: after.appointmentEndTime,
465
+ });
507
466
 
508
467
  // Then update calendar events to PENDING status (waiting for patient confirmation)
509
468
  await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
510
469
  after,
511
- CalendarEventStatus.PENDING
470
+ CalendarEventStatus.PENDING,
512
471
  );
513
472
 
514
473
  // Send reschedule proposal email to patient
515
474
  if (patientSensitiveInfo?.email && patientProfile) {
516
475
  Logger.info(
517
- `[AggService] Sending reschedule proposal email to patient ${patientSensitiveInfo.email}`
476
+ `[AggService] Sending reschedule proposal email to patient ${patientSensitiveInfo.email}`,
518
477
  );
519
478
  const rescheduleEmailData = {
520
479
  appointment: after, // The new state of the appointment
@@ -523,12 +482,12 @@ export class AppointmentAggregationService {
523
482
  previousEndTime: before.appointmentEndTime,
524
483
  };
525
484
  await this.appointmentMailingService.sendAppointmentRescheduledProposalEmail(
526
- rescheduleEmailData as any // TODO: Properly import PatientProfileInfo and types
485
+ rescheduleEmailData as any, // TODO: Properly import PatientProfileInfo and types
527
486
  );
528
487
  }
529
488
 
530
489
  Logger.info(
531
- `[AggService] TODO: Send reschedule proposal notifications to practitioner as well.`
490
+ `[AggService] TODO: Send reschedule proposal notifications to practitioner as well.`,
532
491
  );
533
492
  // TODO: Update calendar event to reflect proposed new time via calendarAdminService.
534
493
  }
@@ -545,21 +504,18 @@ export class AppointmentAggregationService {
545
504
  // Update existing requirements as superseded and create new ones
546
505
  await this.updateRelatedPatientRequirementInstances(
547
506
  before,
548
- PatientRequirementOverallStatus.SUPERSEDED_RESCHEDULE
507
+ PatientRequirementOverallStatus.SUPERSEDED_RESCHEDULE,
549
508
  );
550
509
  await this.createPreAppointmentRequirementInstances(after);
551
510
 
552
511
  // Update calendar event times with new times
553
- await this.calendarAdminService.updateAppointmentCalendarEventsTime(
554
- after,
555
- {
556
- start: after.appointmentStartTime,
557
- end: after.appointmentEndTime,
558
- }
559
- );
512
+ await this.calendarAdminService.updateAppointmentCalendarEventsTime(after, {
513
+ start: after.appointmentStartTime,
514
+ end: after.appointmentEndTime,
515
+ });
560
516
  } else {
561
517
  Logger.warn(
562
- `[AggService] Independent time change detected for ${after.id} with status ${after.status}. Review implications for requirements and calendar.`
518
+ `[AggService] Independent time change detected for ${after.id} with status ${after.status}. Review implications for requirements and calendar.`,
563
519
  );
564
520
  }
565
521
  }
@@ -568,17 +524,21 @@ export class AppointmentAggregationService {
568
524
  // const paymentStatusChanged = before.paymentStatus !== after.paymentStatus;
569
525
  // if (paymentStatusChanged && after.paymentStatus === PaymentStatus.PAID) { ... }
570
526
 
527
+ // Handle Zone Photos Changes
528
+ if (zonePhotosChanged) {
529
+ Logger.info(`[AggService] Zone photos changed for appointment ${after.id}`);
530
+ await this.handleZonePhotosUpdate(before, after);
531
+ }
532
+
571
533
  // TODO: Handle Review Added
572
534
  // const reviewAdded = !before.reviewInfo && after.reviewInfo;
573
535
  // if (reviewAdded) { ... }
574
536
 
575
- Logger.info(
576
- `[AggService] Successfully processed UPDATE for appointment: ${after.id}`
577
- );
537
+ Logger.info(`[AggService] Successfully processed UPDATE for appointment: ${after.id}`);
578
538
  } catch (error) {
579
539
  Logger.error(
580
540
  `[AggService] Critical error in handleAppointmentUpdate for appointment ${after.id}:`,
581
- error
541
+ error,
582
542
  );
583
543
  }
584
544
  }
@@ -588,22 +548,16 @@ export class AppointmentAggregationService {
588
548
  * @param deletedAppointment - The Appointment object that was deleted.
589
549
  * @returns {Promise<void>}
590
550
  */
591
- async handleAppointmentDelete(
592
- deletedAppointment: Appointment
593
- ): Promise<void> {
594
- Logger.info(
595
- `[AggService] Handling DELETE for appointment: ${deletedAppointment.id}`
596
- );
551
+ async handleAppointmentDelete(deletedAppointment: Appointment): Promise<void> {
552
+ Logger.info(`[AggService] Handling DELETE for appointment: ${deletedAppointment.id}`);
597
553
  // Similar to cancellation
598
554
  await this.updateRelatedPatientRequirementInstances(
599
555
  deletedAppointment,
600
- PatientRequirementOverallStatus.CANCELLED_APPOINTMENT
556
+ PatientRequirementOverallStatus.CANCELLED_APPOINTMENT,
601
557
  );
602
558
 
603
559
  // Fetch patient profile first
604
- const patientProfile = await this.fetchPatientProfile(
605
- deletedAppointment.patientId
606
- );
560
+ const patientProfile = await this.fetchPatientProfile(deletedAppointment.patientId);
607
561
 
608
562
  // Update relationship links if patient profile exists
609
563
  if (patientProfile) {
@@ -611,14 +565,12 @@ export class AppointmentAggregationService {
611
565
  patientProfile,
612
566
  deletedAppointment.practitionerId,
613
567
  deletedAppointment.clinicBranchId,
614
- "cancel"
568
+ 'cancel',
615
569
  );
616
570
  }
617
571
 
618
572
  // Delete all associated calendar events
619
- await this.calendarAdminService.deleteAppointmentCalendarEvents(
620
- deletedAppointment
621
- );
573
+ await this.calendarAdminService.deleteAppointmentCalendarEvents(deletedAppointment);
622
574
 
623
575
  // TODO: Send cancellation/deletion notifications if appropriate (though data is gone)
624
576
  }
@@ -634,16 +586,14 @@ export class AppointmentAggregationService {
634
586
  * @param {Appointment} appointment - The appointment for which to create pre-requirement instances.
635
587
  * @returns {Promise<void>} A promise that resolves when the operation is complete.
636
588
  */
637
- private async createPreAppointmentRequirementInstances(
638
- appointment: Appointment
639
- ): Promise<void> {
589
+ private async createPreAppointmentRequirementInstances(appointment: Appointment): Promise<void> {
640
590
  Logger.info(
641
- `[AggService] Creating PRE-appointment requirement instances for appt: ${appointment.id}, patient: ${appointment.patientId}`
591
+ `[AggService] Creating PRE-appointment requirement instances for appt: ${appointment.id}, patient: ${appointment.patientId}`,
642
592
  );
643
593
 
644
594
  if (!appointment.procedureId) {
645
595
  Logger.warn(
646
- `[AggService] Appointment ${appointment.id} has no procedureId. Cannot create pre-requirement instances.`
596
+ `[AggService] Appointment ${appointment.id} has no procedureId. Cannot create pre-requirement instances.`,
647
597
  );
648
598
  return;
649
599
  }
@@ -653,7 +603,7 @@ export class AppointmentAggregationService {
653
603
  appointment.preProcedureRequirements.length === 0
654
604
  ) {
655
605
  Logger.info(
656
- `[AggService] No preProcedureRequirements found on appointment ${appointment.id}. Nothing to create.`
606
+ `[AggService] No preProcedureRequirements found on appointment ${appointment.id}. Nothing to create.`,
657
607
  );
658
608
  return;
659
609
  }
@@ -669,21 +619,21 @@ export class AppointmentAggregationService {
669
619
  `[AggService] Found ${
670
620
  appointment.preProcedureRequirements.length
671
621
  } pre-requirements to process: ${JSON.stringify(
672
- appointment.preProcedureRequirements.map((r) => ({
622
+ appointment.preProcedureRequirements.map(r => ({
673
623
  id: r.id,
674
624
  name: r.name,
675
625
  type: r.type,
676
626
  isActive: r.isActive,
677
627
  hasTimeframe: !!r.timeframe,
678
628
  notifyAtLength: r.timeframe?.notifyAt?.length || 0,
679
- }))
680
- )}`
629
+ })),
630
+ )}`,
681
631
  );
682
632
 
683
633
  for (const template of appointment.preProcedureRequirements) {
684
634
  if (!template) {
685
635
  Logger.warn(
686
- `[AggService] Found null/undefined template in preProcedureRequirements array`
636
+ `[AggService] Found null/undefined template in preProcedureRequirements array`,
687
637
  );
688
638
  continue;
689
639
  }
@@ -691,7 +641,7 @@ export class AppointmentAggregationService {
691
641
  // Ensure it's an active, PRE-type requirement
692
642
  if (template.type !== RequirementType.PRE || !template.isActive) {
693
643
  Logger.debug(
694
- `[AggService] Skipping template ${template.id} (${template.name}): not an active PRE requirement.`
644
+ `[AggService] Skipping template ${template.id} (${template.name}): not an active PRE requirement.`,
695
645
  );
696
646
  continue;
697
647
  }
@@ -702,12 +652,12 @@ export class AppointmentAggregationService {
702
652
  template.timeframe.notifyAt.length === 0
703
653
  ) {
704
654
  Logger.warn(
705
- `[AggService] Template ${template.id} (${template.name}) has no timeframe.notifyAt values. Creating with empty instructions.`
655
+ `[AggService] Template ${template.id} (${template.name}) has no timeframe.notifyAt values. Creating with empty instructions.`,
706
656
  );
707
657
  }
708
658
 
709
659
  Logger.debug(
710
- `[AggService] Processing template ${template.id} (${template.name}) for appt ${appointment.id}`
660
+ `[AggService] Processing template ${template.id} (${template.name}) for appt ${appointment.id}`,
711
661
  );
712
662
 
713
663
  const newInstanceRef = this.db
@@ -717,18 +667,14 @@ export class AppointmentAggregationService {
717
667
  .doc(); // Auto-generate ID for the new instance
718
668
 
719
669
  // Log the path for debugging
720
- Logger.debug(
721
- `[AggService] Created doc reference: ${newInstanceRef.path}`
722
- );
670
+ Logger.debug(`[AggService] Created doc reference: ${newInstanceRef.path}`);
723
671
 
724
672
  const instructions: PatientRequirementInstruction[] = (
725
673
  template.timeframe?.notifyAt || []
726
- ).map((notifyAtValue) => {
674
+ ).map(notifyAtValue => {
727
675
  let dueTime: any = appointment.appointmentStartTime;
728
- if (template.timeframe && typeof notifyAtValue === "number") {
729
- const dueDateTime = new Date(
730
- appointment.appointmentStartTime.toMillis()
731
- );
676
+ if (template.timeframe && typeof notifyAtValue === 'number') {
677
+ const dueDateTime = new Date(appointment.appointmentStartTime.toMillis());
732
678
  if (template.timeframe.unit === TimeUnit.DAYS) {
733
679
  dueDateTime.setDate(dueDateTime.getDate() - notifyAtValue);
734
680
  } else if (template.timeframe.unit === TimeUnit.HOURS) {
@@ -739,18 +685,13 @@ export class AppointmentAggregationService {
739
685
 
740
686
  // TODO: Determine source or default for 'actionableWindow' - consult requirements for PatientRequirementInstruction
741
687
  const actionableWindowHours =
742
- template.importance === "high"
743
- ? 1
744
- : template.importance === "medium"
745
- ? 4
746
- : 15; // Default to 15 hours for low importance; // Placeholder default, TODO: Define source
688
+ template.importance === 'high' ? 1 : template.importance === 'medium' ? 4 : 15; // Default to 15 hours for low importance; // Placeholder default, TODO: Define source
747
689
 
748
690
  const instructionObject: PatientRequirementInstruction = {
749
- instructionId:
750
- `${template.id}_${notifyAtValue}_${newInstanceRef.id}`.replace(
751
- /[^a-zA-Z0-9_]/g,
752
- "_"
753
- ),
691
+ instructionId: `${template.id}_${notifyAtValue}_${newInstanceRef.id}`.replace(
692
+ /[^a-zA-Z0-9_]/g,
693
+ '_',
694
+ ),
754
695
  instructionText: template.description || template.name,
755
696
  dueTime: dueTime as any,
756
697
  actionableWindow: actionableWindowHours, // Directly assigning the placeholder default value
@@ -786,7 +727,7 @@ export class AppointmentAggregationService {
786
727
  appointmentId: newInstanceData.appointmentId,
787
728
  requirementName: newInstanceData.requirementName,
788
729
  instructionsCount: newInstanceData.instructions.length,
789
- })}`
730
+ })}`,
790
731
  );
791
732
 
792
733
  batch.set(newInstanceRef, newInstanceData);
@@ -798,7 +739,7 @@ export class AppointmentAggregationService {
798
739
 
799
740
  instancesCreatedCount++;
800
741
  Logger.debug(
801
- `[AggService] Added PatientRequirementInstance ${newInstanceRef.id} to batch for template ${template.id}.`
742
+ `[AggService] Added PatientRequirementInstance ${newInstanceRef.id} to batch for template ${template.id}.`,
802
743
  );
803
744
  }
804
745
 
@@ -806,7 +747,7 @@ export class AppointmentAggregationService {
806
747
  try {
807
748
  await batch.commit();
808
749
  Logger.info(
809
- `[AggService] Successfully created ${instancesCreatedCount} PRE_APPOINTMENT requirement instances for appointment ${appointment.id}.`
750
+ `[AggService] Successfully created ${instancesCreatedCount} PRE_APPOINTMENT requirement instances for appointment ${appointment.id}.`,
810
751
  );
811
752
 
812
753
  // Verify creation success
@@ -815,116 +756,106 @@ export class AppointmentAggregationService {
815
756
  .collection(PATIENTS_COLLECTION)
816
757
  .doc(appointment.patientId)
817
758
  .collection(PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME)
818
- .where("appointmentId", "==", appointment.id)
759
+ .where('appointmentId', '==', appointment.id)
819
760
  .get();
820
761
 
821
762
  if (verifySnapshot.empty) {
822
763
  Logger.warn(
823
- `[AggService] Batch commit reported success but documents not found! Attempting direct creation as fallback...`
764
+ `[AggService] Batch commit reported success but documents not found! Attempting direct creation as fallback...`,
824
765
  );
825
766
 
826
767
  // Fallback to direct creation if batch worked but docs aren't there
827
- const fallbackPromises = createdInstances.map(
828
- async ({ ref, data }) => {
829
- try {
830
- await ref.set(data);
831
- Logger.info(
832
- `[AggService] Fallback direct creation success for ${ref.id}`
833
- );
834
- return true;
835
- } catch (fallbackError) {
836
- Logger.error(
837
- `[AggService] Fallback direct creation failed for ${ref.id}:`,
838
- fallbackError
839
- );
840
- return false;
841
- }
768
+ const fallbackPromises = createdInstances.map(async ({ ref, data }) => {
769
+ try {
770
+ await ref.set(data);
771
+ Logger.info(`[AggService] Fallback direct creation success for ${ref.id}`);
772
+ return true;
773
+ } catch (fallbackError) {
774
+ Logger.error(
775
+ `[AggService] Fallback direct creation failed for ${ref.id}:`,
776
+ fallbackError,
777
+ );
778
+ return false;
842
779
  }
843
- );
780
+ });
844
781
 
845
- const fallbackResults = await Promise.allSettled(
846
- fallbackPromises
847
- );
782
+ const fallbackResults = await Promise.allSettled(fallbackPromises);
848
783
  const successCount = fallbackResults.filter(
849
- (r) => r.status === "fulfilled" && r.value === true
784
+ r => r.status === 'fulfilled' && r.value === true,
850
785
  ).length;
851
786
 
852
787
  if (successCount > 0) {
853
788
  Logger.info(
854
- `[AggService] Fallback mechanism created ${successCount} out of ${createdInstances.length} requirements`
789
+ `[AggService] Fallback mechanism created ${successCount} out of ${createdInstances.length} requirements`,
855
790
  );
856
791
  } else {
857
792
  Logger.error(
858
- `[AggService] Both batch and fallback mechanisms failed to create requirements`
793
+ `[AggService] Both batch and fallback mechanisms failed to create requirements`,
859
794
  );
860
795
  throw new Error(
861
- "Failed to create patient requirements through both batch and direct methods"
796
+ 'Failed to create patient requirements through both batch and direct methods',
862
797
  );
863
798
  }
864
799
  } else {
865
800
  Logger.info(
866
- `[AggService] Verification confirmed ${verifySnapshot.size} requirement documents created`
801
+ `[AggService] Verification confirmed ${verifySnapshot.size} requirement documents created`,
867
802
  );
868
803
  }
869
804
  } catch (verifyError) {
870
805
  Logger.error(
871
806
  `[AggService] Error during verification of created requirements:`,
872
- verifyError
807
+ verifyError,
873
808
  );
874
809
  }
875
810
  } catch (commitError) {
876
811
  Logger.error(
877
812
  `[AggService] Error committing batch for PRE_APPOINTMENT requirement instances for appointment ${appointment.id}:`,
878
- commitError
813
+ commitError,
879
814
  );
880
815
 
881
816
  // Try direct creation as fallback
882
817
  Logger.info(`[AggService] Attempting direct creation as fallback...`);
883
- const fallbackPromises = createdInstances.map(
884
- async ({ ref, data }) => {
885
- try {
886
- await ref.set(data);
887
- Logger.info(
888
- `[AggService] Fallback direct creation success for ${ref.id}`
889
- );
890
- return true;
891
- } catch (fallbackError) {
892
- Logger.error(
893
- `[AggService] Fallback direct creation failed for ${ref.id}:`,
894
- fallbackError
895
- );
896
- return false;
897
- }
818
+ const fallbackPromises = createdInstances.map(async ({ ref, data }) => {
819
+ try {
820
+ await ref.set(data);
821
+ Logger.info(`[AggService] Fallback direct creation success for ${ref.id}`);
822
+ return true;
823
+ } catch (fallbackError) {
824
+ Logger.error(
825
+ `[AggService] Fallback direct creation failed for ${ref.id}:`,
826
+ fallbackError,
827
+ );
828
+ return false;
898
829
  }
899
- );
830
+ });
900
831
 
901
832
  const fallbackResults = await Promise.allSettled(fallbackPromises);
902
833
  const successCount = fallbackResults.filter(
903
- (r) => r.status === "fulfilled" && r.value === true
834
+ r => r.status === 'fulfilled' && r.value === true,
904
835
  ).length;
905
836
 
906
837
  if (successCount > 0) {
907
838
  Logger.info(
908
- `[AggService] Fallback mechanism created ${successCount} out of ${createdInstances.length} requirements`
839
+ `[AggService] Fallback mechanism created ${successCount} out of ${createdInstances.length} requirements`,
909
840
  );
910
841
  } else {
911
842
  Logger.error(
912
- `[AggService] Both batch and fallback mechanisms failed to create requirements`
843
+ `[AggService] Both batch and fallback mechanisms failed to create requirements`,
913
844
  );
914
845
  throw new Error(
915
- "Failed to create patient requirements through both batch and direct methods"
846
+ 'Failed to create patient requirements through both batch and direct methods',
916
847
  );
917
848
  }
918
849
  }
919
850
  } else {
920
851
  Logger.info(
921
- `[AggService] No new PRE_APPOINTMENT requirement instances were prepared for batch commit for appointment ${appointment.id}.`
852
+ `[AggService] No new PRE_APPOINTMENT requirement instances were prepared for batch commit for appointment ${appointment.id}.`,
922
853
  );
923
854
  }
924
855
  } catch (error) {
925
856
  Logger.error(
926
857
  `[AggService] Error creating PRE_APPOINTMENT requirement instances for appointment ${appointment.id}:`,
927
- error
858
+ error,
928
859
  );
929
860
  throw error; // Re-throw to ensure the caller knows there was a problem
930
861
  }
@@ -939,16 +870,14 @@ export class AppointmentAggregationService {
939
870
  * @param {Appointment} appointment - The appointment for which to create post-requirement instances.
940
871
  * @returns {Promise<void>} A promise that resolves when the operation is complete.
941
872
  */
942
- private async createPostAppointmentRequirementInstances(
943
- appointment: Appointment
944
- ): Promise<void> {
873
+ private async createPostAppointmentRequirementInstances(appointment: Appointment): Promise<void> {
945
874
  Logger.info(
946
- `[AggService] Creating POST-appointment requirement instances for appt: ${appointment.id}, patient: ${appointment.patientId}`
875
+ `[AggService] Creating POST-appointment requirement instances for appt: ${appointment.id}, patient: ${appointment.patientId}`,
947
876
  );
948
877
 
949
878
  if (!appointment.procedureId) {
950
879
  Logger.warn(
951
- `[AggService] Appointment ${appointment.id} has no procedureId. Cannot create post-requirement instances.`
880
+ `[AggService] Appointment ${appointment.id} has no procedureId. Cannot create post-requirement instances.`,
952
881
  );
953
882
  return;
954
883
  }
@@ -958,7 +887,7 @@ export class AppointmentAggregationService {
958
887
  appointment.postProcedureRequirements.length === 0
959
888
  ) {
960
889
  Logger.info(
961
- `[AggService] No postProcedureRequirements found on appointment ${appointment.id}. Nothing to create.`
890
+ `[AggService] No postProcedureRequirements found on appointment ${appointment.id}. Nothing to create.`,
962
891
  );
963
892
  return;
964
893
  }
@@ -974,21 +903,21 @@ export class AppointmentAggregationService {
974
903
  `[AggService] Found ${
975
904
  appointment.postProcedureRequirements.length
976
905
  } post-requirements to process: ${JSON.stringify(
977
- appointment.postProcedureRequirements.map((r) => ({
906
+ appointment.postProcedureRequirements.map(r => ({
978
907
  id: r.id,
979
908
  name: r.name,
980
909
  type: r.type,
981
910
  isActive: r.isActive,
982
911
  hasTimeframe: !!r.timeframe,
983
912
  notifyAtLength: r.timeframe?.notifyAt?.length || 0,
984
- }))
985
- )}`
913
+ })),
914
+ )}`,
986
915
  );
987
916
 
988
917
  for (const template of appointment.postProcedureRequirements) {
989
918
  if (!template) {
990
919
  Logger.warn(
991
- `[AggService] Found null/undefined template in postProcedureRequirements array`
920
+ `[AggService] Found null/undefined template in postProcedureRequirements array`,
992
921
  );
993
922
  continue;
994
923
  }
@@ -996,7 +925,7 @@ export class AppointmentAggregationService {
996
925
  // Ensure it's an active, POST-type requirement
997
926
  if (template.type !== RequirementType.POST || !template.isActive) {
998
927
  Logger.debug(
999
- `[AggService] Skipping template ${template.id} (${template.name}): not an active POST requirement.`
928
+ `[AggService] Skipping template ${template.id} (${template.name}): not an active POST requirement.`,
1000
929
  );
1001
930
  continue;
1002
931
  }
@@ -1007,12 +936,12 @@ export class AppointmentAggregationService {
1007
936
  template.timeframe.notifyAt.length === 0
1008
937
  ) {
1009
938
  Logger.warn(
1010
- `[AggService] Template ${template.id} (${template.name}) has no timeframe.notifyAt values. Creating with empty instructions.`
939
+ `[AggService] Template ${template.id} (${template.name}) has no timeframe.notifyAt values. Creating with empty instructions.`,
1011
940
  );
1012
941
  }
1013
942
 
1014
943
  Logger.debug(
1015
- `[AggService] Processing template ${template.id} (${template.name}) for appt ${appointment.id}`
944
+ `[AggService] Processing template ${template.id} (${template.name}) for appt ${appointment.id}`,
1016
945
  );
1017
946
 
1018
947
  const newInstanceRef = this.db
@@ -1022,18 +951,14 @@ export class AppointmentAggregationService {
1022
951
  .doc(); // Auto-generate ID for the new instance
1023
952
 
1024
953
  // Log the path for debugging
1025
- Logger.debug(
1026
- `[AggService] Created doc reference: ${newInstanceRef.path}`
1027
- );
954
+ Logger.debug(`[AggService] Created doc reference: ${newInstanceRef.path}`);
1028
955
 
1029
956
  const instructions: PatientRequirementInstruction[] = (
1030
957
  template.timeframe?.notifyAt || []
1031
- ).map((notifyAtValue) => {
958
+ ).map(notifyAtValue => {
1032
959
  let dueTime: any = appointment.appointmentEndTime;
1033
- if (template.timeframe && typeof notifyAtValue === "number") {
1034
- const dueDateTime = new Date(
1035
- appointment.appointmentEndTime.toMillis()
1036
- );
960
+ if (template.timeframe && typeof notifyAtValue === 'number') {
961
+ const dueDateTime = new Date(appointment.appointmentEndTime.toMillis());
1037
962
  // For POST requirements, notifyAtValue means AFTER the event
1038
963
  if (template.timeframe.unit === TimeUnit.DAYS) {
1039
964
  dueDateTime.setDate(dueDateTime.getDate() + notifyAtValue);
@@ -1044,18 +969,13 @@ export class AppointmentAggregationService {
1044
969
  }
1045
970
 
1046
971
  const actionableWindowHours =
1047
- template.importance === "high"
1048
- ? 1
1049
- : template.importance === "medium"
1050
- ? 4
1051
- : 15; // Default to 15 hours for low importance
972
+ template.importance === 'high' ? 1 : template.importance === 'medium' ? 4 : 15; // Default to 15 hours for low importance
1052
973
 
1053
974
  const instructionObject: PatientRequirementInstruction = {
1054
- instructionId:
1055
- `${template.id}_${notifyAtValue}_${newInstanceRef.id}`.replace(
1056
- /[^a-zA-Z0-9_]/g,
1057
- "_"
1058
- ),
975
+ instructionId: `${template.id}_${notifyAtValue}_${newInstanceRef.id}`.replace(
976
+ /[^a-zA-Z0-9_]/g,
977
+ '_',
978
+ ),
1059
979
  instructionText: template.description || template.name,
1060
980
  dueTime: dueTime as any,
1061
981
  actionableWindow: actionableWindowHours,
@@ -1092,7 +1012,7 @@ export class AppointmentAggregationService {
1092
1012
  appointmentId: newInstanceData.appointmentId,
1093
1013
  requirementName: newInstanceData.requirementName,
1094
1014
  instructionsCount: newInstanceData.instructions.length,
1095
- })}`
1015
+ })}`,
1096
1016
  );
1097
1017
 
1098
1018
  batch.set(newInstanceRef, newInstanceData);
@@ -1104,7 +1024,7 @@ export class AppointmentAggregationService {
1104
1024
 
1105
1025
  instancesCreatedCount++;
1106
1026
  Logger.debug(
1107
- `[AggService] Added PatientRequirementInstance ${newInstanceRef.id} to batch for template ${template.id}.`
1027
+ `[AggService] Added PatientRequirementInstance ${newInstanceRef.id} to batch for template ${template.id}.`,
1108
1028
  );
1109
1029
  }
1110
1030
 
@@ -1112,7 +1032,7 @@ export class AppointmentAggregationService {
1112
1032
  try {
1113
1033
  await batch.commit();
1114
1034
  Logger.info(
1115
- `[AggService] Successfully created ${instancesCreatedCount} POST_APPOINTMENT requirement instances for appointment ${appointment.id}.`
1035
+ `[AggService] Successfully created ${instancesCreatedCount} POST_APPOINTMENT requirement instances for appointment ${appointment.id}.`,
1116
1036
  );
1117
1037
 
1118
1038
  // Verify creation success
@@ -1121,116 +1041,106 @@ export class AppointmentAggregationService {
1121
1041
  .collection(PATIENTS_COLLECTION)
1122
1042
  .doc(appointment.patientId)
1123
1043
  .collection(PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME)
1124
- .where("appointmentId", "==", appointment.id)
1044
+ .where('appointmentId', '==', appointment.id)
1125
1045
  .get();
1126
1046
 
1127
1047
  if (verifySnapshot.empty) {
1128
1048
  Logger.warn(
1129
- `[AggService] Batch commit reported success but documents not found! Attempting direct creation as fallback...`
1049
+ `[AggService] Batch commit reported success but documents not found! Attempting direct creation as fallback...`,
1130
1050
  );
1131
1051
 
1132
1052
  // Fallback to direct creation if batch worked but docs aren't there
1133
- const fallbackPromises = createdInstances.map(
1134
- async ({ ref, data }) => {
1135
- try {
1136
- await ref.set(data);
1137
- Logger.info(
1138
- `[AggService] Fallback direct creation success for ${ref.id}`
1139
- );
1140
- return true;
1141
- } catch (fallbackError) {
1142
- Logger.error(
1143
- `[AggService] Fallback direct creation failed for ${ref.id}:`,
1144
- fallbackError
1145
- );
1146
- return false;
1147
- }
1053
+ const fallbackPromises = createdInstances.map(async ({ ref, data }) => {
1054
+ try {
1055
+ await ref.set(data);
1056
+ Logger.info(`[AggService] Fallback direct creation success for ${ref.id}`);
1057
+ return true;
1058
+ } catch (fallbackError) {
1059
+ Logger.error(
1060
+ `[AggService] Fallback direct creation failed for ${ref.id}:`,
1061
+ fallbackError,
1062
+ );
1063
+ return false;
1148
1064
  }
1149
- );
1065
+ });
1150
1066
 
1151
- const fallbackResults = await Promise.allSettled(
1152
- fallbackPromises
1153
- );
1067
+ const fallbackResults = await Promise.allSettled(fallbackPromises);
1154
1068
  const successCount = fallbackResults.filter(
1155
- (r) => r.status === "fulfilled" && r.value === true
1069
+ r => r.status === 'fulfilled' && r.value === true,
1156
1070
  ).length;
1157
1071
 
1158
1072
  if (successCount > 0) {
1159
1073
  Logger.info(
1160
- `[AggService] Fallback mechanism created ${successCount} out of ${createdInstances.length} requirements`
1074
+ `[AggService] Fallback mechanism created ${successCount} out of ${createdInstances.length} requirements`,
1161
1075
  );
1162
1076
  } else {
1163
1077
  Logger.error(
1164
- `[AggService] Both batch and fallback mechanisms failed to create requirements`
1078
+ `[AggService] Both batch and fallback mechanisms failed to create requirements`,
1165
1079
  );
1166
1080
  throw new Error(
1167
- "Failed to create patient requirements through both batch and direct methods"
1081
+ 'Failed to create patient requirements through both batch and direct methods',
1168
1082
  );
1169
1083
  }
1170
1084
  } else {
1171
1085
  Logger.info(
1172
- `[AggService] Verification confirmed ${verifySnapshot.size} requirement documents created`
1086
+ `[AggService] Verification confirmed ${verifySnapshot.size} requirement documents created`,
1173
1087
  );
1174
1088
  }
1175
1089
  } catch (verifyError) {
1176
1090
  Logger.error(
1177
1091
  `[AggService] Error during verification of created requirements:`,
1178
- verifyError
1092
+ verifyError,
1179
1093
  );
1180
1094
  }
1181
1095
  } catch (commitError) {
1182
1096
  Logger.error(
1183
1097
  `[AggService] Error committing batch for POST_APPOINTMENT requirement instances for appointment ${appointment.id}:`,
1184
- commitError
1098
+ commitError,
1185
1099
  );
1186
1100
 
1187
1101
  // Try direct creation as fallback
1188
1102
  Logger.info(`[AggService] Attempting direct creation as fallback...`);
1189
- const fallbackPromises = createdInstances.map(
1190
- async ({ ref, data }) => {
1191
- try {
1192
- await ref.set(data);
1193
- Logger.info(
1194
- `[AggService] Fallback direct creation success for ${ref.id}`
1195
- );
1196
- return true;
1197
- } catch (fallbackError) {
1198
- Logger.error(
1199
- `[AggService] Fallback direct creation failed for ${ref.id}:`,
1200
- fallbackError
1201
- );
1202
- return false;
1203
- }
1103
+ const fallbackPromises = createdInstances.map(async ({ ref, data }) => {
1104
+ try {
1105
+ await ref.set(data);
1106
+ Logger.info(`[AggService] Fallback direct creation success for ${ref.id}`);
1107
+ return true;
1108
+ } catch (fallbackError) {
1109
+ Logger.error(
1110
+ `[AggService] Fallback direct creation failed for ${ref.id}:`,
1111
+ fallbackError,
1112
+ );
1113
+ return false;
1204
1114
  }
1205
- );
1115
+ });
1206
1116
 
1207
1117
  const fallbackResults = await Promise.allSettled(fallbackPromises);
1208
1118
  const successCount = fallbackResults.filter(
1209
- (r) => r.status === "fulfilled" && r.value === true
1119
+ r => r.status === 'fulfilled' && r.value === true,
1210
1120
  ).length;
1211
1121
 
1212
1122
  if (successCount > 0) {
1213
1123
  Logger.info(
1214
- `[AggService] Fallback mechanism created ${successCount} out of ${createdInstances.length} requirements`
1124
+ `[AggService] Fallback mechanism created ${successCount} out of ${createdInstances.length} requirements`,
1215
1125
  );
1216
1126
  } else {
1217
1127
  Logger.error(
1218
- `[AggService] Both batch and fallback mechanisms failed to create requirements`
1128
+ `[AggService] Both batch and fallback mechanisms failed to create requirements`,
1219
1129
  );
1220
1130
  throw new Error(
1221
- "Failed to create patient requirements through both batch and direct methods"
1131
+ 'Failed to create patient requirements through both batch and direct methods',
1222
1132
  );
1223
1133
  }
1224
1134
  }
1225
1135
  } else {
1226
1136
  Logger.info(
1227
- `[AggService] No new POST_APPOINTMENT requirement instances were prepared for batch commit for appointment ${appointment.id}.`
1137
+ `[AggService] No new POST_APPOINTMENT requirement instances were prepared for batch commit for appointment ${appointment.id}.`,
1228
1138
  );
1229
1139
  }
1230
1140
  } catch (error) {
1231
1141
  Logger.error(
1232
1142
  `[AggService] Error creating POST_APPOINTMENT requirement instances for appointment ${appointment.id}:`,
1233
- error
1143
+ error,
1234
1144
  );
1235
1145
  throw error; // Re-throw to ensure the caller knows there was a problem
1236
1146
  }
@@ -1247,16 +1157,16 @@ export class AppointmentAggregationService {
1247
1157
  private async updateRelatedPatientRequirementInstances(
1248
1158
  appointment: Appointment,
1249
1159
  newOverallStatus: PatientRequirementOverallStatus,
1250
- _previousAppointmentData?: Appointment // Not used in this basic implementation, but kept for signature consistency
1160
+ _previousAppointmentData?: Appointment, // Not used in this basic implementation, but kept for signature consistency
1251
1161
  ): Promise<void> {
1252
1162
  Logger.info(
1253
- `[AggService] Updating related patient req instances for appt ${appointment.id} (patient: ${appointment.patientId}) to ${newOverallStatus}`
1163
+ `[AggService] Updating related patient req instances for appt ${appointment.id} (patient: ${appointment.patientId}) to ${newOverallStatus}`,
1254
1164
  );
1255
1165
 
1256
1166
  if (!appointment.id || !appointment.patientId) {
1257
1167
  Logger.error(
1258
- "[AggService] updateRelatedPatientRequirementInstances called with missing appointmentId or patientId.",
1259
- { appointmentId: appointment.id, patientId: appointment.patientId }
1168
+ '[AggService] updateRelatedPatientRequirementInstances called with missing appointmentId or patientId.',
1169
+ { appointmentId: appointment.id, patientId: appointment.patientId },
1260
1170
  );
1261
1171
  return;
1262
1172
  }
@@ -1266,12 +1176,12 @@ export class AppointmentAggregationService {
1266
1176
  .collection(PATIENTS_COLLECTION)
1267
1177
  .doc(appointment.patientId)
1268
1178
  .collection(PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME)
1269
- .where("appointmentId", "==", appointment.id)
1179
+ .where('appointmentId', '==', appointment.id)
1270
1180
  .get();
1271
1181
 
1272
1182
  if (instancesSnapshot.empty) {
1273
1183
  Logger.info(
1274
- `[AggService] No patient requirement instances found for appointment ${appointment.id} to update.`
1184
+ `[AggService] No patient requirement instances found for appointment ${appointment.id} to update.`,
1275
1185
  );
1276
1186
  return;
1277
1187
  }
@@ -1279,13 +1189,12 @@ export class AppointmentAggregationService {
1279
1189
  const batch = this.db.batch();
1280
1190
  let instancesUpdatedCount = 0;
1281
1191
 
1282
- instancesSnapshot.docs.forEach((doc) => {
1192
+ instancesSnapshot.docs.forEach(doc => {
1283
1193
  const instance = doc.data() as PatientRequirementInstance;
1284
1194
  // Update only if the status is actually different and not already in a terminal state like FAILED_TO_PROCESS
1285
1195
  if (
1286
1196
  instance.overallStatus !== newOverallStatus &&
1287
- instance.overallStatus !==
1288
- PatientRequirementOverallStatus.FAILED_TO_PROCESS
1197
+ instance.overallStatus !== PatientRequirementOverallStatus.FAILED_TO_PROCESS
1289
1198
  ) {
1290
1199
  batch.update(doc.ref, {
1291
1200
  overallStatus: newOverallStatus,
@@ -1295,7 +1204,7 @@ export class AppointmentAggregationService {
1295
1204
  });
1296
1205
  instancesUpdatedCount++;
1297
1206
  Logger.debug(
1298
- `[AggService] Added update for PatientRequirementInstance ${doc.id} to batch. New status: ${newOverallStatus}`
1207
+ `[AggService] Added update for PatientRequirementInstance ${doc.id} to batch. New status: ${newOverallStatus}`,
1299
1208
  );
1300
1209
  }
1301
1210
  });
@@ -1303,17 +1212,17 @@ export class AppointmentAggregationService {
1303
1212
  if (instancesUpdatedCount > 0) {
1304
1213
  await batch.commit();
1305
1214
  Logger.info(
1306
- `[AggService] Successfully updated ${instancesUpdatedCount} patient requirement instances for appointment ${appointment.id} to status ${newOverallStatus}.`
1215
+ `[AggService] Successfully updated ${instancesUpdatedCount} patient requirement instances for appointment ${appointment.id} to status ${newOverallStatus}.`,
1307
1216
  );
1308
1217
  } else {
1309
1218
  Logger.info(
1310
- `[AggService] No patient requirement instances needed an update for appointment ${appointment.id}.`
1219
+ `[AggService] No patient requirement instances needed an update for appointment ${appointment.id}.`,
1311
1220
  );
1312
1221
  }
1313
1222
  } catch (error) {
1314
1223
  Logger.error(
1315
1224
  `[AggService] Error updating patient requirement instances for appointment ${appointment.id}:`,
1316
- error
1225
+ error,
1317
1226
  );
1318
1227
  }
1319
1228
  }
@@ -1333,27 +1242,27 @@ export class AppointmentAggregationService {
1333
1242
  patientProfile: PatientProfile,
1334
1243
  practitionerId: string,
1335
1244
  clinicId: string,
1336
- action: "create" | "cancel",
1337
- cancelStatus?: AppointmentStatus
1245
+ action: 'create' | 'cancel',
1246
+ cancelStatus?: AppointmentStatus,
1338
1247
  ): Promise<void> {
1339
1248
  Logger.info(
1340
- `[AggService] Managing patient-clinic-practitioner links for patient ${patientProfile.id}, action: ${action}`
1249
+ `[AggService] Managing patient-clinic-practitioner links for patient ${patientProfile.id}, action: ${action}`,
1341
1250
  );
1342
1251
 
1343
1252
  try {
1344
- if (action === "create") {
1253
+ if (action === 'create') {
1345
1254
  await this.addPatientLinks(patientProfile, practitionerId, clinicId);
1346
- } else if (action === "cancel") {
1255
+ } else if (action === 'cancel') {
1347
1256
  await this.removePatientLinksIfNoActiveAppointments(
1348
1257
  patientProfile,
1349
1258
  practitionerId,
1350
- clinicId
1259
+ clinicId,
1351
1260
  );
1352
1261
  }
1353
1262
  } catch (error) {
1354
1263
  Logger.error(
1355
1264
  `[AggService] Error managing patient-clinic-practitioner links for patient ${patientProfile.id}:`,
1356
- error
1265
+ error,
1357
1266
  );
1358
1267
  }
1359
1268
  }
@@ -1369,52 +1278,45 @@ export class AppointmentAggregationService {
1369
1278
  private async addPatientLinks(
1370
1279
  patientProfile: PatientProfile,
1371
1280
  practitionerId: string,
1372
- clinicId: string
1281
+ clinicId: string,
1373
1282
  ): Promise<void> {
1374
1283
  try {
1375
1284
  // Check if the IDs already exist in the arrays
1376
- const hasDoctor =
1377
- patientProfile.doctorIds?.includes(practitionerId) || false;
1285
+ const hasDoctor = patientProfile.doctorIds?.includes(practitionerId) || false;
1378
1286
  const hasClinic = patientProfile.clinicIds?.includes(clinicId) || false;
1379
1287
 
1380
1288
  // Only update if necessary
1381
1289
  if (!hasDoctor || !hasClinic) {
1382
- const patientRef = this.db
1383
- .collection(PATIENTS_COLLECTION)
1384
- .doc(patientProfile.id);
1290
+ const patientRef = this.db.collection(PATIENTS_COLLECTION).doc(patientProfile.id);
1385
1291
  const updateData: Record<string, any> = {
1386
1292
  updatedAt: admin.firestore.FieldValue.serverTimestamp(),
1387
1293
  };
1388
1294
 
1389
1295
  if (!hasDoctor) {
1390
1296
  Logger.debug(
1391
- `[AggService] Adding practitioner ${practitionerId} to patient ${patientProfile.id}`
1297
+ `[AggService] Adding practitioner ${practitionerId} to patient ${patientProfile.id}`,
1392
1298
  );
1393
- updateData.doctorIds =
1394
- admin.firestore.FieldValue.arrayUnion(practitionerId);
1299
+ updateData.doctorIds = admin.firestore.FieldValue.arrayUnion(practitionerId);
1395
1300
  }
1396
1301
 
1397
1302
  if (!hasClinic) {
1398
- Logger.debug(
1399
- `[AggService] Adding clinic ${clinicId} to patient ${patientProfile.id}`
1400
- );
1401
- updateData.clinicIds =
1402
- admin.firestore.FieldValue.arrayUnion(clinicId);
1303
+ Logger.debug(`[AggService] Adding clinic ${clinicId} to patient ${patientProfile.id}`);
1304
+ updateData.clinicIds = admin.firestore.FieldValue.arrayUnion(clinicId);
1403
1305
  }
1404
1306
 
1405
1307
  await patientRef.update(updateData);
1406
1308
  Logger.info(
1407
- `[AggService] Successfully updated patient ${patientProfile.id} with new links.`
1309
+ `[AggService] Successfully updated patient ${patientProfile.id} with new links.`,
1408
1310
  );
1409
1311
  } else {
1410
1312
  Logger.info(
1411
- `[AggService] Patient ${patientProfile.id} already has links to both practitioner ${practitionerId} and clinic ${clinicId}.`
1313
+ `[AggService] Patient ${patientProfile.id} already has links to both practitioner ${practitionerId} and clinic ${clinicId}.`,
1412
1314
  );
1413
1315
  }
1414
1316
  } catch (error) {
1415
1317
  Logger.error(
1416
1318
  `[AggService] Error updating patient ${patientProfile.id} with new links:`,
1417
- error
1319
+ error,
1418
1320
  );
1419
1321
  throw error;
1420
1322
  }
@@ -1431,30 +1333,28 @@ export class AppointmentAggregationService {
1431
1333
  private async removePatientLinksIfNoActiveAppointments(
1432
1334
  patientProfile: PatientProfile,
1433
1335
  practitionerId: string,
1434
- clinicId: string
1336
+ clinicId: string,
1435
1337
  ): Promise<void> {
1436
1338
  try {
1437
1339
  // Check for active appointments with this practitioner and clinic
1438
1340
  const activePractitionerAppointments = await this.checkActiveAppointments(
1439
1341
  patientProfile.id,
1440
- "practitionerId",
1441
- practitionerId
1342
+ 'practitionerId',
1343
+ practitionerId,
1442
1344
  );
1443
1345
 
1444
1346
  const activeClinicAppointments = await this.checkActiveAppointments(
1445
1347
  patientProfile.id,
1446
- "clinicBranchId",
1447
- clinicId
1348
+ 'clinicBranchId',
1349
+ clinicId,
1448
1350
  );
1449
1351
 
1450
1352
  Logger.info(
1451
- `[AggService] Active appointment count for patient ${patientProfile.id}: With practitioner ${practitionerId}: ${activePractitionerAppointments}, With clinic ${clinicId}: ${activeClinicAppointments}`
1353
+ `[AggService] Active appointment count for patient ${patientProfile.id}: With practitioner ${practitionerId}: ${activePractitionerAppointments}, With clinic ${clinicId}: ${activeClinicAppointments}`,
1452
1354
  );
1453
1355
 
1454
1356
  // Only update if there are no active appointments
1455
- const patientRef = this.db
1456
- .collection(PATIENTS_COLLECTION)
1457
- .doc(patientProfile.id);
1357
+ const patientRef = this.db.collection(PATIENTS_COLLECTION).doc(patientProfile.id);
1458
1358
  const updateData: Record<string, any> = {};
1459
1359
  let updateNeeded = false;
1460
1360
 
@@ -1463,20 +1363,14 @@ export class AppointmentAggregationService {
1463
1363
  patientProfile.doctorIds?.includes(practitionerId)
1464
1364
  ) {
1465
1365
  Logger.debug(
1466
- `[AggService] Removing practitioner ${practitionerId} from patient ${patientProfile.id}`
1366
+ `[AggService] Removing practitioner ${practitionerId} from patient ${patientProfile.id}`,
1467
1367
  );
1468
- updateData.doctorIds =
1469
- admin.firestore.FieldValue.arrayRemove(practitionerId);
1368
+ updateData.doctorIds = admin.firestore.FieldValue.arrayRemove(practitionerId);
1470
1369
  updateNeeded = true;
1471
1370
  }
1472
1371
 
1473
- if (
1474
- activeClinicAppointments === 0 &&
1475
- patientProfile.clinicIds?.includes(clinicId)
1476
- ) {
1477
- Logger.debug(
1478
- `[AggService] Removing clinic ${clinicId} from patient ${patientProfile.id}`
1479
- );
1372
+ if (activeClinicAppointments === 0 && patientProfile.clinicIds?.includes(clinicId)) {
1373
+ Logger.debug(`[AggService] Removing clinic ${clinicId} from patient ${patientProfile.id}`);
1480
1374
  updateData.clinicIds = admin.firestore.FieldValue.arrayRemove(clinicId);
1481
1375
  updateNeeded = true;
1482
1376
  }
@@ -1484,19 +1378,12 @@ export class AppointmentAggregationService {
1484
1378
  if (updateNeeded) {
1485
1379
  updateData.updatedAt = admin.firestore.FieldValue.serverTimestamp();
1486
1380
  await patientRef.update(updateData);
1487
- Logger.info(
1488
- `[AggService] Successfully removed links from patient ${patientProfile.id}`
1489
- );
1381
+ Logger.info(`[AggService] Successfully removed links from patient ${patientProfile.id}`);
1490
1382
  } else {
1491
- Logger.info(
1492
- `[AggService] No links need to be removed from patient ${patientProfile.id}`
1493
- );
1383
+ Logger.info(`[AggService] No links need to be removed from patient ${patientProfile.id}`);
1494
1384
  }
1495
1385
  } catch (error) {
1496
- Logger.error(
1497
- `[AggService] Error removing links from patient profile:`,
1498
- error
1499
- );
1386
+ Logger.error(`[AggService] Error removing links from patient profile:`, error);
1500
1387
  throw error;
1501
1388
  }
1502
1389
  }
@@ -1511,8 +1398,8 @@ export class AppointmentAggregationService {
1511
1398
  */
1512
1399
  private async checkActiveAppointments(
1513
1400
  patientId: string,
1514
- entityField: "practitionerId" | "clinicBranchId",
1515
- entityId: string
1401
+ entityField: 'practitionerId' | 'clinicBranchId',
1402
+ entityId: string,
1516
1403
  ): Promise<number> {
1517
1404
  try {
1518
1405
  // Define all cancelled/inactive appointment statuses
@@ -1524,10 +1411,10 @@ export class AppointmentAggregationService {
1524
1411
  ];
1525
1412
 
1526
1413
  const snapshot = await this.db
1527
- .collection("appointments")
1528
- .where("patientId", "==", patientId)
1529
- .where(entityField, "==", entityId)
1530
- .where("status", "not-in", inactiveStatuses)
1414
+ .collection('appointments')
1415
+ .where('patientId', '==', patientId)
1416
+ .where(entityField, '==', entityId)
1417
+ .where('status', 'not-in', inactiveStatuses)
1531
1418
  .get();
1532
1419
 
1533
1420
  return snapshot.size;
@@ -1538,20 +1425,12 @@ export class AppointmentAggregationService {
1538
1425
  }
1539
1426
 
1540
1427
  // --- Data Fetching Helpers (Consider moving to a data access layer or using existing services if available) ---
1541
- private async fetchPatientProfile(
1542
- patientId: string
1543
- ): Promise<PatientProfile | null> {
1428
+ private async fetchPatientProfile(patientId: string): Promise<PatientProfile | null> {
1544
1429
  try {
1545
- const doc = await this.db
1546
- .collection(PATIENTS_COLLECTION)
1547
- .doc(patientId)
1548
- .get();
1430
+ const doc = await this.db.collection(PATIENTS_COLLECTION).doc(patientId).get();
1549
1431
  return doc.exists ? (doc.data() as PatientProfile) : null;
1550
1432
  } catch (error) {
1551
- Logger.error(
1552
- `[AggService] Error fetching patient profile ${patientId}:`,
1553
- error
1554
- );
1433
+ Logger.error(`[AggService] Error fetching patient profile ${patientId}:`, error);
1555
1434
  return null;
1556
1435
  }
1557
1436
  }
@@ -1561,9 +1440,7 @@ export class AppointmentAggregationService {
1561
1440
  * @param patientId The ID of the patient to fetch sensitive information for.
1562
1441
  * @returns {Promise<PatientSensitiveInfo | null>} The patient sensitive information or null if not found or an error occurs.
1563
1442
  */
1564
- private async fetchPatientSensitiveInfo(
1565
- patientId: string
1566
- ): Promise<PatientSensitiveInfo | null> {
1443
+ private async fetchPatientSensitiveInfo(patientId: string): Promise<PatientSensitiveInfo | null> {
1567
1444
  try {
1568
1445
  // Assuming sensitive info is in a subcollection PATIENT_SENSITIVE_INFO_COLLECTION
1569
1446
  // under the patient's document, and the sensitive info document ID is the patientId itself.
@@ -1575,17 +1452,12 @@ export class AppointmentAggregationService {
1575
1452
  .doc(patientId) // CONFIRM THIS DOCUMENT ID PATTERN
1576
1453
  .get();
1577
1454
  if (!doc.exists) {
1578
- Logger.warn(
1579
- `[AggService] No sensitive info found for patient ${patientId}`
1580
- );
1455
+ Logger.warn(`[AggService] No sensitive info found for patient ${patientId}`);
1581
1456
  return null;
1582
1457
  }
1583
1458
  return doc.data() as PatientSensitiveInfo;
1584
1459
  } catch (error) {
1585
- Logger.error(
1586
- `[AggService] Error fetching patient sensitive info ${patientId}:`,
1587
- error
1588
- );
1460
+ Logger.error(`[AggService] Error fetching patient sensitive info ${patientId}:`, error);
1589
1461
  return null;
1590
1462
  }
1591
1463
  }
@@ -1595,32 +1467,20 @@ export class AppointmentAggregationService {
1595
1467
  * @param practitionerId The ID of the practitioner to fetch.
1596
1468
  * @returns {Promise<Practitioner | null>} The practitioner profile or null if not found or an error occurs.
1597
1469
  */
1598
- private async fetchPractitionerProfile(
1599
- practitionerId: string
1600
- ): Promise<Practitioner | null> {
1470
+ private async fetchPractitionerProfile(practitionerId: string): Promise<Practitioner | null> {
1601
1471
  if (!practitionerId) {
1602
- Logger.warn(
1603
- "[AggService] fetchPractitionerProfile called with no practitionerId."
1604
- );
1472
+ Logger.warn('[AggService] fetchPractitionerProfile called with no practitionerId.');
1605
1473
  return null;
1606
1474
  }
1607
1475
  try {
1608
- const doc = await this.db
1609
- .collection(PRACTITIONERS_COLLECTION)
1610
- .doc(practitionerId)
1611
- .get();
1476
+ const doc = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
1612
1477
  if (!doc.exists) {
1613
- Logger.warn(
1614
- `[AggService] No practitioner profile found for ID ${practitionerId}`
1615
- );
1478
+ Logger.warn(`[AggService] No practitioner profile found for ID ${practitionerId}`);
1616
1479
  return null;
1617
1480
  }
1618
1481
  return doc.data() as Practitioner;
1619
1482
  } catch (error) {
1620
- Logger.error(
1621
- `[AggService] Error fetching practitioner profile ${practitionerId}:`,
1622
- error
1623
- );
1483
+ Logger.error(`[AggService] Error fetching practitioner profile ${practitionerId}:`, error);
1624
1484
  return null;
1625
1485
  }
1626
1486
  }
@@ -1632,25 +1492,167 @@ export class AppointmentAggregationService {
1632
1492
  */
1633
1493
  private async fetchClinicInfo(clinicId: string): Promise<Clinic | null> {
1634
1494
  if (!clinicId) {
1635
- Logger.warn("[AggService] fetchClinicInfo called with no clinicId.");
1495
+ Logger.warn('[AggService] fetchClinicInfo called with no clinicId.');
1636
1496
  return null;
1637
1497
  }
1638
1498
  try {
1639
- const doc = await this.db
1640
- .collection(CLINICS_COLLECTION)
1641
- .doc(clinicId)
1642
- .get();
1499
+ const doc = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
1643
1500
  if (!doc.exists) {
1644
1501
  Logger.warn(`[AggService] No clinic info found for ID ${clinicId}`);
1645
1502
  return null;
1646
1503
  }
1647
1504
  return doc.data() as Clinic;
1505
+ } catch (error) {
1506
+ Logger.error(`[AggService] Error fetching clinic info ${clinicId}:`, error);
1507
+ return null;
1508
+ }
1509
+ }
1510
+
1511
+ /**
1512
+ * Checks if zone photos have changed between two appointment states
1513
+ * @param before - The appointment state before update
1514
+ * @param after - The appointment state after update
1515
+ * @returns True if zone photos have changed, false otherwise
1516
+ */
1517
+ private hasZonePhotosChanged(before: Appointment, after: Appointment): boolean {
1518
+ const beforePhotos = before.metadata?.zonePhotos;
1519
+ const afterPhotos = after.metadata?.zonePhotos;
1520
+
1521
+ // If both are null/undefined, no change
1522
+ if (!beforePhotos && !afterPhotos) {
1523
+ return false;
1524
+ }
1525
+
1526
+ // If one is null and the other isn't, there's a change
1527
+ if (!beforePhotos || !afterPhotos) {
1528
+ return true;
1529
+ }
1530
+
1531
+ // Compare the number of zones
1532
+ const beforeZones = Object.keys(beforePhotos);
1533
+ const afterZones = Object.keys(afterPhotos);
1534
+
1535
+ if (beforeZones.length !== afterZones.length) {
1536
+ return true;
1537
+ }
1538
+
1539
+ // Compare each zone's photos
1540
+ for (const zoneId of afterZones) {
1541
+ const beforeZonePhotos = beforePhotos[zoneId];
1542
+ const afterZonePhotos = afterPhotos[zoneId];
1543
+
1544
+ if (!beforeZonePhotos && !afterZonePhotos) {
1545
+ continue;
1546
+ }
1547
+
1548
+ if (!beforeZonePhotos || !afterZonePhotos) {
1549
+ return true;
1550
+ }
1551
+
1552
+ // Compare before and after photos and notes
1553
+ if (
1554
+ beforeZonePhotos.before !== afterZonePhotos.before ||
1555
+ beforeZonePhotos.after !== afterZonePhotos.after ||
1556
+ beforeZonePhotos.beforeNote !== afterZonePhotos.beforeNote ||
1557
+ beforeZonePhotos.afterNote !== afterZonePhotos.afterNote
1558
+ ) {
1559
+ return true;
1560
+ }
1561
+ }
1562
+
1563
+ return false;
1564
+ }
1565
+
1566
+ /**
1567
+ * Handles zone photos update notifications and logging
1568
+ * @param before - The appointment state before update
1569
+ * @param after - The appointment state after update
1570
+ */
1571
+ private async handleZonePhotosUpdate(before: Appointment, after: Appointment): Promise<void> {
1572
+ try {
1573
+ Logger.info(`[AggService] Processing zone photos update for appointment ${after.id}`);
1574
+
1575
+ const beforePhotos = before.metadata?.zonePhotos || {};
1576
+ const afterPhotos = after.metadata?.zonePhotos || {};
1577
+
1578
+ // Find zones with new or updated photos
1579
+ const updatedZones: string[] = [];
1580
+ const newPhotoTypes: { zoneId: string; photoType: 'before' | 'after' }[] = [];
1581
+
1582
+ for (const zoneId of Object.keys(afterPhotos)) {
1583
+ const beforeZonePhotos = beforePhotos[zoneId];
1584
+ const afterZonePhotos = afterPhotos[zoneId];
1585
+
1586
+ if (!beforeZonePhotos) {
1587
+ // New zone with photos
1588
+ updatedZones.push(zoneId);
1589
+ if (afterZonePhotos.before) {
1590
+ newPhotoTypes.push({ zoneId, photoType: 'before' });
1591
+ }
1592
+ if (afterZonePhotos.after) {
1593
+ newPhotoTypes.push({ zoneId, photoType: 'after' });
1594
+ }
1595
+ } else {
1596
+ // Check for new or updated photos
1597
+ if (beforeZonePhotos.before !== afterZonePhotos.before && afterZonePhotos.before) {
1598
+ updatedZones.push(zoneId);
1599
+ newPhotoTypes.push({ zoneId, photoType: 'before' });
1600
+ }
1601
+ if (beforeZonePhotos.after !== afterZonePhotos.after && afterZonePhotos.after) {
1602
+ updatedZones.push(zoneId);
1603
+ newPhotoTypes.push({ zoneId, photoType: 'after' });
1604
+ }
1605
+ }
1606
+ }
1607
+
1608
+ if (updatedZones.length > 0) {
1609
+ Logger.info(
1610
+ `[AggService] Zone photos updated for appointment ${after.id}: ${updatedZones.join(
1611
+ ', ',
1612
+ )}`,
1613
+ );
1614
+
1615
+ // Log specific photo types that were added
1616
+ for (const { zoneId, photoType } of newPhotoTypes) {
1617
+ Logger.info(
1618
+ `[AggService] New ${photoType} photo added for zone ${zoneId} in appointment ${after.id}`,
1619
+ );
1620
+ }
1621
+
1622
+ // TODO: Add notifications to practitioners/clinic admins about photo updates
1623
+ // TODO: Add audit logging for photo uploads
1624
+ // TODO: Trigger any business logic related to photo completion (e.g., appointment progress tracking)
1625
+ }
1626
+
1627
+ // Check if all required photos are now complete
1628
+ const selectedZones = after.metadata?.selectedZones || [];
1629
+ if (selectedZones.length > 0) {
1630
+ const completedZones = selectedZones.filter(zoneId => {
1631
+ const zonePhotos = afterPhotos[zoneId];
1632
+ return zonePhotos && (zonePhotos.before || zonePhotos.after);
1633
+ });
1634
+
1635
+ const completionPercentage = (completedZones.length / selectedZones.length) * 100;
1636
+ Logger.info(
1637
+ `[AggService] Photo completion for appointment ${
1638
+ after.id
1639
+ }: ${completionPercentage.toFixed(1)}% (${completedZones.length}/${
1640
+ selectedZones.length
1641
+ } zones)`,
1642
+ );
1643
+
1644
+ // TODO: Trigger notifications when all photos are complete
1645
+ if (completionPercentage === 100) {
1646
+ Logger.info(`[AggService] All zone photos completed for appointment ${after.id}`);
1647
+ // TODO: Send notification to relevant parties
1648
+ }
1649
+ }
1648
1650
  } catch (error) {
1649
1651
  Logger.error(
1650
- `[AggService] Error fetching clinic info ${clinicId}:`,
1651
- error
1652
+ `[AggService] Error handling zone photos update for appointment ${after.id}:`,
1653
+ error,
1652
1654
  );
1653
- return null;
1655
+ // Don't throw - this is a side effect and shouldn't break the main update flow
1654
1656
  }
1655
1657
  }
1656
1658
  }