@blackcode_sa/metaestetics-api 1.7.2 → 1.7.4
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 +41 -8
- package/dist/admin/index.d.ts +41 -8
- package/dist/admin/index.js +621 -109
- package/dist/admin/index.mjs +621 -109
- package/dist/index.d.mts +281 -342
- package/dist/index.d.ts +281 -342
- package/dist/index.js +1178 -1124
- package/dist/index.mjs +388 -334
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +553 -112
- package/src/admin/calendar/calendar.admin.service.ts +230 -65
- package/src/services/calendar/calendar-refactored.service.ts +2 -0
- package/src/services/patient/utils/medical.utils.ts +26 -23
- package/src/services/patient/utils/practitioner.utils.ts +102 -0
- package/src/types/calendar/index.ts +1 -0
- package/src/types/patient/medical-info.types.ts +2 -2
- package/src/validations/common.schema.ts +15 -4
|
@@ -37,6 +37,7 @@ import { CalendarAdminService } from "../../calendar/calendar.admin.service";
|
|
|
37
37
|
import { AppointmentMailingService } from "../../mailing/appointment/appointment.mailing.service";
|
|
38
38
|
import { Logger } from "../../logger";
|
|
39
39
|
import { UserRole } from "../../../types";
|
|
40
|
+
import { CalendarEventStatus } from "../../../types/calendar";
|
|
40
41
|
|
|
41
42
|
// Mailgun client will be injected via constructor
|
|
42
43
|
|
|
@@ -87,11 +88,7 @@ export class AppointmentAggregationService {
|
|
|
87
88
|
);
|
|
88
89
|
|
|
89
90
|
try {
|
|
90
|
-
// 1.
|
|
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");
|
|
93
|
-
|
|
94
|
-
// 2. Fetch necessary profiles for notifications and context
|
|
91
|
+
// 1. Fetch necessary profiles for notifications and context
|
|
95
92
|
// These can be fetched in parallel
|
|
96
93
|
const [
|
|
97
94
|
patientProfile,
|
|
@@ -105,6 +102,17 @@ export class AppointmentAggregationService {
|
|
|
105
102
|
this.fetchClinicInfo(appointment.clinicBranchId), // Needed for clinic admin notifications
|
|
106
103
|
]);
|
|
107
104
|
|
|
105
|
+
// 2. Manage Patient-Clinic-Practitioner Links (moved from beginning to here)
|
|
106
|
+
// Now we can pass the already fetched patient profile
|
|
107
|
+
if (patientProfile) {
|
|
108
|
+
await this.managePatientClinicPractitionerLinks(
|
|
109
|
+
patientProfile,
|
|
110
|
+
appointment.practitionerId,
|
|
111
|
+
appointment.clinicBranchId,
|
|
112
|
+
"create"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
108
116
|
// 3. Initial State Handling based on appointment status
|
|
109
117
|
if (appointment.status === AppointmentStatus.CONFIRMED) {
|
|
110
118
|
Logger.info(
|
|
@@ -252,6 +260,13 @@ export class AppointmentAggregationService {
|
|
|
252
260
|
) {
|
|
253
261
|
Logger.info(`[AggService] Appt ${after.id} PENDING -> CONFIRMED.`);
|
|
254
262
|
await this.createPreAppointmentRequirementInstances(after);
|
|
263
|
+
|
|
264
|
+
// Update calendar events to CONFIRMED status
|
|
265
|
+
await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
|
|
266
|
+
after,
|
|
267
|
+
CalendarEventStatus.CONFIRMED
|
|
268
|
+
);
|
|
269
|
+
|
|
255
270
|
// Send confirmation notifications
|
|
256
271
|
if (patientSensitiveInfo?.email && patientProfile) {
|
|
257
272
|
Logger.info(
|
|
@@ -296,10 +311,72 @@ export class AppointmentAggregationService {
|
|
|
296
311
|
recipientRole: "practitioner" as const,
|
|
297
312
|
};
|
|
298
313
|
await this.appointmentMailingService.sendAppointmentConfirmedEmail(
|
|
299
|
-
practitionerEmailData as any
|
|
314
|
+
practitionerEmailData as any
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// --- RESCHEDULED_BY_CLINIC -> CONFIRMED (Reschedule Acceptance) ---
|
|
319
|
+
else if (
|
|
320
|
+
before.status === AppointmentStatus.RESCHEDULED_BY_CLINIC &&
|
|
321
|
+
after.status === AppointmentStatus.CONFIRMED
|
|
322
|
+
) {
|
|
323
|
+
Logger.info(
|
|
324
|
+
`[AggService] Appt ${after.id} RESCHEDULED_BY_CLINIC -> CONFIRMED.`
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// Update existing requirements as superseded and create new ones
|
|
328
|
+
await this.updateRelatedPatientRequirementInstances(
|
|
329
|
+
before,
|
|
330
|
+
PatientRequirementOverallStatus.SUPERSEDED_RESCHEDULE
|
|
331
|
+
);
|
|
332
|
+
await this.createPreAppointmentRequirementInstances(after);
|
|
333
|
+
|
|
334
|
+
// Update calendar events to CONFIRMED status and update times
|
|
335
|
+
await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
|
|
336
|
+
after,
|
|
337
|
+
CalendarEventStatus.CONFIRMED
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Send confirmation notifications (similar to PENDING -> CONFIRMED)
|
|
341
|
+
if (patientSensitiveInfo?.email && patientProfile) {
|
|
342
|
+
Logger.info(
|
|
343
|
+
`[AggService] Sending appointment confirmed email to patient ${patientSensitiveInfo.email}`
|
|
344
|
+
);
|
|
345
|
+
const emailData = {
|
|
346
|
+
appointment: after,
|
|
347
|
+
recipientProfile: after.patientInfo,
|
|
348
|
+
recipientRole: "patient" as const,
|
|
349
|
+
};
|
|
350
|
+
await this.appointmentMailingService.sendAppointmentConfirmedEmail(
|
|
351
|
+
emailData as any
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (
|
|
356
|
+
patientProfile?.expoTokens &&
|
|
357
|
+
patientProfile.expoTokens.length > 0
|
|
358
|
+
) {
|
|
359
|
+
await this.notificationsAdmin.sendAppointmentConfirmedPush(
|
|
360
|
+
after,
|
|
361
|
+
after.patientId,
|
|
362
|
+
patientProfile.expoTokens,
|
|
363
|
+
UserRole.PATIENT
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (practitionerProfile?.basicInfo?.email) {
|
|
368
|
+
Logger.info(
|
|
369
|
+
`[AggService] Sending appointment confirmation email to practitioner ${practitionerProfile.basicInfo.email}`
|
|
370
|
+
);
|
|
371
|
+
const practitionerEmailData = {
|
|
372
|
+
appointment: after,
|
|
373
|
+
recipientProfile: after.practitionerInfo,
|
|
374
|
+
recipientRole: "practitioner" as const,
|
|
375
|
+
};
|
|
376
|
+
await this.appointmentMailingService.sendAppointmentConfirmedEmail(
|
|
377
|
+
practitionerEmailData as any
|
|
300
378
|
);
|
|
301
379
|
}
|
|
302
|
-
// TODO: Add push notification for practitioner if they have expoTokens
|
|
303
380
|
}
|
|
304
381
|
// --- Any -> CANCELLED_* ---
|
|
305
382
|
else if (
|
|
@@ -315,7 +392,37 @@ export class AppointmentAggregationService {
|
|
|
315
392
|
after,
|
|
316
393
|
PatientRequirementOverallStatus.CANCELLED_APPOINTMENT
|
|
317
394
|
);
|
|
318
|
-
|
|
395
|
+
|
|
396
|
+
// Update patient-clinic-practitioner links if patient profile exists
|
|
397
|
+
if (patientProfile) {
|
|
398
|
+
await this.managePatientClinicPractitionerLinks(
|
|
399
|
+
patientProfile,
|
|
400
|
+
after.practitionerId,
|
|
401
|
+
after.clinicBranchId,
|
|
402
|
+
"cancel",
|
|
403
|
+
after.status
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const calendarStatus = (status: AppointmentStatus) => {
|
|
408
|
+
switch (status) {
|
|
409
|
+
case AppointmentStatus.NO_SHOW:
|
|
410
|
+
return CalendarEventStatus.NO_SHOW;
|
|
411
|
+
case AppointmentStatus.CANCELED_CLINIC:
|
|
412
|
+
return CalendarEventStatus.REJECTED;
|
|
413
|
+
case AppointmentStatus.CANCELED_PATIENT:
|
|
414
|
+
return CalendarEventStatus.CANCELED;
|
|
415
|
+
case AppointmentStatus.CANCELED_PATIENT_RESCHEDULED:
|
|
416
|
+
return CalendarEventStatus.REJECTED;
|
|
417
|
+
default:
|
|
418
|
+
return CalendarEventStatus.CANCELED;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
|
|
423
|
+
after,
|
|
424
|
+
calendarStatus(after.status)
|
|
425
|
+
);
|
|
319
426
|
|
|
320
427
|
// Send cancellation email to Patient
|
|
321
428
|
if (patientSensitiveInfo?.email && patientProfile) {
|
|
@@ -357,6 +464,12 @@ export class AppointmentAggregationService {
|
|
|
357
464
|
Logger.info(`[AggService] Appt ${after.id} status -> COMPLETED.`);
|
|
358
465
|
await this.createPostAppointmentRequirementInstances(after);
|
|
359
466
|
|
|
467
|
+
// Update calendar events to COMPLETED status
|
|
468
|
+
await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
|
|
469
|
+
after,
|
|
470
|
+
CalendarEventStatus.COMPLETED
|
|
471
|
+
);
|
|
472
|
+
|
|
360
473
|
// Send review request email to patient
|
|
361
474
|
if (patientSensitiveInfo?.email && patientProfile) {
|
|
362
475
|
Logger.info(
|
|
@@ -382,8 +495,20 @@ export class AppointmentAggregationService {
|
|
|
382
495
|
before, // Pass the 'before' state for old requirements
|
|
383
496
|
PatientRequirementOverallStatus.SUPERSEDED_RESCHEDULE
|
|
384
497
|
);
|
|
385
|
-
|
|
386
|
-
|
|
498
|
+
|
|
499
|
+
// First update the calendar event times with new proposed times
|
|
500
|
+
await this.calendarAdminService.updateAppointmentCalendarEventsTime(
|
|
501
|
+
after,
|
|
502
|
+
{
|
|
503
|
+
start: after.appointmentStartTime,
|
|
504
|
+
end: after.appointmentEndTime,
|
|
505
|
+
}
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
// Then update calendar events to PENDING status (waiting for patient confirmation)
|
|
509
|
+
await this.calendarAdminService.updateAppointmentCalendarEventsStatus(
|
|
510
|
+
after,
|
|
511
|
+
CalendarEventStatus.PENDING
|
|
387
512
|
);
|
|
388
513
|
|
|
389
514
|
// Send reschedule proposal email to patient
|
|
@@ -414,16 +539,29 @@ export class AppointmentAggregationService {
|
|
|
414
539
|
if (timeChanged && !statusChanged) {
|
|
415
540
|
// Or if status change didn't fully cover reschedule implications
|
|
416
541
|
Logger.info(`[AggService] Appointment ${after.id} time changed.`);
|
|
417
|
-
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
542
|
+
|
|
543
|
+
// If confirmed appointment has time change, we need to update requirements
|
|
544
|
+
if (after.status === AppointmentStatus.CONFIRMED) {
|
|
545
|
+
// Update existing requirements as superseded and create new ones
|
|
546
|
+
await this.updateRelatedPatientRequirementInstances(
|
|
547
|
+
before,
|
|
548
|
+
PatientRequirementOverallStatus.SUPERSEDED_RESCHEDULE
|
|
549
|
+
);
|
|
550
|
+
await this.createPreAppointmentRequirementInstances(after);
|
|
551
|
+
|
|
552
|
+
// Update calendar event times with new times
|
|
553
|
+
await this.calendarAdminService.updateAppointmentCalendarEventsTime(
|
|
554
|
+
after,
|
|
555
|
+
{
|
|
556
|
+
start: after.appointmentStartTime,
|
|
557
|
+
end: after.appointmentEndTime,
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
} else {
|
|
561
|
+
Logger.warn(
|
|
562
|
+
`[AggService] Independent time change detected for ${after.id} with status ${after.status}. Review implications for requirements and calendar.`
|
|
563
|
+
);
|
|
564
|
+
}
|
|
427
565
|
}
|
|
428
566
|
|
|
429
567
|
// TODO: Handle Payment Status Change
|
|
@@ -461,11 +599,27 @@ export class AppointmentAggregationService {
|
|
|
461
599
|
deletedAppointment,
|
|
462
600
|
PatientRequirementOverallStatus.CANCELLED_APPOINTMENT
|
|
463
601
|
);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
602
|
+
|
|
603
|
+
// Fetch patient profile first
|
|
604
|
+
const patientProfile = await this.fetchPatientProfile(
|
|
605
|
+
deletedAppointment.patientId
|
|
468
606
|
);
|
|
607
|
+
|
|
608
|
+
// Update relationship links if patient profile exists
|
|
609
|
+
if (patientProfile) {
|
|
610
|
+
await this.managePatientClinicPractitionerLinks(
|
|
611
|
+
patientProfile,
|
|
612
|
+
deletedAppointment.practitionerId,
|
|
613
|
+
deletedAppointment.clinicBranchId,
|
|
614
|
+
"cancel"
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Delete all associated calendar events
|
|
619
|
+
await this.calendarAdminService.deleteAppointmentCalendarEvents(
|
|
620
|
+
deletedAppointment
|
|
621
|
+
);
|
|
622
|
+
|
|
469
623
|
// TODO: Send cancellation/deletion notifications if appropriate (though data is gone)
|
|
470
624
|
}
|
|
471
625
|
|
|
@@ -812,10 +966,34 @@ export class AppointmentAggregationService {
|
|
|
812
966
|
try {
|
|
813
967
|
const batch = this.db.batch();
|
|
814
968
|
let instancesCreatedCount = 0;
|
|
969
|
+
// Store created instances for fallback direct creation if needed
|
|
970
|
+
let createdInstances = [];
|
|
971
|
+
|
|
972
|
+
// Log more details about the post-requirements
|
|
973
|
+
Logger.info(
|
|
974
|
+
`[AggService] Found ${
|
|
975
|
+
appointment.postProcedureRequirements.length
|
|
976
|
+
} post-requirements to process: ${JSON.stringify(
|
|
977
|
+
appointment.postProcedureRequirements.map((r) => ({
|
|
978
|
+
id: r.id,
|
|
979
|
+
name: r.name,
|
|
980
|
+
type: r.type,
|
|
981
|
+
isActive: r.isActive,
|
|
982
|
+
hasTimeframe: !!r.timeframe,
|
|
983
|
+
notifyAtLength: r.timeframe?.notifyAt?.length || 0,
|
|
984
|
+
}))
|
|
985
|
+
)}`
|
|
986
|
+
);
|
|
815
987
|
|
|
816
988
|
for (const template of appointment.postProcedureRequirements) {
|
|
817
|
-
if (!template)
|
|
989
|
+
if (!template) {
|
|
990
|
+
Logger.warn(
|
|
991
|
+
`[AggService] Found null/undefined template in postProcedureRequirements array`
|
|
992
|
+
);
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
818
995
|
|
|
996
|
+
// Ensure it's an active, POST-type requirement
|
|
819
997
|
if (template.type !== RequirementType.POST || !template.isActive) {
|
|
820
998
|
Logger.debug(
|
|
821
999
|
`[AggService] Skipping template ${template.id} (${template.name}): not an active POST requirement.`
|
|
@@ -823,25 +1001,40 @@ export class AppointmentAggregationService {
|
|
|
823
1001
|
continue;
|
|
824
1002
|
}
|
|
825
1003
|
|
|
1004
|
+
if (
|
|
1005
|
+
!template.timeframe ||
|
|
1006
|
+
!template.timeframe.notifyAt ||
|
|
1007
|
+
template.timeframe.notifyAt.length === 0
|
|
1008
|
+
) {
|
|
1009
|
+
Logger.warn(
|
|
1010
|
+
`[AggService] Template ${template.id} (${template.name}) has no timeframe.notifyAt values. Creating with empty instructions.`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
826
1014
|
Logger.debug(
|
|
827
|
-
`[AggService] Processing
|
|
1015
|
+
`[AggService] Processing template ${template.id} (${template.name}) for appt ${appointment.id}`
|
|
828
1016
|
);
|
|
829
1017
|
|
|
830
1018
|
const newInstanceRef = this.db
|
|
831
1019
|
.collection(PATIENTS_COLLECTION)
|
|
832
1020
|
.doc(appointment.patientId)
|
|
833
1021
|
.collection(PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME)
|
|
834
|
-
.doc();
|
|
1022
|
+
.doc(); // Auto-generate ID for the new instance
|
|
1023
|
+
|
|
1024
|
+
// Log the path for debugging
|
|
1025
|
+
Logger.debug(
|
|
1026
|
+
`[AggService] Created doc reference: ${newInstanceRef.path}`
|
|
1027
|
+
);
|
|
835
1028
|
|
|
836
1029
|
const instructions: PatientRequirementInstruction[] = (
|
|
837
1030
|
template.timeframe?.notifyAt || []
|
|
838
1031
|
).map((notifyAtValue) => {
|
|
839
|
-
let dueTime: any = appointment.appointmentEndTime;
|
|
1032
|
+
let dueTime: any = appointment.appointmentEndTime;
|
|
840
1033
|
if (template.timeframe && typeof notifyAtValue === "number") {
|
|
841
1034
|
const dueDateTime = new Date(
|
|
842
1035
|
appointment.appointmentEndTime.toMillis()
|
|
843
1036
|
);
|
|
844
|
-
// For POST requirements, notifyAtValue
|
|
1037
|
+
// For POST requirements, notifyAtValue means AFTER the event
|
|
845
1038
|
if (template.timeframe.unit === TimeUnit.DAYS) {
|
|
846
1039
|
dueDateTime.setDate(dueDateTime.getDate() + notifyAtValue);
|
|
847
1040
|
} else if (template.timeframe.unit === TimeUnit.HOURS) {
|
|
@@ -855,9 +1048,9 @@ export class AppointmentAggregationService {
|
|
|
855
1048
|
? 1
|
|
856
1049
|
: template.importance === "medium"
|
|
857
1050
|
? 4
|
|
858
|
-
: 15; // Default to 15 hours for low importance
|
|
1051
|
+
: 15; // Default to 15 hours for low importance
|
|
859
1052
|
|
|
860
|
-
|
|
1053
|
+
const instructionObject: PatientRequirementInstruction = {
|
|
861
1054
|
instructionId:
|
|
862
1055
|
`${template.id}_${notifyAtValue}_${newInstanceRef.id}`.replace(
|
|
863
1056
|
/[^a-zA-Z0-9_]/g,
|
|
@@ -869,19 +1062,21 @@ export class AppointmentAggregationService {
|
|
|
869
1062
|
status: PatientInstructionStatus.PENDING_NOTIFICATION,
|
|
870
1063
|
originalNotifyAtValue: notifyAtValue,
|
|
871
1064
|
originalTimeframeUnit: template.timeframe.unit,
|
|
872
|
-
updatedAt: admin.firestore.Timestamp.now() as any,
|
|
1065
|
+
updatedAt: admin.firestore.Timestamp.now() as any,
|
|
873
1066
|
notificationId: undefined,
|
|
874
1067
|
actionTakenAt: undefined,
|
|
875
|
-
}
|
|
1068
|
+
};
|
|
1069
|
+
return instructionObject;
|
|
876
1070
|
});
|
|
877
1071
|
|
|
878
|
-
const newInstanceData:
|
|
1072
|
+
const newInstanceData: PatientRequirementInstance = {
|
|
1073
|
+
id: newInstanceRef.id,
|
|
879
1074
|
patientId: appointment.patientId,
|
|
880
1075
|
appointmentId: appointment.id,
|
|
881
1076
|
originalRequirementId: template.id,
|
|
882
1077
|
requirementName: template.name,
|
|
883
1078
|
requirementDescription: template.description,
|
|
884
|
-
requirementType: template.type,
|
|
1079
|
+
requirementType: template.type,
|
|
885
1080
|
requirementImportance: template.importance,
|
|
886
1081
|
overallStatus: PatientRequirementOverallStatus.ACTIVE,
|
|
887
1082
|
instructions: instructions,
|
|
@@ -889,18 +1084,144 @@ export class AppointmentAggregationService {
|
|
|
889
1084
|
updatedAt: admin.firestore.FieldValue.serverTimestamp() as any,
|
|
890
1085
|
};
|
|
891
1086
|
|
|
1087
|
+
// Log the data being set
|
|
1088
|
+
Logger.debug(
|
|
1089
|
+
`[AggService] Setting data for requirement: ${JSON.stringify({
|
|
1090
|
+
id: newInstanceRef.id,
|
|
1091
|
+
patientId: newInstanceData.patientId,
|
|
1092
|
+
appointmentId: newInstanceData.appointmentId,
|
|
1093
|
+
requirementName: newInstanceData.requirementName,
|
|
1094
|
+
instructionsCount: newInstanceData.instructions.length,
|
|
1095
|
+
})}`
|
|
1096
|
+
);
|
|
1097
|
+
|
|
892
1098
|
batch.set(newInstanceRef, newInstanceData);
|
|
1099
|
+
// Store for potential fallback
|
|
1100
|
+
createdInstances.push({
|
|
1101
|
+
ref: newInstanceRef,
|
|
1102
|
+
data: newInstanceData,
|
|
1103
|
+
});
|
|
1104
|
+
|
|
893
1105
|
instancesCreatedCount++;
|
|
894
1106
|
Logger.debug(
|
|
895
|
-
`[AggService] Added
|
|
1107
|
+
`[AggService] Added PatientRequirementInstance ${newInstanceRef.id} to batch for template ${template.id}.`
|
|
896
1108
|
);
|
|
897
1109
|
}
|
|
898
1110
|
|
|
899
1111
|
if (instancesCreatedCount > 0) {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1112
|
+
try {
|
|
1113
|
+
await batch.commit();
|
|
1114
|
+
Logger.info(
|
|
1115
|
+
`[AggService] Successfully created ${instancesCreatedCount} POST_APPOINTMENT requirement instances for appointment ${appointment.id}.`
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
// Verify creation success
|
|
1119
|
+
try {
|
|
1120
|
+
const verifySnapshot = await this.db
|
|
1121
|
+
.collection(PATIENTS_COLLECTION)
|
|
1122
|
+
.doc(appointment.patientId)
|
|
1123
|
+
.collection(PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME)
|
|
1124
|
+
.where("appointmentId", "==", appointment.id)
|
|
1125
|
+
.get();
|
|
1126
|
+
|
|
1127
|
+
if (verifySnapshot.empty) {
|
|
1128
|
+
Logger.warn(
|
|
1129
|
+
`[AggService] Batch commit reported success but documents not found! Attempting direct creation as fallback...`
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
// Fallback to direct creation if batch worked but docs aren't there
|
|
1133
|
+
const fallbackPromises = createdInstances.map(
|
|
1134
|
+
async ({ ref, data }) => {
|
|
1135
|
+
try {
|
|
1136
|
+
await ref.set(data);
|
|
1137
|
+
Logger.info(
|
|
1138
|
+
`[AggService] Fallback direct creation success for ${ref.id}`
|
|
1139
|
+
);
|
|
1140
|
+
return true;
|
|
1141
|
+
} catch (fallbackError) {
|
|
1142
|
+
Logger.error(
|
|
1143
|
+
`[AggService] Fallback direct creation failed for ${ref.id}:`,
|
|
1144
|
+
fallbackError
|
|
1145
|
+
);
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
);
|
|
1150
|
+
|
|
1151
|
+
const fallbackResults = await Promise.allSettled(
|
|
1152
|
+
fallbackPromises
|
|
1153
|
+
);
|
|
1154
|
+
const successCount = fallbackResults.filter(
|
|
1155
|
+
(r) => r.status === "fulfilled" && r.value === true
|
|
1156
|
+
).length;
|
|
1157
|
+
|
|
1158
|
+
if (successCount > 0) {
|
|
1159
|
+
Logger.info(
|
|
1160
|
+
`[AggService] Fallback mechanism created ${successCount} out of ${createdInstances.length} requirements`
|
|
1161
|
+
);
|
|
1162
|
+
} else {
|
|
1163
|
+
Logger.error(
|
|
1164
|
+
`[AggService] Both batch and fallback mechanisms failed to create requirements`
|
|
1165
|
+
);
|
|
1166
|
+
throw new Error(
|
|
1167
|
+
"Failed to create patient requirements through both batch and direct methods"
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
} else {
|
|
1171
|
+
Logger.info(
|
|
1172
|
+
`[AggService] Verification confirmed ${verifySnapshot.size} requirement documents created`
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
} catch (verifyError) {
|
|
1176
|
+
Logger.error(
|
|
1177
|
+
`[AggService] Error during verification of created requirements:`,
|
|
1178
|
+
verifyError
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
} catch (commitError) {
|
|
1182
|
+
Logger.error(
|
|
1183
|
+
`[AggService] Error committing batch for POST_APPOINTMENT requirement instances for appointment ${appointment.id}:`,
|
|
1184
|
+
commitError
|
|
1185
|
+
);
|
|
1186
|
+
|
|
1187
|
+
// Try direct creation as fallback
|
|
1188
|
+
Logger.info(`[AggService] Attempting direct creation as fallback...`);
|
|
1189
|
+
const fallbackPromises = createdInstances.map(
|
|
1190
|
+
async ({ ref, data }) => {
|
|
1191
|
+
try {
|
|
1192
|
+
await ref.set(data);
|
|
1193
|
+
Logger.info(
|
|
1194
|
+
`[AggService] Fallback direct creation success for ${ref.id}`
|
|
1195
|
+
);
|
|
1196
|
+
return true;
|
|
1197
|
+
} catch (fallbackError) {
|
|
1198
|
+
Logger.error(
|
|
1199
|
+
`[AggService] Fallback direct creation failed for ${ref.id}:`,
|
|
1200
|
+
fallbackError
|
|
1201
|
+
);
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
);
|
|
1206
|
+
|
|
1207
|
+
const fallbackResults = await Promise.allSettled(fallbackPromises);
|
|
1208
|
+
const successCount = fallbackResults.filter(
|
|
1209
|
+
(r) => r.status === "fulfilled" && r.value === true
|
|
1210
|
+
).length;
|
|
1211
|
+
|
|
1212
|
+
if (successCount > 0) {
|
|
1213
|
+
Logger.info(
|
|
1214
|
+
`[AggService] Fallback mechanism created ${successCount} out of ${createdInstances.length} requirements`
|
|
1215
|
+
);
|
|
1216
|
+
} else {
|
|
1217
|
+
Logger.error(
|
|
1218
|
+
`[AggService] Both batch and fallback mechanisms failed to create requirements`
|
|
1219
|
+
);
|
|
1220
|
+
throw new Error(
|
|
1221
|
+
"Failed to create patient requirements through both batch and direct methods"
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
904
1225
|
} else {
|
|
905
1226
|
Logger.info(
|
|
906
1227
|
`[AggService] No new POST_APPOINTMENT requirement instances were prepared for batch commit for appointment ${appointment.id}.`
|
|
@@ -911,6 +1232,7 @@ export class AppointmentAggregationService {
|
|
|
911
1232
|
`[AggService] Error creating POST_APPOINTMENT requirement instances for appointment ${appointment.id}:`,
|
|
912
1233
|
error
|
|
913
1234
|
);
|
|
1235
|
+
throw error; // Re-throw to ensure the caller knows there was a problem
|
|
914
1236
|
}
|
|
915
1237
|
}
|
|
916
1238
|
|
|
@@ -997,102 +1319,221 @@ export class AppointmentAggregationService {
|
|
|
997
1319
|
}
|
|
998
1320
|
|
|
999
1321
|
/**
|
|
1000
|
-
* Manages
|
|
1001
|
-
*
|
|
1002
|
-
* Removes patientId on appointment cancellation (basic removal, can be enhanced).
|
|
1322
|
+
* Manages relationships between a patient and clinics/practitioners.
|
|
1323
|
+
* Only updates the patient profile with doctorIds and clinicIds.
|
|
1003
1324
|
*
|
|
1004
|
-
* @param {
|
|
1005
|
-
* @param {
|
|
1325
|
+
* @param {PatientProfile} patientProfile - The patient profile to update
|
|
1326
|
+
* @param {string} practitionerId - The practitioner ID
|
|
1327
|
+
* @param {string} clinicId - The clinic ID
|
|
1328
|
+
* @param {"create" | "cancel"} action - 'create' to add IDs, 'cancel' to potentially remove them
|
|
1329
|
+
* @param {AppointmentStatus} [cancelStatus] - The appointment status if action is 'cancel'
|
|
1006
1330
|
* @returns {Promise<void>} A promise that resolves when the operation is complete.
|
|
1007
1331
|
*/
|
|
1008
1332
|
private async managePatientClinicPractitionerLinks(
|
|
1009
|
-
|
|
1010
|
-
|
|
1333
|
+
patientProfile: PatientProfile,
|
|
1334
|
+
practitionerId: string,
|
|
1335
|
+
clinicId: string,
|
|
1336
|
+
action: "create" | "cancel",
|
|
1337
|
+
cancelStatus?: AppointmentStatus
|
|
1011
1338
|
): Promise<void> {
|
|
1012
1339
|
Logger.info(
|
|
1013
|
-
`[AggService] Managing patient-clinic-practitioner links for
|
|
1340
|
+
`[AggService] Managing patient-clinic-practitioner links for patient ${patientProfile.id}, action: ${action}`
|
|
1014
1341
|
);
|
|
1015
1342
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1343
|
+
try {
|
|
1344
|
+
if (action === "create") {
|
|
1345
|
+
await this.addPatientLinks(patientProfile, practitionerId, clinicId);
|
|
1346
|
+
} else if (action === "cancel") {
|
|
1347
|
+
await this.removePatientLinksIfNoActiveAppointments(
|
|
1348
|
+
patientProfile,
|
|
1349
|
+
practitionerId,
|
|
1350
|
+
clinicId
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
} catch (error) {
|
|
1021
1354
|
Logger.error(
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
patientId: appointment.patientId,
|
|
1025
|
-
practitionerId: appointment.practitionerId,
|
|
1026
|
-
clinicBranchId: appointment.clinicBranchId,
|
|
1027
|
-
}
|
|
1355
|
+
`[AggService] Error managing patient-clinic-practitioner links for patient ${patientProfile.id}:`,
|
|
1356
|
+
error
|
|
1028
1357
|
);
|
|
1029
|
-
return;
|
|
1030
1358
|
}
|
|
1359
|
+
}
|
|
1031
1360
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
//
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1361
|
+
/**
|
|
1362
|
+
* Adds practitioner and clinic IDs to the patient profile.
|
|
1363
|
+
*
|
|
1364
|
+
* @param {PatientProfile} patientProfile - The patient profile to update
|
|
1365
|
+
* @param {string} practitionerId - The practitioner ID to add
|
|
1366
|
+
* @param {string} clinicId - The clinic ID to add
|
|
1367
|
+
* @returns {Promise<void>} A promise that resolves when the operation is complete.
|
|
1368
|
+
*/
|
|
1369
|
+
private async addPatientLinks(
|
|
1370
|
+
patientProfile: PatientProfile,
|
|
1371
|
+
practitionerId: string,
|
|
1372
|
+
clinicId: string
|
|
1373
|
+
): Promise<void> {
|
|
1374
|
+
try {
|
|
1375
|
+
// Check if the IDs already exist in the arrays
|
|
1376
|
+
const hasDoctor =
|
|
1377
|
+
patientProfile.doctorIds?.includes(practitionerId) || false;
|
|
1378
|
+
const hasClinic = patientProfile.clinicIds?.includes(clinicId) || false;
|
|
1379
|
+
|
|
1380
|
+
// Only update if necessary
|
|
1381
|
+
if (!hasDoctor || !hasClinic) {
|
|
1382
|
+
const patientRef = this.db
|
|
1383
|
+
.collection(PATIENTS_COLLECTION)
|
|
1384
|
+
.doc(patientProfile.id);
|
|
1385
|
+
const updateData: Record<string, any> = {
|
|
1386
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
1387
|
+
};
|
|
1055
1388
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
`[AggService] Adding patient ${patientId} to clinic ${appointment.clinicBranchId} patientIds.`
|
|
1064
|
-
);
|
|
1065
|
-
} else if (action === "cancel") {
|
|
1066
|
-
// Basic removal.
|
|
1067
|
-
// TODO: Implement more robust removal logic: only remove if this was the last active/confirmed appointment
|
|
1068
|
-
// linking this patient to this practitioner/clinic.
|
|
1069
|
-
batch.update(practitionerRef, {
|
|
1070
|
-
patientIds: admin.firestore.FieldValue.arrayRemove(patientId),
|
|
1071
|
-
updatedAt: admin.firestore.FieldValue.serverTimestamp() as any,
|
|
1072
|
-
});
|
|
1073
|
-
Logger.debug(
|
|
1074
|
-
`[AggService] Removing patient ${patientId} from practitioner ${appointment.practitionerId} patientIds.`
|
|
1075
|
-
);
|
|
1389
|
+
if (!hasDoctor) {
|
|
1390
|
+
Logger.debug(
|
|
1391
|
+
`[AggService] Adding practitioner ${practitionerId} to patient ${patientProfile.id}`
|
|
1392
|
+
);
|
|
1393
|
+
updateData.doctorIds =
|
|
1394
|
+
admin.firestore.FieldValue.arrayUnion(practitionerId);
|
|
1395
|
+
}
|
|
1076
1396
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1397
|
+
if (!hasClinic) {
|
|
1398
|
+
Logger.debug(
|
|
1399
|
+
`[AggService] Adding clinic ${clinicId} to patient ${patientProfile.id}`
|
|
1400
|
+
);
|
|
1401
|
+
updateData.clinicIds =
|
|
1402
|
+
admin.firestore.FieldValue.arrayUnion(clinicId);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
await patientRef.update(updateData);
|
|
1406
|
+
Logger.info(
|
|
1407
|
+
`[AggService] Successfully updated patient ${patientProfile.id} with new links.`
|
|
1408
|
+
);
|
|
1409
|
+
} else {
|
|
1410
|
+
Logger.info(
|
|
1411
|
+
`[AggService] Patient ${patientProfile.id} already has links to both practitioner ${practitionerId} and clinic ${clinicId}.`
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
Logger.error(
|
|
1416
|
+
`[AggService] Error updating patient ${patientProfile.id} with new links:`,
|
|
1417
|
+
error
|
|
1083
1418
|
);
|
|
1419
|
+
throw error;
|
|
1084
1420
|
}
|
|
1421
|
+
}
|
|
1085
1422
|
|
|
1423
|
+
/**
|
|
1424
|
+
* Removes practitioner and clinic IDs from the patient profile if there are no more active appointments.
|
|
1425
|
+
*
|
|
1426
|
+
* @param {PatientProfile} patientProfile - The patient profile to update
|
|
1427
|
+
* @param {string} practitionerId - The practitioner ID to remove
|
|
1428
|
+
* @param {string} clinicId - The clinic ID to remove
|
|
1429
|
+
* @returns {Promise<void>} A promise that resolves when the operation is complete.
|
|
1430
|
+
*/
|
|
1431
|
+
private async removePatientLinksIfNoActiveAppointments(
|
|
1432
|
+
patientProfile: PatientProfile,
|
|
1433
|
+
practitionerId: string,
|
|
1434
|
+
clinicId: string
|
|
1435
|
+
): Promise<void> {
|
|
1086
1436
|
try {
|
|
1087
|
-
|
|
1437
|
+
// Check for active appointments with this practitioner and clinic
|
|
1438
|
+
const activePractitionerAppointments = await this.checkActiveAppointments(
|
|
1439
|
+
patientProfile.id,
|
|
1440
|
+
"practitionerId",
|
|
1441
|
+
practitionerId
|
|
1442
|
+
);
|
|
1443
|
+
|
|
1444
|
+
const activeClinicAppointments = await this.checkActiveAppointments(
|
|
1445
|
+
patientProfile.id,
|
|
1446
|
+
"clinicBranchId",
|
|
1447
|
+
clinicId
|
|
1448
|
+
);
|
|
1449
|
+
|
|
1088
1450
|
Logger.info(
|
|
1089
|
-
`[AggService]
|
|
1451
|
+
`[AggService] Active appointment count for patient ${patientProfile.id}: With practitioner ${practitionerId}: ${activePractitionerAppointments}, With clinic ${clinicId}: ${activeClinicAppointments}`
|
|
1090
1452
|
);
|
|
1453
|
+
|
|
1454
|
+
// Only update if there are no active appointments
|
|
1455
|
+
const patientRef = this.db
|
|
1456
|
+
.collection(PATIENTS_COLLECTION)
|
|
1457
|
+
.doc(patientProfile.id);
|
|
1458
|
+
const updateData: Record<string, any> = {};
|
|
1459
|
+
let updateNeeded = false;
|
|
1460
|
+
|
|
1461
|
+
if (
|
|
1462
|
+
activePractitionerAppointments === 0 &&
|
|
1463
|
+
patientProfile.doctorIds?.includes(practitionerId)
|
|
1464
|
+
) {
|
|
1465
|
+
Logger.debug(
|
|
1466
|
+
`[AggService] Removing practitioner ${practitionerId} from patient ${patientProfile.id}`
|
|
1467
|
+
);
|
|
1468
|
+
updateData.doctorIds =
|
|
1469
|
+
admin.firestore.FieldValue.arrayRemove(practitionerId);
|
|
1470
|
+
updateNeeded = true;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
if (
|
|
1474
|
+
activeClinicAppointments === 0 &&
|
|
1475
|
+
patientProfile.clinicIds?.includes(clinicId)
|
|
1476
|
+
) {
|
|
1477
|
+
Logger.debug(
|
|
1478
|
+
`[AggService] Removing clinic ${clinicId} from patient ${patientProfile.id}`
|
|
1479
|
+
);
|
|
1480
|
+
updateData.clinicIds = admin.firestore.FieldValue.arrayRemove(clinicId);
|
|
1481
|
+
updateNeeded = true;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
if (updateNeeded) {
|
|
1485
|
+
updateData.updatedAt = admin.firestore.FieldValue.serverTimestamp();
|
|
1486
|
+
await patientRef.update(updateData);
|
|
1487
|
+
Logger.info(
|
|
1488
|
+
`[AggService] Successfully removed links from patient ${patientProfile.id}`
|
|
1489
|
+
);
|
|
1490
|
+
} else {
|
|
1491
|
+
Logger.info(
|
|
1492
|
+
`[AggService] No links need to be removed from patient ${patientProfile.id}`
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1091
1495
|
} catch (error) {
|
|
1092
1496
|
Logger.error(
|
|
1093
|
-
`[AggService] Error
|
|
1497
|
+
`[AggService] Error removing links from patient profile:`,
|
|
1094
1498
|
error
|
|
1095
1499
|
);
|
|
1500
|
+
throw error;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* Checks if there are active appointments between a patient and another entity (practitioner or clinic).
|
|
1506
|
+
*
|
|
1507
|
+
* @param {string} patientId - The patient ID.
|
|
1508
|
+
* @param {"practitionerId" | "clinicBranchId"} entityField - The field to check for the entity ID.
|
|
1509
|
+
* @param {string} entityId - The entity ID (practitioner or clinic).
|
|
1510
|
+
* @returns {Promise<number>} The number of active appointments found.
|
|
1511
|
+
*/
|
|
1512
|
+
private async checkActiveAppointments(
|
|
1513
|
+
patientId: string,
|
|
1514
|
+
entityField: "practitionerId" | "clinicBranchId",
|
|
1515
|
+
entityId: string
|
|
1516
|
+
): Promise<number> {
|
|
1517
|
+
try {
|
|
1518
|
+
// Define all cancelled/inactive appointment statuses
|
|
1519
|
+
const inactiveStatuses = [
|
|
1520
|
+
AppointmentStatus.CANCELED_CLINIC,
|
|
1521
|
+
AppointmentStatus.CANCELED_PATIENT,
|
|
1522
|
+
AppointmentStatus.CANCELED_PATIENT_RESCHEDULED,
|
|
1523
|
+
AppointmentStatus.NO_SHOW,
|
|
1524
|
+
];
|
|
1525
|
+
|
|
1526
|
+
const snapshot = await this.db
|
|
1527
|
+
.collection("appointments")
|
|
1528
|
+
.where("patientId", "==", patientId)
|
|
1529
|
+
.where(entityField, "==", entityId)
|
|
1530
|
+
.where("status", "not-in", inactiveStatuses)
|
|
1531
|
+
.get();
|
|
1532
|
+
|
|
1533
|
+
return snapshot.size;
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
Logger.error(`[AggService] Error checking active appointments:`, error);
|
|
1536
|
+
throw error;
|
|
1096
1537
|
}
|
|
1097
1538
|
}
|
|
1098
1539
|
|