@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.
- package/dist/admin/index.d.mts +13 -3
- package/dist/admin/index.d.ts +13 -3
- package/dist/admin/index.js +194 -180
- package/dist/admin/index.mjs +194 -180
- package/dist/index.d.mts +51 -7
- package/dist/index.d.ts +51 -7
- package/dist/index.js +2087 -1888
- package/dist/index.mjs +2158 -1959
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +459 -457
- package/src/services/appointment/appointment.service.ts +297 -0
- package/src/services/reviews/reviews.service.ts +30 -71
- package/src/types/appointment/index.ts +11 -0
- package/src/types/reviews/index.ts +0 -3
- package/src/validations/appointment.schema.ts +38 -18
|
@@ -1,43 +1,40 @@
|
|
|
1
|
-
import * as admin from
|
|
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
|
|
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
|
|
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
|
|
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
|
|
26
|
-
import {
|
|
27
|
-
|
|
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
|
|
35
|
-
import { NotificationsAdmin } from
|
|
36
|
-
import { CalendarAdminService } from
|
|
37
|
-
import { AppointmentMailingService } from
|
|
38
|
-
import { Logger } from
|
|
39
|
-
import { UserRole } from
|
|
40
|
-
import { CalendarEventStatus } from
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
674
|
+
).map(notifyAtValue => {
|
|
727
675
|
let dueTime: any = appointment.appointmentStartTime;
|
|
728
|
-
if (template.timeframe && typeof notifyAtValue ===
|
|
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 ===
|
|
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
|
-
|
|
751
|
-
|
|
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(
|
|
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
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
958
|
+
).map(notifyAtValue => {
|
|
1032
959
|
let dueTime: any = appointment.appointmentEndTime;
|
|
1033
|
-
if (template.timeframe && typeof notifyAtValue ===
|
|
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 ===
|
|
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
|
-
|
|
1056
|
-
|
|
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(
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
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 ===
|
|
1253
|
+
if (action === 'create') {
|
|
1345
1254
|
await this.addPatientLinks(patientProfile, practitionerId, clinicId);
|
|
1346
|
-
} else if (action ===
|
|
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
|
-
|
|
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
|
-
|
|
1441
|
-
practitionerId
|
|
1342
|
+
'practitionerId',
|
|
1343
|
+
practitionerId,
|
|
1442
1344
|
);
|
|
1443
1345
|
|
|
1444
1346
|
const activeClinicAppointments = await this.checkActiveAppointments(
|
|
1445
1347
|
patientProfile.id,
|
|
1446
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
1528
|
-
.where(
|
|
1529
|
-
.where(entityField,
|
|
1530
|
-
.where(
|
|
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(
|
|
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
|
|
1651
|
-
error
|
|
1652
|
+
`[AggService] Error handling zone photos update for appointment ${after.id}:`,
|
|
1653
|
+
error,
|
|
1652
1654
|
);
|
|
1653
|
-
|
|
1655
|
+
// Don't throw - this is a side effect and shouldn't break the main update flow
|
|
1654
1656
|
}
|
|
1655
1657
|
}
|
|
1656
1658
|
}
|