@blackcode_sa/metaestetics-api 1.5.28 → 1.5.30
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 +1324 -1
- package/dist/admin/index.d.ts +1324 -1
- package/dist/admin/index.js +1674 -2
- package/dist/admin/index.mjs +1668 -2
- package/dist/backoffice/index.d.mts +99 -7
- package/dist/backoffice/index.d.ts +99 -7
- package/dist/index.d.mts +4036 -2372
- package/dist/index.d.ts +4036 -2372
- package/dist/index.js +2331 -2009
- package/dist/index.mjs +2279 -1954
- package/package.json +2 -1
- package/src/admin/aggregation/README.md +79 -0
- package/src/admin/aggregation/clinic/README.md +52 -0
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +642 -0
- package/src/admin/aggregation/patient/README.md +27 -0
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -0
- package/src/admin/aggregation/practitioner/README.md +42 -0
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -0
- package/src/admin/aggregation/procedure/README.md +43 -0
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +508 -0
- package/src/admin/index.ts +60 -4
- package/src/admin/mailing/README.md +95 -0
- package/src/admin/mailing/base.mailing.service.ts +131 -0
- package/src/admin/mailing/index.ts +2 -0
- package/src/admin/mailing/practitionerInvite/index.ts +1 -0
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +256 -0
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -0
- package/src/index.ts +28 -4
- package/src/services/README.md +106 -0
- package/src/services/clinic/README.md +87 -0
- package/src/services/clinic/clinic.service.ts +197 -107
- package/src/services/clinic/utils/clinic.utils.ts +68 -119
- package/src/services/clinic/utils/filter.utils.d.ts +23 -0
- package/src/services/clinic/utils/filter.utils.ts +264 -0
- package/src/services/practitioner/README.md +145 -0
- package/src/services/practitioner/practitioner.service.ts +439 -104
- package/src/services/procedure/README.md +88 -0
- package/src/services/procedure/procedure.service.ts +521 -311
- package/src/services/reviews/reviews.service.ts +842 -0
- package/src/types/clinic/index.ts +24 -56
- package/src/types/practitioner/index.ts +34 -33
- package/src/types/procedure/index.ts +32 -0
- package/src/types/profile/index.ts +1 -1
- package/src/types/reviews/index.ts +126 -0
- package/src/validations/clinic.schema.ts +37 -64
- package/src/validations/practitioner.schema.ts +42 -32
- package/src/validations/procedure.schema.ts +11 -3
- package/src/validations/reviews.schema.ts +189 -0
- package/src/services/clinic/utils/review.utils.ts +0 -93
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as admin from "firebase-admin";
|
|
2
|
+
|
|
3
|
+
const CALENDAR_SUBCOLLECTION_ID = "calendar";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @class PatientAggregationService
|
|
7
|
+
* @description Handles aggregation tasks related to patient data updates/deletions.
|
|
8
|
+
*/
|
|
9
|
+
export class PatientAggregationService {
|
|
10
|
+
private db: admin.firestore.Firestore;
|
|
11
|
+
|
|
12
|
+
constructor(firestore?: admin.firestore.Firestore) {
|
|
13
|
+
this.db = firestore || admin.firestore();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// --- Methods for Patient Creation --- >
|
|
17
|
+
// No specific aggregations defined for patient creation in the plan.
|
|
18
|
+
|
|
19
|
+
// --- Methods for Patient Update --- >
|
|
20
|
+
/**
|
|
21
|
+
* Updates patient information in calendar events
|
|
22
|
+
* @param patientId - ID of the patient
|
|
23
|
+
* @param patientInfo - Updated patient information
|
|
24
|
+
* @returns {Promise<void>}
|
|
25
|
+
*/
|
|
26
|
+
async updatePatientInfoInCalendarEvents(
|
|
27
|
+
patientId: string,
|
|
28
|
+
patientInfo: any
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
if (!patientId || !patientInfo) {
|
|
31
|
+
console.log(
|
|
32
|
+
"[PatientAggregationService] Missing patientId or patientInfo for calendar update. Skipping."
|
|
33
|
+
);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(
|
|
38
|
+
`[PatientAggregationService] Querying upcoming calendar events for patient ${patientId} to update patient info.`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const now = admin.firestore.Timestamp.now();
|
|
42
|
+
// Use a Collection Group query
|
|
43
|
+
const calendarEventsQuery = this.db
|
|
44
|
+
.collectionGroup(CALENDAR_SUBCOLLECTION_ID)
|
|
45
|
+
.where("patientId", "==", patientId)
|
|
46
|
+
.where("eventTime.start", ">", now);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const snapshot = await calendarEventsQuery.get();
|
|
50
|
+
if (snapshot.empty) {
|
|
51
|
+
console.log(
|
|
52
|
+
`[PatientAggregationService] No upcoming calendar events found for patient ${patientId}. No patient info updates needed.`
|
|
53
|
+
);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const batch = this.db.batch();
|
|
58
|
+
snapshot.docs.forEach((doc) => {
|
|
59
|
+
console.log(
|
|
60
|
+
`[PatientAggregationService] Updating patient info for calendar event ${doc.ref.path}`
|
|
61
|
+
);
|
|
62
|
+
batch.update(doc.ref, {
|
|
63
|
+
patientInfo: patientInfo,
|
|
64
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await batch.commit();
|
|
69
|
+
console.log(
|
|
70
|
+
`[PatientAggregationService] Successfully updated patient info in ${snapshot.size} upcoming calendar events for patient ${patientId}.`
|
|
71
|
+
);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(
|
|
74
|
+
`[PatientAggregationService] Error updating patient info in calendar events for patient ${patientId}:`,
|
|
75
|
+
error
|
|
76
|
+
);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- Methods for Patient Deletion --- >
|
|
82
|
+
/**
|
|
83
|
+
* Cancels all upcoming calendar events associated with a deleted patient
|
|
84
|
+
* @param patientId - ID of the deleted patient
|
|
85
|
+
* @returns {Promise<void>}
|
|
86
|
+
*/
|
|
87
|
+
async cancelUpcomingCalendarEventsForPatient(
|
|
88
|
+
patientId: string
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
if (!patientId) {
|
|
91
|
+
console.log(
|
|
92
|
+
"[PatientAggregationService] Missing patientId for canceling calendar events. Skipping."
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(
|
|
98
|
+
`[PatientAggregationService] Querying upcoming calendar events for patient ${patientId} to cancel.`
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const now = admin.firestore.Timestamp.now();
|
|
102
|
+
// Use a Collection Group query
|
|
103
|
+
const calendarEventsQuery = this.db
|
|
104
|
+
.collectionGroup(CALENDAR_SUBCOLLECTION_ID)
|
|
105
|
+
.where("patientId", "==", patientId)
|
|
106
|
+
.where("eventTime.start", ">", now);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const snapshot = await calendarEventsQuery.get();
|
|
110
|
+
if (snapshot.empty) {
|
|
111
|
+
console.log(
|
|
112
|
+
`[PatientAggregationService] No upcoming calendar events found for patient ${patientId}. No events to cancel.`
|
|
113
|
+
);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const batch = this.db.batch();
|
|
118
|
+
snapshot.docs.forEach((doc) => {
|
|
119
|
+
console.log(
|
|
120
|
+
`[PatientAggregationService] Canceling calendar event ${doc.ref.path}`
|
|
121
|
+
);
|
|
122
|
+
batch.update(doc.ref, {
|
|
123
|
+
status: "CANCELED",
|
|
124
|
+
cancelReason: "Patient deleted",
|
|
125
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await batch.commit();
|
|
130
|
+
console.log(
|
|
131
|
+
`[PatientAggregationService] Successfully canceled ${snapshot.size} upcoming calendar events for patient ${patientId}.`
|
|
132
|
+
);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(
|
|
135
|
+
`[PatientAggregationService] Error canceling calendar events for patient ${patientId}:`,
|
|
136
|
+
error
|
|
137
|
+
);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Practitioner Aggregation Service (Admin)
|
|
2
|
+
|
|
3
|
+
This service centralizes the logic for updating aggregated data in various Firestore collections _when a practitioner is created, updated, or deleted_. It is designed primarily for use by Cloud Functions triggered by changes in the `practitioners` collection.
|
|
4
|
+
|
|
5
|
+
## `PractitionerAggregationService` Class
|
|
6
|
+
|
|
7
|
+
### Constructor
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
constructor(firestore?: admin.firestore.Firestore)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Initializes the service with an Admin Firestore instance. Uses the default instance if none is provided.
|
|
14
|
+
|
|
15
|
+
### Methods Triggered by Practitioner **Creation**
|
|
16
|
+
|
|
17
|
+
- **`addPractitionerToClinic(clinicId: string, doctorInfo: DoctorInfo): Promise<void>`**
|
|
18
|
+
- Adds the `practitionerId` (from `doctorInfo.id`) and the `doctorInfo` object to the `doctors` and `doctorsInfo` arrays respectively in the specified `Clinic` document.
|
|
19
|
+
- Typically called for each clinic the new practitioner is associated with.
|
|
20
|
+
|
|
21
|
+
### Methods Triggered by Practitioner **Update**
|
|
22
|
+
|
|
23
|
+
- **`updatePractitionerInfoInClinics(clinicIds: string[], doctorInfo: DoctorInfo): Promise<void>`**
|
|
24
|
+
- Updates the aggregated `doctorsInfo` array within multiple `Clinic` documents associated with the practitioner.
|
|
25
|
+
- Uses a batch write to first remove the old `DoctorInfo` (matching by `id`) and then add the updated `doctorInfo`.
|
|
26
|
+
- **`updatePractitionerInfoInProcedures(procedureIds: string[], doctorInfo: DoctorInfo): Promise<void>`**
|
|
27
|
+
- Updates the embedded `doctorInfo` object within multiple `Procedure` documents associated with the updated practitioner.
|
|
28
|
+
- **`updatePractitionerInfoInCalendarEvents(practitionerId: string, practitionerInfo: DoctorInfo): Promise<void>`**
|
|
29
|
+
- Uses a Collection Group query (`calendar` subcollection) to find all _upcoming_ calendar events associated with the `practitionerId`.
|
|
30
|
+
- Updates the embedded `practitionerInfo` object in these calendar events.
|
|
31
|
+
|
|
32
|
+
### Methods Triggered by Practitioner **Deletion**
|
|
33
|
+
|
|
34
|
+
- **`removePractitionerFromClinics(clinicIds: string[], practitionerId: string): Promise<void>`**
|
|
35
|
+
- Removes the `practitionerId` from the `doctors` array and the corresponding `DoctorInfo` object (matching by `id`) from the `doctorsInfo` array in multiple `Clinic` documents.
|
|
36
|
+
- **`cancelUpcomingCalendarEventsForPractitioner(practitionerId: string): Promise<void>`**
|
|
37
|
+
- Uses a Collection Group query (`calendar` subcollection) to find all _upcoming_ calendar events associated with the `practitionerId`.
|
|
38
|
+
- Sets the `status` of these events to `CANCELED` and adds a `cancelReason` ("Practitioner deleted").
|
|
39
|
+
- **`removePractitionerFromPatients(patientIds: string[], practitionerId: string): Promise<void>`**
|
|
40
|
+
- Removes the `practitionerId` from the `doctorIds` array in multiple `Patient` documents.
|
|
41
|
+
- **`inactivateProceduresForPractitioner(procedureIds: string[]): Promise<void>`**
|
|
42
|
+
- Sets the `isActive` flag to `false` for multiple `Procedure` documents associated with the deleted practitioner.
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import * as admin from "firebase-admin";
|
|
2
|
+
import { DoctorInfo } from "../../../types/clinic";
|
|
3
|
+
import { PROCEDURES_COLLECTION } from "../../../types/procedure";
|
|
4
|
+
import { PRACTITIONERS_COLLECTION } from "../../../types/practitioner";
|
|
5
|
+
import { PATIENTS_COLLECTION } from "../../../types/patient";
|
|
6
|
+
import { CLINICS_COLLECTION } from "../../../types/clinic";
|
|
7
|
+
|
|
8
|
+
const CALENDAR_SUBCOLLECTION_ID = "calendar";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @class PractitionerAggregationService
|
|
12
|
+
* @description Handles aggregation tasks related to practitioner data updates/deletions.
|
|
13
|
+
*/
|
|
14
|
+
export class PractitionerAggregationService {
|
|
15
|
+
private db: admin.firestore.Firestore;
|
|
16
|
+
|
|
17
|
+
constructor(firestore?: admin.firestore.Firestore) {
|
|
18
|
+
this.db = firestore || admin.firestore();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Adds practitioner information to a clinic when a new practitioner is created
|
|
23
|
+
* @param clinicId - ID of the clinic to update
|
|
24
|
+
* @param doctorInfo - Doctor information to add to the clinic
|
|
25
|
+
* @returns {Promise<void>}
|
|
26
|
+
*/
|
|
27
|
+
async addPractitionerToClinic(
|
|
28
|
+
clinicId: string,
|
|
29
|
+
doctorInfo: DoctorInfo
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
if (!clinicId || !doctorInfo) {
|
|
32
|
+
console.log(
|
|
33
|
+
"[PractitionerAggregationService] Missing clinicId or doctorInfo for adding practitioner to clinic. Skipping."
|
|
34
|
+
);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const practitionerId = doctorInfo.id;
|
|
39
|
+
console.log(
|
|
40
|
+
`[PractitionerAggregationService] Adding practitioner ${practitionerId} to clinic ${clinicId}.`
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const clinicRef = this.db.collection(CLINICS_COLLECTION).doc(clinicId);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await clinicRef.update({
|
|
47
|
+
doctors: admin.firestore.FieldValue.arrayUnion(practitionerId),
|
|
48
|
+
doctorsInfo: admin.firestore.FieldValue.arrayUnion(doctorInfo),
|
|
49
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log(
|
|
53
|
+
`[PractitionerAggregationService] Successfully added practitioner ${practitionerId} to clinic ${clinicId}.`
|
|
54
|
+
);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(
|
|
57
|
+
`[PractitionerAggregationService] Error adding practitioner ${practitionerId} to clinic ${clinicId}:`,
|
|
58
|
+
error
|
|
59
|
+
);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Updates practitioner information in associated clinics
|
|
66
|
+
* @param clinicIds - IDs of clinics associated with the practitioner
|
|
67
|
+
* @param doctorInfo - Updated doctor information
|
|
68
|
+
* @returns {Promise<void>}
|
|
69
|
+
*/
|
|
70
|
+
async updatePractitionerInfoInClinics(
|
|
71
|
+
clinicIds: string[],
|
|
72
|
+
doctorInfo: DoctorInfo
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
if (!clinicIds || clinicIds.length === 0 || !doctorInfo) {
|
|
75
|
+
console.log(
|
|
76
|
+
"[PractitionerAggregationService] Missing clinicIds or doctorInfo for updating practitioner in clinics. Skipping."
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const batch = this.db.batch();
|
|
82
|
+
const practitionerId = doctorInfo.id;
|
|
83
|
+
|
|
84
|
+
console.log(
|
|
85
|
+
`[PractitionerAggregationService] Starting batch update of practitioner ${practitionerId} in ${clinicIds.length} clinics.`
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
for (const clinicId of clinicIds) {
|
|
89
|
+
const clinicRef = this.db.collection(CLINICS_COLLECTION).doc(clinicId);
|
|
90
|
+
|
|
91
|
+
// Remove old doctor info based on ID matcher
|
|
92
|
+
batch.update(clinicRef, {
|
|
93
|
+
doctorsInfo: admin.firestore.FieldValue.arrayRemove({
|
|
94
|
+
id: practitionerId,
|
|
95
|
+
}),
|
|
96
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
97
|
+
});
|
|
98
|
+
// Add updated doctor info
|
|
99
|
+
batch.update(clinicRef, {
|
|
100
|
+
doctorsInfo: admin.firestore.FieldValue.arrayUnion(doctorInfo),
|
|
101
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await batch.commit();
|
|
107
|
+
console.log(
|
|
108
|
+
`[PractitionerAggregationService] Successfully updated practitioner ${practitionerId} info in ${clinicIds.length} clinics.`
|
|
109
|
+
);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(
|
|
112
|
+
`[PractitionerAggregationService] Error updating practitioner ${practitionerId} info in clinics:`,
|
|
113
|
+
error
|
|
114
|
+
);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Updates practitioner information in associated procedures
|
|
121
|
+
* @param procedureIds - IDs of procedures associated with the practitioner
|
|
122
|
+
* @param doctorInfo - Updated doctor information
|
|
123
|
+
* @returns {Promise<void>}
|
|
124
|
+
*/
|
|
125
|
+
async updatePractitionerInfoInProcedures(
|
|
126
|
+
procedureIds: string[],
|
|
127
|
+
doctorInfo: DoctorInfo
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
if (!procedureIds || procedureIds.length === 0 || !doctorInfo) {
|
|
130
|
+
console.log(
|
|
131
|
+
"[PractitionerAggregationService] Missing procedureIds or doctorInfo for updating practitioner in procedures. Skipping."
|
|
132
|
+
);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const batch = this.db.batch();
|
|
137
|
+
const practitionerId = doctorInfo.id;
|
|
138
|
+
|
|
139
|
+
console.log(
|
|
140
|
+
`[PractitionerAggregationService] Starting batch update of practitioner ${practitionerId} in ${procedureIds.length} procedures.`
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
for (const procedureId of procedureIds) {
|
|
144
|
+
const procedureRef = this.db
|
|
145
|
+
.collection(PROCEDURES_COLLECTION)
|
|
146
|
+
.doc(procedureId);
|
|
147
|
+
|
|
148
|
+
// Update the embedded doctorInfo object directly
|
|
149
|
+
batch.update(procedureRef, {
|
|
150
|
+
doctorInfo: doctorInfo,
|
|
151
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await batch.commit();
|
|
157
|
+
console.log(
|
|
158
|
+
`[PractitionerAggregationService] Successfully updated practitioner ${practitionerId} info in ${procedureIds.length} procedures.`
|
|
159
|
+
);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error(
|
|
162
|
+
`[PractitionerAggregationService] Error updating practitioner ${practitionerId} info in procedures:`,
|
|
163
|
+
error
|
|
164
|
+
);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Updates practitioner information in calendar events
|
|
171
|
+
* @param practitionerId - ID of the practitioner
|
|
172
|
+
* @param practitionerInfo - Updated practitioner information
|
|
173
|
+
* @returns {Promise<void>}
|
|
174
|
+
*/
|
|
175
|
+
async updatePractitionerInfoInCalendarEvents(
|
|
176
|
+
practitionerId: string,
|
|
177
|
+
practitionerInfo: DoctorInfo
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
if (!practitionerId || !practitionerInfo) {
|
|
180
|
+
console.log(
|
|
181
|
+
"[PractitionerAggregationService] Missing practitionerId or practitionerInfo for calendar update. Skipping."
|
|
182
|
+
);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.log(
|
|
187
|
+
`[PractitionerAggregationService] Querying upcoming calendar events for practitioner ${practitionerId} to update practitioner info.`
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const now = admin.firestore.Timestamp.now();
|
|
191
|
+
// Use a Collection Group query
|
|
192
|
+
const calendarEventsQuery = this.db
|
|
193
|
+
.collectionGroup(CALENDAR_SUBCOLLECTION_ID)
|
|
194
|
+
.where("practitionerId", "==", practitionerId)
|
|
195
|
+
.where("eventTime.start", ">", now);
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const snapshot = await calendarEventsQuery.get();
|
|
199
|
+
if (snapshot.empty) {
|
|
200
|
+
console.log(
|
|
201
|
+
`[PractitionerAggregationService] No upcoming calendar events found for practitioner ${practitionerId}. No doctor info updates needed.`
|
|
202
|
+
);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const batch = this.db.batch();
|
|
207
|
+
snapshot.docs.forEach((doc) => {
|
|
208
|
+
console.log(
|
|
209
|
+
`[PractitionerAggregationService] Updating practitioner info for calendar event ${doc.ref.path}`
|
|
210
|
+
);
|
|
211
|
+
batch.update(doc.ref, {
|
|
212
|
+
practitionerInfo: practitionerInfo,
|
|
213
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await batch.commit();
|
|
218
|
+
console.log(
|
|
219
|
+
`[PractitionerAggregationService] Successfully updated practitioner info in ${snapshot.size} upcoming calendar events for practitioner ${practitionerId}.`
|
|
220
|
+
);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error(
|
|
223
|
+
`[PractitionerAggregationService] Error updating practitioner info in calendar events for practitioner ${practitionerId}:`,
|
|
224
|
+
error
|
|
225
|
+
);
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Removes practitioner from clinics when a practitioner is deleted
|
|
232
|
+
* @param clinicIds - IDs of clinics associated with the practitioner
|
|
233
|
+
* @param practitionerId - ID of the deleted practitioner
|
|
234
|
+
* @returns {Promise<void>}
|
|
235
|
+
*/
|
|
236
|
+
async removePractitionerFromClinics(
|
|
237
|
+
clinicIds: string[],
|
|
238
|
+
practitionerId: string
|
|
239
|
+
): Promise<void> {
|
|
240
|
+
if (!clinicIds || clinicIds.length === 0 || !practitionerId) {
|
|
241
|
+
console.log(
|
|
242
|
+
"[PractitionerAggregationService] Missing clinicIds or practitionerId for removing practitioner from clinics. Skipping."
|
|
243
|
+
);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const batch = this.db.batch();
|
|
248
|
+
|
|
249
|
+
console.log(
|
|
250
|
+
`[PractitionerAggregationService] Starting batch removal of practitioner ${practitionerId} from ${clinicIds.length} clinics.`
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
for (const clinicId of clinicIds) {
|
|
254
|
+
const clinicRef = this.db.collection(CLINICS_COLLECTION).doc(clinicId);
|
|
255
|
+
|
|
256
|
+
// Remove doctor ID from doctors array
|
|
257
|
+
batch.update(clinicRef, {
|
|
258
|
+
doctors: admin.firestore.FieldValue.arrayRemove(practitionerId),
|
|
259
|
+
// Remove all doctor info objects where id matches the practitioner ID
|
|
260
|
+
doctorsInfo: admin.firestore.FieldValue.arrayRemove({
|
|
261
|
+
id: practitionerId,
|
|
262
|
+
}),
|
|
263
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
await batch.commit();
|
|
269
|
+
console.log(
|
|
270
|
+
`[PractitionerAggregationService] Successfully removed practitioner ${practitionerId} from ${clinicIds.length} clinics.`
|
|
271
|
+
);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error(
|
|
274
|
+
`[PractitionerAggregationService] Error removing practitioner ${practitionerId} from clinics:`,
|
|
275
|
+
error
|
|
276
|
+
);
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Cancels all upcoming calendar events for a deleted practitioner
|
|
283
|
+
* @param practitionerId - ID of the deleted practitioner
|
|
284
|
+
* @returns {Promise<void>}
|
|
285
|
+
*/
|
|
286
|
+
async cancelUpcomingCalendarEventsForPractitioner(
|
|
287
|
+
practitionerId: string
|
|
288
|
+
): Promise<void> {
|
|
289
|
+
if (!practitionerId) {
|
|
290
|
+
console.log(
|
|
291
|
+
"[PractitionerAggregationService] Missing practitionerId for canceling calendar events. Skipping."
|
|
292
|
+
);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log(
|
|
297
|
+
`[PractitionerAggregationService] Querying upcoming calendar events for practitioner ${practitionerId} to cancel.`
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const now = admin.firestore.Timestamp.now();
|
|
301
|
+
// Use a Collection Group query
|
|
302
|
+
const calendarEventsQuery = this.db
|
|
303
|
+
.collectionGroup(CALENDAR_SUBCOLLECTION_ID)
|
|
304
|
+
.where("practitionerId", "==", practitionerId)
|
|
305
|
+
.where("eventTime.start", ">", now);
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const snapshot = await calendarEventsQuery.get();
|
|
309
|
+
if (snapshot.empty) {
|
|
310
|
+
console.log(
|
|
311
|
+
`[PractitionerAggregationService] No upcoming calendar events found for practitioner ${practitionerId}. No events to cancel.`
|
|
312
|
+
);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const batch = this.db.batch();
|
|
317
|
+
snapshot.docs.forEach((doc) => {
|
|
318
|
+
console.log(
|
|
319
|
+
`[PractitionerAggregationService] Canceling calendar event ${doc.ref.path}`
|
|
320
|
+
);
|
|
321
|
+
batch.update(doc.ref, {
|
|
322
|
+
status: "CANCELED",
|
|
323
|
+
cancelReason: "Practitioner deleted",
|
|
324
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
await batch.commit();
|
|
329
|
+
console.log(
|
|
330
|
+
`[PractitionerAggregationService] Successfully canceled ${snapshot.size} upcoming calendar events for practitioner ${practitionerId}.`
|
|
331
|
+
);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error(
|
|
334
|
+
`[PractitionerAggregationService] Error canceling calendar events for practitioner ${practitionerId}:`,
|
|
335
|
+
error
|
|
336
|
+
);
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Removes practitioner from patients when a practitioner is deleted
|
|
343
|
+
* @param patientIds - IDs of patients associated with the practitioner
|
|
344
|
+
* @param practitionerId - ID of the deleted practitioner
|
|
345
|
+
* @returns {Promise<void>}
|
|
346
|
+
*/
|
|
347
|
+
async removePractitionerFromPatients(
|
|
348
|
+
patientIds: string[],
|
|
349
|
+
practitionerId: string
|
|
350
|
+
): Promise<void> {
|
|
351
|
+
if (!patientIds || patientIds.length === 0 || !practitionerId) {
|
|
352
|
+
console.log(
|
|
353
|
+
"[PractitionerAggregationService] Missing patientIds or practitionerId for removing practitioner from patients. Skipping."
|
|
354
|
+
);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const batch = this.db.batch();
|
|
359
|
+
|
|
360
|
+
console.log(
|
|
361
|
+
`[PractitionerAggregationService] Starting batch removal of practitioner ${practitionerId} from ${patientIds.length} patients.`
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
for (const patientId of patientIds) {
|
|
365
|
+
const patientRef = this.db.collection(PATIENTS_COLLECTION).doc(patientId);
|
|
366
|
+
|
|
367
|
+
// Remove doctor ID from doctorIds array
|
|
368
|
+
batch.update(patientRef, {
|
|
369
|
+
doctorIds: admin.firestore.FieldValue.arrayRemove(practitionerId),
|
|
370
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
await batch.commit();
|
|
376
|
+
console.log(
|
|
377
|
+
`[PractitionerAggregationService] Successfully removed practitioner ${practitionerId} from ${patientIds.length} patients.`
|
|
378
|
+
);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.error(
|
|
381
|
+
`[PractitionerAggregationService] Error removing practitioner ${practitionerId} from patients:`,
|
|
382
|
+
error
|
|
383
|
+
);
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Inactivates all procedures associated with a deleted practitioner
|
|
390
|
+
* @param procedureIds - IDs of procedures provided by the practitioner
|
|
391
|
+
* @returns {Promise<void>}
|
|
392
|
+
*/
|
|
393
|
+
async inactivateProceduresForPractitioner(
|
|
394
|
+
procedureIds: string[]
|
|
395
|
+
): Promise<void> {
|
|
396
|
+
if (!procedureIds || procedureIds.length === 0) {
|
|
397
|
+
console.log(
|
|
398
|
+
"[PractitionerAggregationService] No procedure IDs provided for inactivation. Skipping."
|
|
399
|
+
);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const batch = this.db.batch();
|
|
404
|
+
|
|
405
|
+
console.log(
|
|
406
|
+
`[PractitionerAggregationService] Starting inactivation of ${procedureIds.length} procedures.`
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
for (const procedureId of procedureIds) {
|
|
410
|
+
const procedureRef = this.db
|
|
411
|
+
.collection(PROCEDURES_COLLECTION)
|
|
412
|
+
.doc(procedureId);
|
|
413
|
+
|
|
414
|
+
batch.update(procedureRef, {
|
|
415
|
+
isActive: false,
|
|
416
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
await batch.commit();
|
|
422
|
+
console.log(
|
|
423
|
+
`[PractitionerAggregationService] Successfully inactivated ${procedureIds.length} procedures.`
|
|
424
|
+
);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
console.error(
|
|
427
|
+
`[PractitionerAggregationService] Error committing batch inactivation of procedures:`,
|
|
428
|
+
error
|
|
429
|
+
);
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Procedure Aggregation Service (Admin)
|
|
2
|
+
|
|
3
|
+
This service centralizes the logic for updating aggregated data in various Firestore collections _when a procedure is created, updated, or deleted/inactivated_. It is designed primarily for use by Cloud Functions triggered by changes in the `procedures` collection.
|
|
4
|
+
|
|
5
|
+
## `ProcedureAggregationService` Class
|
|
6
|
+
|
|
7
|
+
### Constructor
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
constructor(firestore?: admin.firestore.Firestore)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Initializes the service with an Admin Firestore instance. Uses the default instance if none is provided.
|
|
14
|
+
|
|
15
|
+
### Methods Triggered by Procedure **Creation**
|
|
16
|
+
|
|
17
|
+
- **`addProcedureToPractitioner(practitionerId: string, procedureSummary: ProcedureSummaryInfo): Promise<void>`**
|
|
18
|
+
- Adds the `procedureId` (from `procedureSummary.id`) and the `procedureSummary` object to the `procedureIds` and `proceduresInfo` arrays respectively in the specified `Practitioner` document.
|
|
19
|
+
- **`addProcedureToClinic(clinicId: string, procedureSummary: ProcedureSummaryInfo): Promise<void>`**
|
|
20
|
+
- Adds the `procedureId` (from `procedureSummary.id`) and the `procedureSummary` object to the `procedures` and `proceduresInfo` arrays respectively in the specified `Clinic` document.
|
|
21
|
+
|
|
22
|
+
### Methods Triggered by Procedure **Update**
|
|
23
|
+
|
|
24
|
+
- **`updateProcedureInfoInPractitioner(practitionerId: string, procedureSummary: ProcedureSummaryInfo): Promise<void>`**
|
|
25
|
+
- Updates the `proceduresInfo` array within the specified `Practitioner` document.
|
|
26
|
+
- Uses a transaction to read the existing array, filter out the old summary (matching by `id`), add the updated `procedureSummary`, and write the modified array back. This prevents race conditions.
|
|
27
|
+
- **`updateProcedureInfoInClinic(clinicId: string, procedureSummary: ProcedureSummaryInfo): Promise<void>`**
|
|
28
|
+
- Updates the `proceduresInfo` array within the specified `Clinic` document using the same transaction-based read-modify-write pattern as above.
|
|
29
|
+
- **`updateProcedureInfoInCalendarEvents(procedureId: string, procedureInfo: any): Promise<void>`**
|
|
30
|
+
- Uses a Collection Group query (`calendar` subcollection) to find all _upcoming_ calendar events associated with the `procedureId`.
|
|
31
|
+
- Updates the embedded `procedureInfo` object in these calendar events with the provided `procedureInfo`.
|
|
32
|
+
|
|
33
|
+
### Methods Triggered by Procedure **Deletion / Deactivation**
|
|
34
|
+
|
|
35
|
+
- **`cancelUpcomingCalendarEventsForProcedure(procedureId: string): Promise<void>`**
|
|
36
|
+
- Uses a Collection Group query (`calendar` subcollection) to find all _upcoming_ calendar events associated with the `procedureId`.
|
|
37
|
+
- Sets the `status` of these events to `CANCELED` and adds a `cancelReason` ("Procedure deleted or inactivated").
|
|
38
|
+
- **`removeProcedureFromPractitioner(practitionerId: string, procedureId: string): Promise<void>`**
|
|
39
|
+
- Removes the `procedureId` from the `procedureIds` array and the corresponding `ProcedureSummaryInfo` object (matching by `id`) from the `proceduresInfo` array in the specified `Practitioner` document.
|
|
40
|
+
- Uses a transaction-based read-modify-write pattern.
|
|
41
|
+
- **`removeProcedureFromClinic(clinicId: string, procedureId: string): Promise<void>`**
|
|
42
|
+
- Removes the `procedureId` from the `procedures` array and the corresponding `ProcedureSummaryInfo` object (matching by `id`) from the `proceduresInfo` array in the specified `Clinic` document.
|
|
43
|
+
- Uses a transaction-based read-modify-write pattern.
|