@blackcode_sa/metaestetics-api 1.6.5 → 1.6.6

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.
@@ -2,29 +2,43 @@ import * as admin from "firebase-admin";
2
2
  import {
3
3
  Appointment,
4
4
  AppointmentStatus,
5
- APPOINTMENTS_COLLECTION,
6
- } from "../../../types/appointment"; // Assuming APPOINTMENTS_COLLECTION is exported here
5
+ // APPOINTMENTS_COLLECTION, // Not directly used in this file after refactor
6
+ } from "../../../types/appointment";
7
7
  import {
8
8
  PatientRequirementInstance,
9
9
  PatientRequirementOverallStatus,
10
10
  PatientInstructionStatus,
11
11
  PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME,
12
+ PatientRequirementInstruction, // Added import
12
13
  } from "../../../types/patient/patient-requirements";
13
14
  import {
14
15
  Requirement as RequirementTemplate,
15
- REQUIREMENTS_COLLECTION as REQUIREMENTS_TEMPLATES_COLLECTION,
16
- RequirementType as RequirementTemplateType,
16
+ // REQUIREMENTS_COLLECTION as REQUIREMENTS_TEMPLATES_COLLECTION, // Not used directly after refactor
17
+ RequirementType,
18
+ TimeUnit, // Added import
17
19
  } from "../../../backoffice/types/requirement.types";
18
- import { PATIENTS_COLLECTION } from "../../../types/patient";
20
+ import {
21
+ PATIENTS_COLLECTION,
22
+ PatientProfile,
23
+ PatientSensitiveInfo,
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";
31
+ // import { UserRole } from "../../../types"; // Not directly used
19
32
 
20
33
  // Dependent Admin Services
21
34
  import { PatientRequirementsAdminService } from "../../requirements/patient-requirements.admin.service";
22
35
  import { NotificationsAdmin } from "../../notifications/notifications.admin";
23
- // import { CalendarAdminService } from "../../calendar/calendar.admin.service"; // Placeholder - To be created
24
- // import { AppointmentMailingService } from "../mailing/appointment/appointment.mailing.service"; // Placeholder - To be created
25
- // import { PatientAdminService } from "../patient/patient.admin.service"; // Placeholder for specific patient admin actions
26
- // import { PractitionerAdminService } from "../practitioner/practitioner.admin.service"; // Placeholder for specific practitioner admin actions
27
- // import { ClinicAdminService } from "../clinic/clinic.admin.service"; // Placeholder for specific clinic admin actions
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
+
41
+ // Mailgun client will be injected via constructor
28
42
 
29
43
  /**
30
44
  * @class AppointmentAggregationService
@@ -34,288 +48,1006 @@ import { NotificationsAdmin } from "../../notifications/notifications.admin";
34
48
  */
35
49
  export class AppointmentAggregationService {
36
50
  private db: admin.firestore.Firestore;
37
- private patientRequirementsAdminService: PatientRequirementsAdminService;
51
+ private appointmentMailingService: AppointmentMailingService;
38
52
  private notificationsAdmin: NotificationsAdmin;
39
- // private calendarAdminService: CalendarAdminService; // Placeholder
40
- // private appointmentMailingService: AppointmentMailingService; // Placeholder
41
- // private patientAdminService: PatientAdminService; // Placeholder
42
- // private practitionerAdminService: PractitionerAdminService; // Placeholder
43
- // private clinicAdminService: ClinicAdminService; // Placeholder
53
+ private calendarAdminService: CalendarAdminService;
54
+ private patientRequirementsAdminService: PatientRequirementsAdminService;
44
55
 
45
56
  /**
46
57
  * Constructor for AppointmentAggregationService.
58
+ * @param mailgunClient - An initialized Mailgun client instance.
47
59
  * @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
48
60
  */
49
- constructor(firestore?: admin.firestore.Firestore) {
61
+ constructor(
62
+ mailgunClient: any, // Type as 'any' for now, to be provided by the calling Cloud Function
63
+ firestore?: admin.firestore.Firestore
64
+ ) {
50
65
  this.db = firestore || admin.firestore();
66
+ this.appointmentMailingService = new AppointmentMailingService(
67
+ this.db,
68
+ mailgunClient // Pass the injected client
69
+ );
70
+ this.notificationsAdmin = new NotificationsAdmin(this.db);
71
+ this.calendarAdminService = new CalendarAdminService(this.db);
51
72
  this.patientRequirementsAdminService = new PatientRequirementsAdminService(
52
73
  this.db
53
74
  );
54
- this.notificationsAdmin = new NotificationsAdmin(this.db);
55
- // this.calendarAdminService = new CalendarAdminService(this.db); // Placeholder
56
- // this.appointmentMailingService = new AppointmentMailingService(this.db /*, mailgunClient */); // Placeholder
57
- // this.patientAdminService = new PatientAdminService(this.db); // Placeholder
58
- // this.practitionerAdminService = new PractitionerAdminService(this.db); // Placeholder
59
- // this.clinicAdminService = new ClinicAdminService(this.db); // Placeholder
75
+ Logger.info("[AppointmentAggregationService] Initialized.");
60
76
  }
61
77
 
62
78
  /**
63
- * Handles side effects when an appointment is confirmed.
64
- * - Creates PRE-appointment PatientRequirementInstances.
65
- * - Triggers calendar event creation.
66
- * - Triggers email/push notifications.
67
- * - Manages patient-clinic-practitioner links.
68
- * @param appointment - The confirmed Appointment object.
79
+ * Handles side effects when an appointment is first created.
80
+ * This function would typically be called by an Firestore onCreate trigger.
81
+ * @param {Appointment} appointment - The newly created Appointment object.
69
82
  * @returns {Promise<void>}
70
83
  */
71
- async handleAppointmentConfirmed(appointment: Appointment): Promise<void> {
72
- console.log(
73
- `[AppointmentAggregationService] Handling confirmed appointment: ${appointment.id}`
84
+ async handleAppointmentCreate(appointment: Appointment): Promise<void> {
85
+ Logger.info(
86
+ `[AggService] Handling CREATE for appointment: ${appointment.id}, patient: ${appointment.patientId}, status: ${appointment.status}`
74
87
  );
75
- // 1. Create PRE-appointment PatientRequirementInstances
76
- // - Fetch RequirementTemplates for procedureId
77
- // - Construct and save PatientRequirementInstance documents
78
- // - Rely on PatientRequirementInstance triggers for notification scheduling
79
88
 
80
- // 2. Trigger calendar event creation (e.g., this.calendarAdminService.createEventForAppointment(appointment))
89
+ try {
90
+ // 1. Manage Patient-Clinic-Practitioner Links (non-critical for core flow, can run in parallel or first)
91
+ // No need to await if other operations don't depend on its immediate completion, but awaiting for simplicity here.
92
+ await this.managePatientClinicPractitionerLinks(appointment, "create");
81
93
 
82
- // 3. Trigger email/push notifications (e.g., this.appointmentMailingService.sendAppointmentConfirmedEmail(appointment))
94
+ // 2. Fetch necessary profiles for notifications and context
95
+ // These can be fetched in parallel
96
+ const [
97
+ patientProfile,
98
+ patientSensitiveInfo,
99
+ practitionerProfile,
100
+ clinicInfo,
101
+ ] = await Promise.all([
102
+ this.fetchPatientProfile(appointment.patientId),
103
+ this.fetchPatientSensitiveInfo(appointment.patientId),
104
+ this.fetchPractitionerProfile(appointment.practitionerId), // Needed for practitioner notifications
105
+ this.fetchClinicInfo(appointment.clinicBranchId), // Needed for clinic admin notifications
106
+ ]);
83
107
 
84
- // 4. Manage patient-clinic-practitioner links (e.g., this.managePatientClinicPractitionerLinks(appointment, 'create'))
108
+ // 3. Initial State Handling based on appointment status
109
+ if (appointment.status === AppointmentStatus.CONFIRMED) {
110
+ Logger.info(
111
+ `[AggService] Appt ${appointment.id} created as CONFIRMED.`
112
+ );
113
+ // Create pre-appointment requirements for confirmed appointments
114
+ await this.createPreAppointmentRequirementInstances(appointment);
85
115
 
86
- // Example: Placeholder for creating pre-requirements
87
- // const requirementTemplates = await this.fetchRequirementTemplates(appointment.procedureId, 'PRE');
88
- // for (const template of requirementTemplates) {
89
- // await this.createPatientRequirementInstance(appointment, template, 'PRE');
90
- // }
91
- }
116
+ // Send confirmation notifications
117
+ if (patientSensitiveInfo?.email && patientProfile) {
118
+ Logger.info(
119
+ `[AggService] Sending appointment confirmed email to patient ${patientSensitiveInfo.email}`
120
+ );
121
+ // Construct the data object for the mailing service
122
+ const emailData = {
123
+ appointment: appointment,
124
+ recipientProfile: appointment.patientInfo,
125
+ recipientRole: "patient" as const, // Use 'as const' for literal type
126
+ };
127
+ // The type cast here might still be an issue if PatientProfileInfo is not imported.
128
+ // However, the structure should be compatible enough for the call.
129
+ await this.appointmentMailingService.sendAppointmentConfirmedEmail(
130
+ emailData as any // Using 'as any' temporarily to bypass strict type checking if PatientProfileInfo is not imported
131
+ // TODO: Properly import PatientProfileInfo and ensure type compatibility
132
+ );
133
+ } else {
134
+ Logger.warn(
135
+ `[AggService] Cannot send confirmation email to patient ${appointment.patientId}: email missing.`
136
+ );
137
+ }
92
138
 
93
- /**
94
- * Handles side effects when an appointment is completed.
95
- * - Creates POST-appointment PatientRequirementInstances.
96
- * - Triggers review request notifications.
97
- * @param appointment - The completed Appointment object.
98
- * @returns {Promise<void>}
99
- */
100
- async handleAppointmentCompleted(appointment: Appointment): Promise<void> {
101
- console.log(
102
- `[AppointmentAggregationService] Handling completed appointment: ${appointment.id}`
103
- );
104
- // 1. Create POST-appointment PatientRequirementInstances
105
- // - Fetch RequirementTemplates for procedureId
106
- // - Construct and save PatientRequirementInstance documents
139
+ if (
140
+ patientProfile?.expoTokens &&
141
+ patientProfile.expoTokens.length > 0
142
+ ) {
143
+ Logger.info(
144
+ `[AggService] TODO: Send appointment confirmed push to patient ${appointment.patientId}`
145
+ );
146
+ await this.notificationsAdmin.sendAppointmentConfirmedPush(
147
+ appointment,
148
+ appointment.patientId,
149
+ patientProfile.expoTokens,
150
+ UserRole.PATIENT
151
+ );
152
+ }
153
+
154
+ if (practitionerProfile?.basicInfo?.email) {
155
+ Logger.info(
156
+ `[AggService] Sending appointment confirmation email to practitioner ${practitionerProfile.basicInfo.email}`
157
+ );
158
+ const practitionerEmailData = {
159
+ appointment: appointment,
160
+ recipientProfile: appointment.practitionerInfo,
161
+ recipientRole: "practitioner" as const,
162
+ };
163
+ await this.appointmentMailingService.sendAppointmentConfirmedEmail(
164
+ practitionerEmailData // TODO: Properly import PractitionerProfileInfo and ensure type compatibility
165
+ );
166
+ }
167
+ // TODO: Add push notification for practitioner if they have expoTokens
168
+ } else if (appointment.status === AppointmentStatus.PENDING) {
169
+ Logger.info(`[AggService] Appt ${appointment.id} created as PENDING.`);
170
+ // Notify clinic admin about the pending appointment
171
+ if (clinicInfo?.contactInfo?.email) {
172
+ Logger.info(
173
+ `[AggService] TODO: Send pending appointment notification email to clinic admin ${clinicInfo.contactInfo.email}`
174
+ );
175
+ const clinicEmailData = {
176
+ appointment: appointment,
177
+ clinicProfile: appointment.clinicInfo, // clinicInfo should be compatible with ClinicInfo type
178
+ };
179
+ await this.appointmentMailingService.sendAppointmentRequestedEmailToClinic(
180
+ clinicEmailData // TODO: Properly import ClinicInfo if stricter typing is needed here and ensure compatibility
181
+ );
182
+ } else {
183
+ Logger.warn(
184
+ `[AggService] Cannot send pending appointment email to clinic ${appointment.clinicBranchId}: email missing.`
185
+ );
186
+ }
187
+ // TODO: Push notification for clinic admin if applicable (they usually don't have tokens)
188
+ }
107
189
 
108
- // 2. Trigger review request (e.g., this.appointmentMailingService.sendReviewRequestEmail(appointment))
190
+ // Calendar events are noted as being handled by BookingAdmin.orchestrateAppointmentCreation during the booking process itself.
191
+ Logger.info(
192
+ `[AggService] Successfully processed CREATE for appointment: ${appointment.id}`
193
+ );
194
+ } catch (error) {
195
+ Logger.error(
196
+ `[AggService] Critical error in handleAppointmentCreate for appointment ${appointment.id}:`,
197
+ error
198
+ );
199
+ // Depending on the error, you might want to re-throw or handle specific cases
200
+ // (e.g., update appointment status to an error state if a critical part failed)
201
+ }
109
202
  }
110
203
 
111
204
  /**
112
- * Handles side effects when an appointment is cancelled.
113
- * - Updates overallStatus of related PatientRequirementInstances to CANCELLED_APPOINTMENT.
114
- * - Triggers calendar event cancellation/update.
115
- * - Triggers email/push notifications for cancellation.
116
- * - Evaluates patient-clinic-practitioner links.
117
- * @param appointment - The cancelled Appointment object.
118
- * @param previousStatus - The status of the appointment before cancellation.
205
+ * Handles side effects when an appointment is updated.
206
+ * This function would typically be called by an Firestore onUpdate trigger.
207
+ * @param {Appointment} before - The Appointment object before the update.
208
+ * @param {Appointment} after - The Appointment object after the update.
119
209
  * @returns {Promise<void>}
120
210
  */
121
- async handleAppointmentCancelled(
122
- appointment: Appointment,
123
- previousStatus?: AppointmentStatus
211
+ async handleAppointmentUpdate(
212
+ before: Appointment,
213
+ after: Appointment
124
214
  ): Promise<void> {
125
- console.log(
126
- `[AppointmentAggregationService] Handling cancelled appointment: ${appointment.id}, previous status: ${previousStatus}`
215
+ Logger.info(
216
+ `[AggService] Handling UPDATE for appointment: ${after.id}. Status ${before.status} -> ${after.status}`
127
217
  );
128
- // 1. Update related PatientRequirementInstances' overallStatus
129
- // - Query instances by appointment.id
130
- // - Update overallStatus to PatientRequirementOverallStatus.CANCELLED_APPOINTMENT
131
- // - PatientRequirementsAdminService (via trigger) should handle notification cancellations.
132
218
 
133
- // 2. Trigger calendar event cancellation (e.g., this.calendarAdminService.cancelEventForAppointment(appointment))
219
+ try {
220
+ const statusChanged = before.status !== after.status;
221
+ const timeChanged =
222
+ before.appointmentStartTime.toMillis() !==
223
+ after.appointmentStartTime.toMillis() ||
224
+ before.appointmentEndTime.toMillis() !==
225
+ after.appointmentEndTime.toMillis();
226
+ // const paymentStatusChanged = before.paymentStatus !== after.paymentStatus; // TODO: Handle later
227
+ // const reviewAdded = !before.reviewInfo && after.reviewInfo; // TODO: Handle later
228
+
229
+ // Fetch profiles for notifications - could be conditional based on changes
230
+ // For simplicity, fetching upfront, but optimize if performance is an issue.
231
+ const [
232
+ patientProfile,
233
+ patientSensitiveInfo,
234
+ practitionerProfile,
235
+ clinicInfo,
236
+ ] = await Promise.all([
237
+ this.fetchPatientProfile(after.patientId),
238
+ this.fetchPatientSensitiveInfo(after.patientId),
239
+ this.fetchPractitionerProfile(after.practitionerId),
240
+ this.fetchClinicInfo(after.clinicBranchId),
241
+ ]);
242
+
243
+ if (statusChanged) {
244
+ Logger.info(
245
+ `[AggService] Status changed for ${after.id}: ${before.status} -> ${after.status}`
246
+ );
247
+
248
+ // --- PENDING -> CONFIRMED ---
249
+ if (
250
+ before.status === AppointmentStatus.PENDING &&
251
+ after.status === AppointmentStatus.CONFIRMED
252
+ ) {
253
+ Logger.info(`[AggService] Appt ${after.id} PENDING -> CONFIRMED.`);
254
+ await this.createPreAppointmentRequirementInstances(after);
255
+ // Send confirmation notifications
256
+ if (patientSensitiveInfo?.email && patientProfile) {
257
+ Logger.info(
258
+ `[AggService] Sending appointment confirmed email to patient ${patientSensitiveInfo.email}`
259
+ );
260
+ const emailData = {
261
+ appointment: after,
262
+ recipientProfile: after.patientInfo,
263
+ recipientRole: "patient" as const,
264
+ };
265
+ await this.appointmentMailingService.sendAppointmentConfirmedEmail(
266
+ emailData as any
267
+ );
268
+ } else {
269
+ Logger.warn(
270
+ `[AggService] Cannot send confirmation email to patient ${after.patientId}: email missing.`
271
+ );
272
+ }
273
+
274
+ if (
275
+ patientProfile?.expoTokens &&
276
+ patientProfile.expoTokens.length > 0
277
+ ) {
278
+ Logger.info(
279
+ `[AggService] TODO: Send appointment confirmed push to patient ${after.patientId}`
280
+ );
281
+ await this.notificationsAdmin.sendAppointmentConfirmedPush(
282
+ after,
283
+ after.patientId,
284
+ patientProfile.expoTokens,
285
+ UserRole.PATIENT
286
+ );
287
+ }
288
+
289
+ if (practitionerProfile?.basicInfo?.email) {
290
+ Logger.info(
291
+ `[AggService] Sending appointment confirmation email to practitioner ${practitionerProfile.basicInfo.email}`
292
+ );
293
+ const practitionerEmailData = {
294
+ appointment: after,
295
+ recipientProfile: after.practitionerInfo,
296
+ recipientRole: "practitioner" as const,
297
+ };
298
+ await this.appointmentMailingService.sendAppointmentConfirmedEmail(
299
+ practitionerEmailData as any // TODO: Properly import PractitionerProfileInfo and ensure type compatibility
300
+ );
301
+ }
302
+ // TODO: Add push notification for practitioner if they have expoTokens
303
+ }
304
+ // --- Any -> CANCELLED_* ---
305
+ else if (
306
+ after.status === AppointmentStatus.CANCELED_CLINIC ||
307
+ after.status === AppointmentStatus.CANCELED_PATIENT ||
308
+ after.status === AppointmentStatus.CANCELED_PATIENT_RESCHEDULED ||
309
+ after.status === AppointmentStatus.NO_SHOW
310
+ ) {
311
+ Logger.info(
312
+ `[AggService] Appt ${after.id} status -> ${after.status}. Processing as cancellation.`
313
+ );
314
+ await this.updateRelatedPatientRequirementInstances(
315
+ after,
316
+ PatientRequirementOverallStatus.CANCELLED_APPOINTMENT
317
+ );
318
+ await this.managePatientClinicPractitionerLinks(after, "cancel");
319
+
320
+ // Send cancellation email to Patient
321
+ if (patientSensitiveInfo?.email && patientProfile) {
322
+ Logger.info(
323
+ `[AggService] Sending appointment cancellation email to patient ${patientSensitiveInfo.email}`
324
+ );
325
+ const patientCancellationData = {
326
+ appointment: after,
327
+ recipientProfile: after.patientInfo,
328
+ recipientRole: "patient" as const,
329
+ // cancellationReason: after.cancellationReason, // TODO: Add if cancellationReason is available on 'after' Appointment
330
+ };
331
+ await this.appointmentMailingService.sendAppointmentCancelledEmail(
332
+ patientCancellationData as any // TODO: Properly import types
333
+ );
334
+ }
335
+
336
+ // Send cancellation email to Practitioner
337
+ if (practitionerProfile?.basicInfo?.email) {
338
+ Logger.info(
339
+ `[AggService] Sending appointment cancellation email to practitioner ${practitionerProfile.basicInfo.email}`
340
+ );
341
+ const practitionerCancellationData = {
342
+ appointment: after,
343
+ recipientProfile: after.practitionerInfo,
344
+ recipientRole: "practitioner" as const,
345
+ // cancellationReason: after.cancellationReason, // TODO: Add if cancellationReason is available on 'after' Appointment
346
+ };
347
+ await this.appointmentMailingService.sendAppointmentCancelledEmail(
348
+ practitionerCancellationData as any // TODO: Properly import types
349
+ );
350
+ }
134
351
 
135
- // 3. Trigger email notifications (e.g., this.appointmentMailingService.sendAppointmentCancelledEmail(appointment))
352
+ // TODO: Send cancellation push notifications (patient, practitioner) via notificationsAdmin
353
+ // TODO: Update/cancel calendar event via calendarAdminService.updateAppointmentCalendarEventStatus(after, CalendarEventStatus.CANCELED)
354
+ }
355
+ // --- Any -> COMPLETED ---
356
+ else if (after.status === AppointmentStatus.COMPLETED) {
357
+ Logger.info(`[AggService] Appt ${after.id} status -> COMPLETED.`);
358
+ await this.createPostAppointmentRequirementInstances(after);
136
359
 
137
- // 4. Evaluate patient-clinic-practitioner links for removal
360
+ // Send review request email to patient
361
+ if (patientSensitiveInfo?.email && patientProfile) {
362
+ Logger.info(
363
+ `[AggService] Sending review request email to patient ${patientSensitiveInfo.email}`
364
+ );
365
+ const reviewRequestData = {
366
+ appointment: after,
367
+ patientProfile: after.patientInfo,
368
+ reviewLink: "TODO: Generate actual review link", // Placeholder
369
+ };
370
+ await this.appointmentMailingService.sendReviewRequestEmail(
371
+ reviewRequestData as any // TODO: Properly import PatientProfileInfo and define reviewLink generation
372
+ );
373
+ }
374
+ // TODO: Send review request push notification to patient
375
+ }
376
+ // --- RESCHEDULE Scenarios (e.g., PENDING/CONFIRMED -> RESCHEDULED_BY_CLINIC) ---
377
+ else if (after.status === AppointmentStatus.RESCHEDULED_BY_CLINIC) {
378
+ Logger.info(
379
+ `[AggService] Appt ${after.id} status -> RESCHEDULED_BY_CLINIC.`
380
+ );
381
+ await this.updateRelatedPatientRequirementInstances(
382
+ before, // Pass the 'before' state for old requirements
383
+ PatientRequirementOverallStatus.SUPERSEDED_RESCHEDULE
384
+ );
385
+ Logger.info(
386
+ `[AggService] TODO: Handle RESCHEDULE logic for requirements carefully based on confirmation flow.`
387
+ );
388
+
389
+ // Send reschedule proposal email to patient
390
+ if (patientSensitiveInfo?.email && patientProfile) {
391
+ Logger.info(
392
+ `[AggService] Sending reschedule proposal email to patient ${patientSensitiveInfo.email}`
393
+ );
394
+ const rescheduleEmailData = {
395
+ appointment: after, // The new state of the appointment
396
+ patientProfile: after.patientInfo,
397
+ previousStartTime: before.appointmentStartTime,
398
+ previousEndTime: before.appointmentEndTime,
399
+ };
400
+ await this.appointmentMailingService.sendAppointmentRescheduledProposalEmail(
401
+ rescheduleEmailData as any // TODO: Properly import PatientProfileInfo and types
402
+ );
403
+ }
404
+
405
+ Logger.info(
406
+ `[AggService] TODO: Send reschedule proposal notifications to practitioner as well.`
407
+ );
408
+ // TODO: Update calendar event to reflect proposed new time via calendarAdminService.
409
+ }
410
+ // TODO: Add more specific status change handlers as needed
411
+ }
412
+
413
+ // --- Independent Time Change (if not tied to a status that already handled it) ---
414
+ if (timeChanged && !statusChanged) {
415
+ // Or if status change didn't fully cover reschedule implications
416
+ Logger.info(`[AggService] Appointment ${after.id} time changed.`);
417
+ // This scenario is tricky. If time changes, pre-requirements might need to be re-evaluated.
418
+ // Typically, a time change would also involve a status change to some form of "rescheduled".
419
+ // If it can happen independently while, e.g., staying CONFIRMED, then:
420
+ // await this.updateRelatedPatientRequirementInstances(before, PatientRequirementOverallStatus.SUPERSEDED_RESCHEDULE);
421
+ // await this.createPreAppointmentRequirementInstances(after);
422
+ // TODO: Send reschedule notifications
423
+ // TODO: Update calendar event via calendarAdminService
424
+ Logger.warn(
425
+ `[AggService] Independent time change detected for ${after.id}. Review implications for requirements and calendar.`
426
+ );
427
+ }
428
+
429
+ // TODO: Handle Payment Status Change
430
+ // const paymentStatusChanged = before.paymentStatus !== after.paymentStatus;
431
+ // if (paymentStatusChanged && after.paymentStatus === PaymentStatus.PAID) { ... }
432
+
433
+ // TODO: Handle Review Added
434
+ // const reviewAdded = !before.reviewInfo && after.reviewInfo;
435
+ // if (reviewAdded) { ... }
436
+
437
+ Logger.info(
438
+ `[AggService] Successfully processed UPDATE for appointment: ${after.id}`
439
+ );
440
+ } catch (error) {
441
+ Logger.error(
442
+ `[AggService] Critical error in handleAppointmentUpdate for appointment ${after.id}:`,
443
+ error
444
+ );
445
+ }
138
446
  }
139
447
 
140
448
  /**
141
- * Handles side effects when an appointment is rescheduled.
142
- * - Updates/recreates PRE-appointment PatientRequirementInstances based on new times.
143
- * - Triggers calendar event updates.
144
- * - Triggers email/push notifications for rescheduling.
145
- * @param appointment - The rescheduled Appointment object with new times.
146
- * @param previousAppointmentData - The appointment data before rescheduling.
449
+ * Handles side effects when an appointment is deleted.
450
+ * @param deletedAppointment - The Appointment object that was deleted.
147
451
  * @returns {Promise<void>}
148
452
  */
149
- async handleAppointmentRescheduled(
150
- appointment: Appointment,
151
- previousAppointmentData: Appointment
453
+ async handleAppointmentDelete(
454
+ deletedAppointment: Appointment
152
455
  ): Promise<void> {
153
- console.log(
154
- `[AppointmentAggregationService] Handling rescheduled appointment: ${appointment.id}`
456
+ Logger.info(
457
+ `[AggService] Handling DELETE for appointment: ${deletedAppointment.id}`
155
458
  );
156
- // 1. Manage PatientRequirementInstances for PRE-requirements:
157
- // - Mark old instances as SUPERSEDED_RESCHEDULE.
158
- // - Create new instances for the new time OR update existing ones (more complex).
159
- // - PatientRequirementsAdminService (via trigger) will manage notification updates.
160
-
161
- // 2. Trigger calendar event update (e.g., this.calendarAdminService.updateEventForAppointment(appointment, previousAppointmentData))
162
-
163
- // 3. Trigger email notifications for reschedule (e.g., this.appointmentMailingService.sendAppointmentRescheduledEmail(appointment))
459
+ // Similar to cancellation
460
+ await this.updateRelatedPatientRequirementInstances(
461
+ deletedAppointment,
462
+ PatientRequirementOverallStatus.CANCELLED_APPOINTMENT
463
+ );
464
+ // await this.calendarAdminService.deleteAppointmentCalendarEvents(deletedAppointment);
465
+ await this.managePatientClinicPractitionerLinks(
466
+ deletedAppointment,
467
+ "cancel"
468
+ );
469
+ // TODO: Send cancellation/deletion notifications if appropriate (though data is gone)
164
470
  }
165
471
 
472
+ // --- Helper Methods for Aggregation Logic ---
473
+
166
474
  /**
167
- * Manages the links between a patient and the clinic/practitioner associated with an appointment.
168
- * Adds links on creation, potentially removes them on cancellation if it's the only link.
169
- * @param appointment - The appointment object.
170
- * @param action - 'create' to add links, 'cancel' to evaluate removal.
171
- * @returns {Promise<void>}
475
+ * Creates PRE_APPOINTMENT PatientRequirementInstance documents for a given appointment.
476
+ * Uses the `appointment.preProcedureRequirements` array, which should contain relevant Requirement templates.
477
+ * For each active PRE requirement template, it constructs a new PatientRequirementInstance document
478
+ * with derived instructions and batch writes them to Firestore under the patient's `patient_requirements` subcollection.
479
+ *
480
+ * @param {Appointment} appointment - The appointment for which to create pre-requirement instances.
481
+ * @returns {Promise<void>} A promise that resolves when the operation is complete.
172
482
  */
173
- async managePatientClinicPractitionerLinks(
174
- appointment: Appointment,
175
- action: "create" | "cancel"
483
+ private async createPreAppointmentRequirementInstances(
484
+ appointment: Appointment
176
485
  ): Promise<void> {
177
- console.log(
178
- `[AppointmentAggregationService] Managing patient-clinic-practitioner links for appointment ${appointment.id}, action: ${action}`
486
+ Logger.info(
487
+ `[AggService] Creating PRE-appointment requirement instances for appt: ${appointment.id}, patient: ${appointment.patientId}`
179
488
  );
180
- // Logic to add patientId to clinic.patientIds / practitioner.patientIds if 'create'
181
- // Logic to check and potentially remove if 'cancel' and no other appointments link them.
182
- // This would likely involve calling methods on a PatientAdminService, ClinicAdminService, PractitionerAdminService
489
+
490
+ if (!appointment.procedureId) {
491
+ Logger.warn(
492
+ `[AggService] Appointment ${appointment.id} has no procedureId. Cannot create pre-requirement instances.`
493
+ );
494
+ return;
495
+ }
496
+
497
+ if (
498
+ !appointment.preProcedureRequirements ||
499
+ appointment.preProcedureRequirements.length === 0
500
+ ) {
501
+ Logger.info(
502
+ `[AggService] No preProcedureRequirements found on appointment ${appointment.id}. Nothing to create.`
503
+ );
504
+ return;
505
+ }
506
+
507
+ try {
508
+ const batch = this.db.batch();
509
+ let instancesCreatedCount = 0;
510
+
511
+ for (const template of appointment.preProcedureRequirements) {
512
+ if (!template) continue; // Skip if template is null or undefined in the array
513
+
514
+ // Ensure it's an active, PRE-type requirement
515
+ if (template.type !== RequirementType.PRE || !template.isActive) {
516
+ Logger.debug(
517
+ `[AggService] Skipping template ${template.id} (${template.name}): not an active PRE requirement.`
518
+ );
519
+ continue;
520
+ }
521
+
522
+ Logger.debug(
523
+ `[AggService] Processing template ${template.id} (${template.name}) for appt ${appointment.id}`
524
+ );
525
+
526
+ const newInstanceRef = this.db
527
+ .collection(PATIENTS_COLLECTION)
528
+ .doc(appointment.patientId)
529
+ .collection(PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME)
530
+ .doc(); // Auto-generate ID for the new instance
531
+
532
+ const instructions: PatientRequirementInstruction[] = (
533
+ template.timeframe?.notifyAt || []
534
+ ).map((notifyAtValue) => {
535
+ let dueTime: any = appointment.appointmentStartTime;
536
+ if (template.timeframe && typeof notifyAtValue === "number") {
537
+ const dueDateTime = new Date(
538
+ appointment.appointmentStartTime.toMillis()
539
+ );
540
+ if (template.timeframe.unit === TimeUnit.DAYS) {
541
+ dueDateTime.setDate(dueDateTime.getDate() - notifyAtValue);
542
+ } else if (template.timeframe.unit === TimeUnit.HOURS) {
543
+ dueDateTime.setHours(dueDateTime.getHours() - notifyAtValue);
544
+ }
545
+ dueTime = admin.firestore.Timestamp.fromDate(dueDateTime);
546
+ }
547
+
548
+ // TODO: Determine source or default for 'actionableWindow' - consult requirements for PatientRequirementInstruction
549
+ const actionableWindowHours =
550
+ template.importance === "high"
551
+ ? 1
552
+ : template.importance === "medium"
553
+ ? 4
554
+ : 15; // Default to 15 hours for low importance; // Placeholder default, TODO: Define source
555
+
556
+ const instructionObject: PatientRequirementInstruction = {
557
+ instructionId:
558
+ `${template.id}_${notifyAtValue}_${newInstanceRef.id}`.replace(
559
+ /[^a-zA-Z0-9_]/g,
560
+ "_"
561
+ ),
562
+ instructionText: template.description || template.name,
563
+ dueTime: dueTime as any,
564
+ actionableWindow: actionableWindowHours, // Directly assigning the placeholder default value
565
+ status: PatientInstructionStatus.PENDING_NOTIFICATION,
566
+ originalNotifyAtValue: notifyAtValue,
567
+ originalTimeframeUnit: template.timeframe.unit,
568
+ updatedAt: admin.firestore.FieldValue.serverTimestamp() as any,
569
+ notificationId: undefined,
570
+ actionTakenAt: undefined,
571
+ };
572
+ return instructionObject;
573
+ });
574
+
575
+ const newInstanceData: Omit<PatientRequirementInstance, "id"> = {
576
+ patientId: appointment.patientId,
577
+ appointmentId: appointment.id,
578
+ originalRequirementId: template.id,
579
+ requirementName: template.name,
580
+ requirementDescription: template.description,
581
+ requirementType: template.type, // Should be RequirementType.PRE
582
+ requirementImportance: template.importance,
583
+ overallStatus: PatientRequirementOverallStatus.ACTIVE,
584
+ instructions: instructions,
585
+ // Timestamps - cast to any to satisfy client-side Timestamp type for now
586
+ createdAt: admin.firestore.FieldValue.serverTimestamp() as any,
587
+ updatedAt: admin.firestore.FieldValue.serverTimestamp() as any,
588
+ };
589
+
590
+ batch.set(newInstanceRef, newInstanceData);
591
+ instancesCreatedCount++;
592
+ Logger.debug(
593
+ `[AggService] Added PatientRequirementInstance ${newInstanceRef.id} to batch for template ${template.id}.`
594
+ );
595
+ }
596
+
597
+ if (instancesCreatedCount > 0) {
598
+ await batch.commit();
599
+ Logger.info(
600
+ `[AggService] Successfully created ${instancesCreatedCount} PRE_APPOINTMENT requirement instances for appointment ${appointment.id}.`
601
+ );
602
+ } else {
603
+ Logger.info(
604
+ `[AggService] No new PRE_APPOINTMENT requirement instances were prepared for batch commit for appointment ${appointment.id}.`
605
+ );
606
+ }
607
+ } catch (error) {
608
+ Logger.error(
609
+ `[AggService] Error creating PRE_APPOINTMENT requirement instances for appointment ${appointment.id}:`,
610
+ error
611
+ );
612
+ // Rethrow or handle as per error policy, e.g., if this failure should halt further processing.
613
+ // For now, just logging.
614
+ }
183
615
  }
184
616
 
185
617
  /**
186
- * Placeholder for a more generic calendar update trigger.
187
- * @param appointment The appointment data.
188
- * @param action The type of action ('create', 'update', 'delete').
189
- * @param previousAppointmentData Optional, for 'update' actions.
190
- * @returns {Promise<void>}
618
+ * Creates POST_APPOINTMENT PatientRequirementInstance documents for a given appointment.
619
+ * Uses the `appointment.postProcedureRequirements` array.
620
+ * For each active POST requirement template, it constructs a new PatientRequirementInstance document
621
+ * with derived instructions and batch writes them to Firestore under the patient's `patient_requirements` subcollection.
622
+ *
623
+ * @param {Appointment} appointment - The appointment for which to create post-requirement instances.
624
+ * @returns {Promise<void>} A promise that resolves when the operation is complete.
191
625
  */
192
- async triggerCalendarUpdate(
193
- appointment: Appointment,
194
- action: "create" | "update" | "delete",
195
- previousAppointmentData?: Appointment
626
+ private async createPostAppointmentRequirementInstances(
627
+ appointment: Appointment
196
628
  ): Promise<void> {
197
- console.log(
198
- `[AppointmentAggregationService] Triggering calendar update for appointment ${appointment.id}, action: ${action}`
629
+ Logger.info(
630
+ `[AggService] Creating POST-appointment requirement instances for appt: ${appointment.id}, patient: ${appointment.patientId}`
199
631
  );
200
- // Call methods on a dedicated CalendarAdminService
632
+
633
+ if (!appointment.procedureId) {
634
+ Logger.warn(
635
+ `[AggService] Appointment ${appointment.id} has no procedureId. Cannot create post-requirement instances.`
636
+ );
637
+ return;
638
+ }
639
+
640
+ if (
641
+ !appointment.postProcedureRequirements ||
642
+ appointment.postProcedureRequirements.length === 0
643
+ ) {
644
+ Logger.info(
645
+ `[AggService] No postProcedureRequirements found on appointment ${appointment.id}. Nothing to create.`
646
+ );
647
+ return;
648
+ }
649
+
650
+ try {
651
+ const batch = this.db.batch();
652
+ let instancesCreatedCount = 0;
653
+
654
+ for (const template of appointment.postProcedureRequirements) {
655
+ if (!template) continue;
656
+
657
+ if (template.type !== RequirementType.POST || !template.isActive) {
658
+ Logger.debug(
659
+ `[AggService] Skipping template ${template.id} (${template.name}): not an active POST requirement.`
660
+ );
661
+ continue;
662
+ }
663
+
664
+ Logger.debug(
665
+ `[AggService] Processing POST template ${template.id} (${template.name}) for appt ${appointment.id}`
666
+ );
667
+
668
+ const newInstanceRef = this.db
669
+ .collection(PATIENTS_COLLECTION)
670
+ .doc(appointment.patientId)
671
+ .collection(PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME)
672
+ .doc();
673
+
674
+ const instructions: PatientRequirementInstruction[] = (
675
+ template.timeframe?.notifyAt || []
676
+ ).map((notifyAtValue) => {
677
+ let dueTime: any = appointment.appointmentEndTime; // Default to appointment end time
678
+ if (template.timeframe && typeof notifyAtValue === "number") {
679
+ const dueDateTime = new Date(
680
+ appointment.appointmentEndTime.toMillis()
681
+ );
682
+ // For POST requirements, notifyAtValue usually means AFTER the event
683
+ if (template.timeframe.unit === TimeUnit.DAYS) {
684
+ dueDateTime.setDate(dueDateTime.getDate() + notifyAtValue);
685
+ } else if (template.timeframe.unit === TimeUnit.HOURS) {
686
+ dueDateTime.setHours(dueDateTime.getHours() + notifyAtValue);
687
+ }
688
+ dueTime = admin.firestore.Timestamp.fromDate(dueDateTime);
689
+ }
690
+
691
+ const actionableWindowHours =
692
+ template.importance === "high"
693
+ ? 1
694
+ : template.importance === "medium"
695
+ ? 4
696
+ : 15; // Default to 15 hours for low importance; // Placeholder default, TODO: Define source
697
+
698
+ return {
699
+ instructionId:
700
+ `${template.id}_${notifyAtValue}_${newInstanceRef.id}`.replace(
701
+ /[^a-zA-Z0-9_]/g,
702
+ "_"
703
+ ),
704
+ instructionText: template.description || template.name,
705
+ dueTime: dueTime as any,
706
+ actionableWindow: actionableWindowHours,
707
+ status: PatientInstructionStatus.PENDING_NOTIFICATION,
708
+ originalNotifyAtValue: notifyAtValue,
709
+ originalTimeframeUnit: template.timeframe.unit,
710
+ updatedAt: admin.firestore.FieldValue.serverTimestamp() as any,
711
+ notificationId: undefined,
712
+ actionTakenAt: undefined,
713
+ } as PatientRequirementInstruction;
714
+ });
715
+
716
+ const newInstanceData: Omit<PatientRequirementInstance, "id"> = {
717
+ patientId: appointment.patientId,
718
+ appointmentId: appointment.id,
719
+ originalRequirementId: template.id,
720
+ requirementName: template.name,
721
+ requirementDescription: template.description,
722
+ requirementType: template.type, // Should be RequirementType.POST
723
+ requirementImportance: template.importance,
724
+ overallStatus: PatientRequirementOverallStatus.ACTIVE,
725
+ instructions: instructions,
726
+ createdAt: admin.firestore.FieldValue.serverTimestamp() as any,
727
+ updatedAt: admin.firestore.FieldValue.serverTimestamp() as any,
728
+ };
729
+
730
+ batch.set(newInstanceRef, newInstanceData);
731
+ instancesCreatedCount++;
732
+ Logger.debug(
733
+ `[AggService] Added POST PatientRequirementInstance ${newInstanceRef.id} to batch for template ${template.id}.`
734
+ );
735
+ }
736
+
737
+ if (instancesCreatedCount > 0) {
738
+ await batch.commit();
739
+ Logger.info(
740
+ `[AggService] Successfully created ${instancesCreatedCount} POST_APPOINTMENT requirement instances for appointment ${appointment.id}.`
741
+ );
742
+ } else {
743
+ Logger.info(
744
+ `[AggService] No new POST_APPOINTMENT requirement instances were prepared for batch commit for appointment ${appointment.id}.`
745
+ );
746
+ }
747
+ } catch (error) {
748
+ Logger.error(
749
+ `[AggService] Error creating POST_APPOINTMENT requirement instances for appointment ${appointment.id}:`,
750
+ error
751
+ );
752
+ }
201
753
  }
202
754
 
203
755
  /**
204
- * Placeholder for a more generic email notification trigger.
205
- * @param appointment The appointment data.
206
- * @param emailType A string identifying the type of email (e.g., 'APPOINTMENT_CONFIRMED_PATIENT').
207
- * @param previousAppointmentData Optional, for context.
208
- * @returns {Promise<void>}
756
+ * Updates the overallStatus of all PatientRequirementInstance documents associated with a given appointment.
757
+ * This is typically used when an appointment is cancelled or rescheduled, making existing requirements void.
758
+ *
759
+ * @param {Appointment} appointment - The appointment whose requirement instances need updating.
760
+ * @param {PatientRequirementOverallStatus} newOverallStatus - The new status to set (e.g., CANCELLED_APPOINTMENT).
761
+ * @returns {Promise<void>} A promise that resolves when the operation is complete.
209
762
  */
210
- async triggerEmailNotification(
763
+ private async updateRelatedPatientRequirementInstances(
211
764
  appointment: Appointment,
212
- emailType: string,
213
- previousAppointmentData?: Appointment
765
+ newOverallStatus: PatientRequirementOverallStatus,
766
+ _previousAppointmentData?: Appointment // Not used in this basic implementation, but kept for signature consistency
214
767
  ): Promise<void> {
215
- console.log(
216
- `[AppointmentAggregationService] Triggering email type '${emailType}' for appointment ${appointment.id}`
768
+ Logger.info(
769
+ `[AggService] Updating related patient req instances for appt ${appointment.id} (patient: ${appointment.patientId}) to ${newOverallStatus}`
217
770
  );
218
- // Call methods on a dedicated AppointmentMailingService
771
+
772
+ if (!appointment.id || !appointment.patientId) {
773
+ Logger.error(
774
+ "[AggService] updateRelatedPatientRequirementInstances called with missing appointmentId or patientId.",
775
+ { appointmentId: appointment.id, patientId: appointment.patientId }
776
+ );
777
+ return;
778
+ }
779
+
780
+ try {
781
+ const instancesSnapshot = await this.db
782
+ .collection(PATIENTS_COLLECTION)
783
+ .doc(appointment.patientId)
784
+ .collection(PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME)
785
+ .where("appointmentId", "==", appointment.id)
786
+ .get();
787
+
788
+ if (instancesSnapshot.empty) {
789
+ Logger.info(
790
+ `[AggService] No patient requirement instances found for appointment ${appointment.id} to update.`
791
+ );
792
+ return;
793
+ }
794
+
795
+ const batch = this.db.batch();
796
+ let instancesUpdatedCount = 0;
797
+
798
+ instancesSnapshot.docs.forEach((doc) => {
799
+ const instance = doc.data() as PatientRequirementInstance;
800
+ // Update only if the status is actually different and not already in a terminal state like FAILED_TO_PROCESS
801
+ if (
802
+ instance.overallStatus !== newOverallStatus &&
803
+ instance.overallStatus !==
804
+ PatientRequirementOverallStatus.FAILED_TO_PROCESS
805
+ ) {
806
+ batch.update(doc.ref, {
807
+ overallStatus: newOverallStatus,
808
+ updatedAt: admin.firestore.FieldValue.serverTimestamp() as any, // Cast for now
809
+ // Potentially also cancel individual instructions if not handled by another trigger
810
+ // instructions: instance.instructions.map(instr => ({ ...instr, status: PatientInstructionStatus.CANCELLED, updatedAt: admin.firestore.FieldValue.serverTimestamp() as any }))
811
+ });
812
+ instancesUpdatedCount++;
813
+ Logger.debug(
814
+ `[AggService] Added update for PatientRequirementInstance ${doc.id} to batch. New status: ${newOverallStatus}`
815
+ );
816
+ }
817
+ });
818
+
819
+ if (instancesUpdatedCount > 0) {
820
+ await batch.commit();
821
+ Logger.info(
822
+ `[AggService] Successfully updated ${instancesUpdatedCount} patient requirement instances for appointment ${appointment.id} to status ${newOverallStatus}.`
823
+ );
824
+ } else {
825
+ Logger.info(
826
+ `[AggService] No patient requirement instances needed an update for appointment ${appointment.id}.`
827
+ );
828
+ }
829
+ } catch (error) {
830
+ Logger.error(
831
+ `[AggService] Error updating patient requirement instances for appointment ${appointment.id}:`,
832
+ error
833
+ );
834
+ }
219
835
  }
220
836
 
221
837
  /**
222
- * Handles logic for sending a review request after appointment completion.
223
- * @param appointment - The completed appointment.
224
- * @returns {Promise<void>}
838
+ * Manages denormalized links between a patient and the clinic/practitioner associated with an appointment.
839
+ * Adds patientId to arrays on clinic and practitioner documents on appointment creation.
840
+ * Removes patientId on appointment cancellation (basic removal, can be enhanced).
841
+ *
842
+ * @param {Appointment} appointment - The appointment object.
843
+ * @param {"create" | "cancel"} action - 'create' to add links, 'cancel' to remove them.
844
+ * @returns {Promise<void>} A promise that resolves when the operation is complete.
225
845
  */
226
- async handleReviewRequest(appointment: Appointment): Promise<void> {
227
- console.log(
228
- `[AppointmentAggregationService] Handling review request for appointment ${appointment.id}`
846
+ private async managePatientClinicPractitionerLinks(
847
+ appointment: Appointment,
848
+ action: "create" | "cancel"
849
+ ): Promise<void> {
850
+ Logger.info(
851
+ `[AggService] Managing patient-clinic-practitioner links for appt ${appointment.id} (patient: ${appointment.patientId}), action: ${action}`
229
852
  );
230
- // Logic to schedule or send a review request notification/email.
231
- // Might involve checking if a review already exists.
853
+
854
+ if (
855
+ !appointment.patientId ||
856
+ !appointment.practitionerId ||
857
+ !appointment.clinicBranchId
858
+ ) {
859
+ Logger.error(
860
+ "[AggService] managePatientClinicPractitionerLinks called with missing IDs.",
861
+ {
862
+ patientId: appointment.patientId,
863
+ practitionerId: appointment.practitionerId,
864
+ clinicBranchId: appointment.clinicBranchId,
865
+ }
866
+ );
867
+ return;
868
+ }
869
+
870
+ const batch = this.db.batch();
871
+ const patientId = appointment.patientId;
872
+
873
+ // Practitioner link
874
+ const practitionerRef = this.db
875
+ .collection(PRACTITIONERS_COLLECTION)
876
+ .doc(appointment.practitionerId);
877
+ // Clinic link
878
+ const clinicRef = this.db
879
+ .collection(CLINICS_COLLECTION)
880
+ .doc(appointment.clinicBranchId);
881
+
882
+ if (action === "create") {
883
+ // Add patient to practitioner's list of patients (if field exists, e.g., 'patientIds')
884
+ // Assuming a field named 'patientIds' on the practitioner document
885
+ batch.update(practitionerRef, {
886
+ patientIds: admin.firestore.FieldValue.arrayUnion(patientId),
887
+ // Optionally, update a count or last interaction timestamp
888
+ updatedAt: admin.firestore.FieldValue.serverTimestamp() as any,
889
+ });
890
+ Logger.debug(
891
+ `[AggService] Adding patient ${patientId} to practitioner ${appointment.practitionerId} patientIds.`
892
+ );
893
+
894
+ // Add patient to clinic's list of patients (if field exists, e.g., 'patientIds')
895
+ // Assuming a field named 'patientIds' on the clinic document
896
+ batch.update(clinicRef, {
897
+ patientIds: admin.firestore.FieldValue.arrayUnion(patientId),
898
+ updatedAt: admin.firestore.FieldValue.serverTimestamp() as any,
899
+ });
900
+ Logger.debug(
901
+ `[AggService] Adding patient ${patientId} to clinic ${appointment.clinicBranchId} patientIds.`
902
+ );
903
+ } else if (action === "cancel") {
904
+ // Basic removal.
905
+ // TODO: Implement more robust removal logic: only remove if this was the last active/confirmed appointment
906
+ // linking this patient to this practitioner/clinic.
907
+ batch.update(practitionerRef, {
908
+ patientIds: admin.firestore.FieldValue.arrayRemove(patientId),
909
+ updatedAt: admin.firestore.FieldValue.serverTimestamp() as any,
910
+ });
911
+ Logger.debug(
912
+ `[AggService] Removing patient ${patientId} from practitioner ${appointment.practitionerId} patientIds.`
913
+ );
914
+
915
+ batch.update(clinicRef, {
916
+ patientIds: admin.firestore.FieldValue.arrayRemove(patientId),
917
+ updatedAt: admin.firestore.FieldValue.serverTimestamp() as any,
918
+ });
919
+ Logger.debug(
920
+ `[AggService] Removing patient ${patientId} from clinic ${appointment.clinicBranchId} patientIds.`
921
+ );
922
+ }
923
+
924
+ try {
925
+ await batch.commit();
926
+ Logger.info(
927
+ `[AggService] Successfully updated patient-clinic-practitioner links for appointment ${appointment.id}, action: ${action}.`
928
+ );
929
+ } catch (error) {
930
+ Logger.error(
931
+ `[AggService] Error managing patient-clinic-practitioner links for appointment ${appointment.id}:`,
932
+ error
933
+ );
934
+ }
232
935
  }
233
936
 
234
- // --- Helper methods --- //
937
+ // --- Data Fetching Helpers (Consider moving to a data access layer or using existing services if available) ---
938
+ private async fetchPatientProfile(
939
+ patientId: string
940
+ ): Promise<PatientProfile | null> {
941
+ try {
942
+ const doc = await this.db
943
+ .collection(PATIENTS_COLLECTION)
944
+ .doc(patientId)
945
+ .get();
946
+ return doc.exists ? (doc.data() as PatientProfile) : null;
947
+ } catch (error) {
948
+ Logger.error(
949
+ `[AggService] Error fetching patient profile ${patientId}:`,
950
+ error
951
+ );
952
+ return null;
953
+ }
954
+ }
235
955
 
236
956
  /**
237
- * Fetches requirement templates based on procedure ID and type (PRE/POST).
238
- * @param procedureId The ID of the procedure.
239
- * @param type 'PRE' or 'POST'.
240
- * @returns {Promise<RequirementTemplate[]>}
957
+ * Fetches the sensitive information for a given patient ID.
958
+ * @param patientId The ID of the patient to fetch sensitive information for.
959
+ * @returns {Promise<PatientSensitiveInfo | null>} The patient sensitive information or null if not found or an error occurs.
241
960
  */
242
- private async fetchRequirementTemplates(
243
- procedureId: string,
244
- type: "PRE" | "POST" // Assuming type is stored on the template
245
- ): Promise<RequirementTemplate[]> {
246
- console.log(
247
- `[AppointmentAggregationService] Fetching '${type}' requirement templates for procedure ${procedureId}`
248
- );
249
- // const templatesSnapshot = await this.db
250
- // .collection(REQUIREMENTS_TEMPLATES_COLLECTION)
251
- // .where("procedureIds", "array-contains", procedureId)
252
- // .where("type", "==", type) // Assuming a 'type' field on the template
253
- // .where("isActive", "==", true)
254
- // .get();
255
- // return templatesSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as RequirementTemplate));
256
- return []; // Placeholder
961
+ private async fetchPatientSensitiveInfo(
962
+ patientId: string
963
+ ): Promise<PatientSensitiveInfo | null> {
964
+ try {
965
+ // Assuming sensitive info is in a subcollection PATIENT_SENSITIVE_INFO_COLLECTION
966
+ // under the patient's document, and the sensitive info document ID is the patientId itself.
967
+ // If the document ID is fixed (e.g., 'details'), this path should be adjusted.
968
+ const doc = await this.db
969
+ .collection(PATIENTS_COLLECTION)
970
+ .doc(patientId)
971
+ .collection(PATIENT_SENSITIVE_INFO_COLLECTION)
972
+ .doc(patientId) // CONFIRM THIS DOCUMENT ID PATTERN
973
+ .get();
974
+ if (!doc.exists) {
975
+ Logger.warn(
976
+ `[AggService] No sensitive info found for patient ${patientId}`
977
+ );
978
+ return null;
979
+ }
980
+ return doc.data() as PatientSensitiveInfo;
981
+ } catch (error) {
982
+ Logger.error(
983
+ `[AggService] Error fetching patient sensitive info ${patientId}:`,
984
+ error
985
+ );
986
+ return null;
987
+ }
257
988
  }
258
989
 
259
990
  /**
260
- * Creates a PatientRequirementInstance document.
261
- * @param appointment The parent appointment.
262
- * @param template The RequirementTemplate to base the instance on.
263
- * @param type 'PRE' or 'POST'.
264
- * @returns {Promise<string>} ID of the created instance.
991
+ * Fetches the profile for a given practitioner ID.
992
+ * @param practitionerId The ID of the practitioner to fetch.
993
+ * @returns {Promise<Practitioner | null>} The practitioner profile or null if not found or an error occurs.
265
994
  */
266
- private async createPatientRequirementInstance(
267
- appointment: Appointment,
268
- template: RequirementTemplate,
269
- type: "PRE" | "POST"
270
- ): Promise<string> {
271
- console.log(
272
- `[AppointmentAggregationService] Creating '${type}' PatientRequirementInstance for appointment ${appointment.id} from template ${template.id}`
273
- );
274
- const newInstanceRef = this.db
275
- .collection(PATIENTS_COLLECTION)
276
- .doc(appointment.patientId)
277
- .collection(PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME)
278
- .doc(); // Auto-generate ID
279
-
280
- // const instructions = template.instructions.map(instrTemplate => ({
281
- // ...instrTemplate, // Spread common fields like instructionText, instructionId (if pre-defined)
282
- // instructionId: instrTemplate.instructionId || this.db.collection('_').doc().id, // Ensure unique ID
283
- // status: PatientInstructionStatus.PENDING_NOTIFICATION,
284
- // dueTime: this.calculateInstructionDueTime(appointment, instrTemplate, type),
285
- // createdAt: admin.firestore.FieldValue.serverTimestamp(),
286
- // updatedAt: admin.firestore.FieldValue.serverTimestamp(),
287
- // }));
288
-
289
- // const instanceData: Omit<PatientRequirementInstance, 'id' | 'createdAt' | 'updatedAt'> = {
290
- // patientId: appointment.patientId,
291
- // appointmentId: appointment.id,
292
- // originalRequirementId: template.id,
293
- // requirementName: template.name,
294
- // type: type, // 'PRE' or 'POST'
295
- // overallStatus: PatientRequirementOverallStatus.ACTIVE,
296
- // instructions: instructions,
297
- // // ... other fields like clinicId, practitionerId if needed from appointment
298
- // };
299
-
300
- // await newInstanceRef.set({
301
- // ...instanceData,
302
- // createdAt: admin.firestore.FieldValue.serverTimestamp(),
303
- // updatedAt: admin.firestore.FieldValue.serverTimestamp(),
304
- // });
305
- // return newInstanceRef.id;
306
- return newInstanceRef.id; // Placeholder
995
+ private async fetchPractitionerProfile(
996
+ practitionerId: string
997
+ ): Promise<Practitioner | null> {
998
+ if (!practitionerId) {
999
+ Logger.warn(
1000
+ "[AggService] fetchPractitionerProfile called with no practitionerId."
1001
+ );
1002
+ return null;
1003
+ }
1004
+ try {
1005
+ const doc = await this.db
1006
+ .collection(PRACTITIONERS_COLLECTION)
1007
+ .doc(practitionerId)
1008
+ .get();
1009
+ if (!doc.exists) {
1010
+ Logger.warn(
1011
+ `[AggService] No practitioner profile found for ID ${practitionerId}`
1012
+ );
1013
+ return null;
1014
+ }
1015
+ return doc.data() as Practitioner;
1016
+ } catch (error) {
1017
+ Logger.error(
1018
+ `[AggService] Error fetching practitioner profile ${practitionerId}:`,
1019
+ error
1020
+ );
1021
+ return null;
1022
+ }
307
1023
  }
308
1024
 
309
1025
  /**
310
- * Calculates the due time for an instruction.
311
- * @param appointment The appointment.
312
- * @param instructionTemplate The instruction template from RequirementTemplate.
313
- * @param type 'PRE' or 'POST'.
314
- * @returns {admin.firestore.Timestamp}
1026
+ * Fetches the information for a given clinic ID.
1027
+ * @param clinicId The ID of the clinic to fetch.
1028
+ * @returns {Promise<Clinic | null>} The clinic information or null if not found or an error occurs.
315
1029
  */
316
- // private calculateInstructionDueTime(appointment: Appointment, instructionTemplate: any, type: 'PRE' | 'POST'): admin.firestore.Timestamp {
317
- // const baseTime = type === 'PRE' ? appointment.appointmentStartTime : appointment.appointmentEndTime;
318
- // // Logic to add/subtract instructionTemplate.offset (e.g., { value: 24, unit: 'hours' }) from baseTime
319
- // return baseTime; // Placeholder
320
- // }
1030
+ private async fetchClinicInfo(clinicId: string): Promise<Clinic | null> {
1031
+ if (!clinicId) {
1032
+ Logger.warn("[AggService] fetchClinicInfo called with no clinicId.");
1033
+ return null;
1034
+ }
1035
+ try {
1036
+ const doc = await this.db
1037
+ .collection(CLINICS_COLLECTION)
1038
+ .doc(clinicId)
1039
+ .get();
1040
+ if (!doc.exists) {
1041
+ Logger.warn(`[AggService] No clinic info found for ID ${clinicId}`);
1042
+ return null;
1043
+ }
1044
+ return doc.data() as Clinic;
1045
+ } catch (error) {
1046
+ Logger.error(
1047
+ `[AggService] Error fetching clinic info ${clinicId}:`,
1048
+ error
1049
+ );
1050
+ return null;
1051
+ }
1052
+ }
321
1053
  }