@blackcode_sa/metaestetics-api 1.5.32 → 1.5.33

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.
@@ -0,0 +1,590 @@
1
+ import {
2
+ Firestore,
3
+ collection,
4
+ doc,
5
+ getDoc,
6
+ getDocs,
7
+ query,
8
+ where,
9
+ setDoc,
10
+ updateDoc,
11
+ serverTimestamp,
12
+ Timestamp,
13
+ orderBy,
14
+ limit,
15
+ startAfter,
16
+ QueryConstraint,
17
+ DocumentSnapshot,
18
+ } from "firebase/firestore";
19
+ import {
20
+ Appointment,
21
+ AppointmentStatus,
22
+ CreateAppointmentData,
23
+ UpdateAppointmentData,
24
+ APPOINTMENTS_COLLECTION,
25
+ SearchAppointmentsParams,
26
+ PaymentStatus,
27
+ } from "../../../types/appointment";
28
+ import { CalendarEvent, CALENDAR_COLLECTION } from "../../../types/calendar";
29
+ import { ProcedureSummaryInfo } from "../../../types/procedure";
30
+ import {
31
+ ClinicInfo,
32
+ PatientProfileInfo,
33
+ PractitionerProfileInfo,
34
+ } from "../../../types/profile";
35
+ import { BlockingCondition } from "../../../backoffice/types/static/blocking-condition.types";
36
+ import { Contraindication } from "../../../backoffice/types/static/contraindication.types";
37
+ import { Requirement } from "../../../backoffice/types/requirement.types";
38
+ import { PRACTITIONERS_COLLECTION } from "../../../types/practitioner";
39
+ import { CLINICS_COLLECTION } from "../../../types/clinic";
40
+ import { PATIENTS_COLLECTION } from "../../../types/patient";
41
+ import { PROCEDURES_COLLECTION } from "../../../types/procedure";
42
+ import {
43
+ Technology,
44
+ TECHNOLOGIES_COLLECTION,
45
+ } from "../../../backoffice/types/technology.types";
46
+
47
+ /**
48
+ * Fetches all the necessary information for an appointment by IDs.
49
+ *
50
+ * @param db Firestore instance
51
+ * @param clinicId Clinic ID
52
+ * @param practitionerId Practitioner ID
53
+ * @param patientId Patient ID
54
+ * @param procedureId Procedure ID
55
+ * @returns Object containing the aggregated information
56
+ */
57
+ export async function fetchAggregatedInfoUtil(
58
+ db: Firestore,
59
+ clinicId: string,
60
+ practitionerId: string,
61
+ patientId: string,
62
+ procedureId: string
63
+ ): Promise<{
64
+ clinicInfo: ClinicInfo;
65
+ practitionerInfo: PractitionerProfileInfo;
66
+ patientInfo: PatientProfileInfo;
67
+ procedureInfo: ProcedureSummaryInfo;
68
+ blockingConditions: BlockingCondition[];
69
+ contraindications: Contraindication[];
70
+ preProcedureRequirements: Requirement[];
71
+ postProcedureRequirements: Requirement[];
72
+ }> {
73
+ try {
74
+ // Fetch all data in parallel for efficiency
75
+ const [clinicDoc, practitionerDoc, patientDoc, procedureDoc] =
76
+ await Promise.all([
77
+ getDoc(doc(db, CLINICS_COLLECTION, clinicId)),
78
+ getDoc(doc(db, PRACTITIONERS_COLLECTION, practitionerId)),
79
+ getDoc(doc(db, PATIENTS_COLLECTION, patientId)),
80
+ getDoc(doc(db, PROCEDURES_COLLECTION, procedureId)),
81
+ ]);
82
+
83
+ // Check if all required entities exist
84
+ if (!clinicDoc.exists()) {
85
+ throw new Error(`Clinic with ID ${clinicId} not found`);
86
+ }
87
+ if (!practitionerDoc.exists()) {
88
+ throw new Error(`Practitioner with ID ${practitionerId} not found`);
89
+ }
90
+ if (!patientDoc.exists()) {
91
+ throw new Error(`Patient with ID ${patientId} not found`);
92
+ }
93
+ if (!procedureDoc.exists()) {
94
+ throw new Error(`Procedure with ID ${procedureId} not found`);
95
+ }
96
+
97
+ const clinicData = clinicDoc.data();
98
+ const practitionerData = practitionerDoc.data();
99
+ const patientData = patientDoc.data();
100
+ const procedureData = procedureDoc.data();
101
+
102
+ // Extract relevant info for ClinicInfo
103
+ const clinicInfo: ClinicInfo = {
104
+ id: clinicId,
105
+ featuredPhoto: clinicData.featuredPhotos?.[0] || "",
106
+ name: clinicData.name,
107
+ description: clinicData.description || null,
108
+ location: clinicData.location,
109
+ contactInfo: clinicData.contactInfo,
110
+ };
111
+
112
+ // Extract relevant info for PractitionerProfileInfo
113
+ const practitionerInfo: PractitionerProfileInfo = {
114
+ id: practitionerId,
115
+ practitionerPhoto: practitionerData.basicInfo?.profileImageUrl || null,
116
+ name: `${practitionerData.basicInfo?.firstName || ""} ${
117
+ practitionerData.basicInfo?.lastName || ""
118
+ }`.trim(),
119
+ email: practitionerData.basicInfo?.email || "",
120
+ phone: practitionerData.basicInfo?.phoneNumber || null,
121
+ certification: practitionerData.certification,
122
+ };
123
+
124
+ // Extract relevant info for PatientProfileInfo
125
+ // Note: This may need adjustment depending on how patient data is structured
126
+ const patientInfo: PatientProfileInfo = {
127
+ id: patientId,
128
+ fullName: patientData.displayName || "",
129
+ email: patientData.email || "",
130
+ phone: patientData.phoneNumber || null,
131
+ dateOfBirth: patientData.dateOfBirth || Timestamp.now(),
132
+ gender: patientData.gender || "other",
133
+ };
134
+
135
+ // Extract procedureInfo from the procedure document
136
+ // Assuming procedureData already has a procedureInfo property or similar structure
137
+ const procedureInfo: ProcedureSummaryInfo = {
138
+ id: procedureId,
139
+ name: procedureData.name,
140
+ description: procedureData.description,
141
+ photo: procedureData.photo || "",
142
+ family: procedureData.family,
143
+ categoryName: procedureData.category?.name || "",
144
+ subcategoryName: procedureData.subcategory?.name || "",
145
+ technologyName: procedureData.technology?.name || "",
146
+ brandName: procedureData.product?.brand || "",
147
+ productName: procedureData.product?.name || "",
148
+ price: procedureData.price || 0,
149
+ pricingMeasure: procedureData.pricingMeasure,
150
+ currency: procedureData.currency,
151
+ duration: procedureData.duration || 0,
152
+ clinicId: clinicId,
153
+ clinicName: clinicInfo.name,
154
+ practitionerId: practitionerId,
155
+ practitionerName: practitionerInfo.name,
156
+ };
157
+
158
+ // Fetch the technology document to get procedure requirements
159
+ let technologyId = "";
160
+ if (procedureData.technology?.id) {
161
+ technologyId = procedureData.technology.id;
162
+ }
163
+
164
+ let blockingConditions: BlockingCondition[] = [];
165
+ let contraindications: Contraindication[] = [];
166
+ let preProcedureRequirements: Requirement[] = [];
167
+ let postProcedureRequirements: Requirement[] = [];
168
+
169
+ // If we have a technology ID, fetch its details
170
+ if (technologyId) {
171
+ const technologyDoc = await getDoc(
172
+ doc(db, TECHNOLOGIES_COLLECTION, technologyId)
173
+ );
174
+ if (technologyDoc.exists()) {
175
+ const technologyData = technologyDoc.data() as Technology;
176
+
177
+ // Extract technology-related info
178
+ blockingConditions = technologyData.blockingConditions || [];
179
+ contraindications = technologyData.contraindications || [];
180
+ preProcedureRequirements = technologyData.requirements?.pre || [];
181
+ postProcedureRequirements = technologyData.requirements?.post || [];
182
+ }
183
+ } else {
184
+ // Fallback to procedure-level data if technology not available
185
+ blockingConditions = procedureData.blockingConditions || [];
186
+ contraindications = procedureData.contraindications || [];
187
+ preProcedureRequirements = procedureData.preRequirements || [];
188
+ postProcedureRequirements = procedureData.postRequirements || [];
189
+ }
190
+
191
+ return {
192
+ clinicInfo,
193
+ practitionerInfo,
194
+ patientInfo,
195
+ procedureInfo,
196
+ blockingConditions,
197
+ contraindications,
198
+ preProcedureRequirements,
199
+ postProcedureRequirements,
200
+ };
201
+ } catch (error) {
202
+ console.error("Error fetching aggregated info:", error);
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Creates a new appointment in Firestore.
209
+ *
210
+ * @param db Firestore instance
211
+ * @param data Data needed to create the appointment
212
+ * @param aggregatedInfo Already fetched and aggregated info
213
+ * @param generateId Function to generate a unique ID
214
+ * @returns The created Appointment
215
+ */
216
+ export async function createAppointmentUtil(
217
+ db: Firestore,
218
+ data: CreateAppointmentData,
219
+ aggregatedInfo: {
220
+ clinicInfo: ClinicInfo;
221
+ practitionerInfo: PractitionerProfileInfo;
222
+ patientInfo: PatientProfileInfo;
223
+ procedureInfo: ProcedureSummaryInfo;
224
+ blockingConditions: BlockingCondition[];
225
+ contraindications: Contraindication[];
226
+ preProcedureRequirements: Requirement[];
227
+ postProcedureRequirements: Requirement[];
228
+ },
229
+ generateId: () => string
230
+ ): Promise<Appointment> {
231
+ try {
232
+ const appointmentId = generateId();
233
+
234
+ // Create appointment object
235
+ const appointment: Omit<Appointment, "createdAt" | "updatedAt"> & {
236
+ createdAt: any;
237
+ updatedAt: any;
238
+ } = {
239
+ id: appointmentId,
240
+ calendarEventId: data.calendarEventId,
241
+ clinicBranchId: data.clinicBranchId,
242
+ clinicInfo: aggregatedInfo.clinicInfo,
243
+ practitionerId: data.practitionerId,
244
+ practitionerInfo: aggregatedInfo.practitionerInfo,
245
+ patientId: data.patientId,
246
+ patientInfo: aggregatedInfo.patientInfo,
247
+ procedureId: data.procedureId,
248
+ procedureInfo: aggregatedInfo.procedureInfo,
249
+ status: data.initialStatus,
250
+ bookingTime: Timestamp.now(),
251
+ appointmentStartTime: data.appointmentStartTime,
252
+ appointmentEndTime: data.appointmentEndTime,
253
+ patientNotes: data.patientNotes || null,
254
+ cost: data.cost,
255
+ currency: data.currency,
256
+ paymentStatus: data.initialPaymentStatus || PaymentStatus.UNPAID,
257
+ blockingConditions: aggregatedInfo.blockingConditions,
258
+ contraindications: aggregatedInfo.contraindications,
259
+ preProcedureRequirements: aggregatedInfo.preProcedureRequirements,
260
+ postProcedureRequirements: aggregatedInfo.postProcedureRequirements,
261
+ completedPreRequirements: [],
262
+ completedPostRequirements: [],
263
+ createdAt: serverTimestamp(),
264
+ updatedAt: serverTimestamp(),
265
+ };
266
+
267
+ // Add additional fields for confirmation if appointment is already confirmed
268
+ if (data.initialStatus === AppointmentStatus.CONFIRMED) {
269
+ appointment.confirmationTime = Timestamp.now();
270
+ }
271
+
272
+ // Save to Firestore
273
+ await setDoc(doc(db, APPOINTMENTS_COLLECTION, appointmentId), appointment);
274
+
275
+ // Update the calendar event with the appointment ID
276
+ const calendarEventRef = doc(db, CALENDAR_COLLECTION, data.calendarEventId);
277
+ await updateDoc(calendarEventRef, {
278
+ appointmentId: appointmentId,
279
+ updatedAt: serverTimestamp(),
280
+ });
281
+
282
+ // Return the created appointment
283
+ // Convert serverTimestamp to regular Timestamp for immediate use
284
+ const now = Timestamp.now();
285
+ return {
286
+ ...appointment,
287
+ createdAt: now,
288
+ updatedAt: now,
289
+ } as Appointment;
290
+ } catch (error) {
291
+ console.error("Error creating appointment:", error);
292
+ throw error;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Updates an existing appointment in Firestore.
298
+ *
299
+ * @param db Firestore instance
300
+ * @param appointmentId ID of the appointment to update
301
+ * @param data Update data for the appointment
302
+ * @returns The updated Appointment
303
+ */
304
+ export async function updateAppointmentUtil(
305
+ db: Firestore,
306
+ appointmentId: string,
307
+ data: UpdateAppointmentData
308
+ ): Promise<Appointment> {
309
+ try {
310
+ const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
311
+ const appointmentDoc = await getDoc(appointmentRef);
312
+
313
+ if (!appointmentDoc.exists()) {
314
+ throw new Error(`Appointment with ID ${appointmentId} not found`);
315
+ }
316
+
317
+ const currentAppointment = appointmentDoc.data() as Appointment;
318
+
319
+ // Handle requirement completion tracking
320
+ let completedPreRequirements =
321
+ currentAppointment.completedPreRequirements || [];
322
+ let completedPostRequirements =
323
+ currentAppointment.completedPostRequirements || [];
324
+
325
+ if (data.completedPreRequirements) {
326
+ // Validate that all IDs exist in the pre-requirements
327
+ const validPreReqIds = currentAppointment.preProcedureRequirements.map(
328
+ (req) => req.id
329
+ );
330
+ const invalidPreReqIds = data.completedPreRequirements.filter(
331
+ (id) => !validPreReqIds.includes(id)
332
+ );
333
+
334
+ if (invalidPreReqIds.length > 0) {
335
+ throw new Error(
336
+ `Invalid pre-requirement IDs: ${invalidPreReqIds.join(", ")}`
337
+ );
338
+ }
339
+
340
+ // Update the completed pre-requirements
341
+ completedPreRequirements = [
342
+ ...new Set([
343
+ ...completedPreRequirements,
344
+ ...data.completedPreRequirements,
345
+ ]),
346
+ ];
347
+ }
348
+
349
+ if (data.completedPostRequirements) {
350
+ // Validate that all IDs exist in the post-requirements
351
+ const validPostReqIds = currentAppointment.postProcedureRequirements.map(
352
+ (req) => req.id
353
+ );
354
+ const invalidPostReqIds = data.completedPostRequirements.filter(
355
+ (id) => !validPostReqIds.includes(id)
356
+ );
357
+
358
+ if (invalidPostReqIds.length > 0) {
359
+ throw new Error(
360
+ `Invalid post-requirement IDs: ${invalidPostReqIds.join(", ")}`
361
+ );
362
+ }
363
+
364
+ // Update the completed post-requirements
365
+ completedPostRequirements = [
366
+ ...new Set([
367
+ ...completedPostRequirements,
368
+ ...data.completedPostRequirements,
369
+ ]),
370
+ ];
371
+ }
372
+
373
+ // Prepare update data
374
+ const updateData: any = {
375
+ ...data,
376
+ completedPreRequirements,
377
+ completedPostRequirements,
378
+ updatedAt: serverTimestamp(),
379
+ };
380
+
381
+ // Remove undefined fields
382
+ Object.keys(updateData).forEach((key) => {
383
+ if (updateData[key] === undefined) {
384
+ delete updateData[key];
385
+ }
386
+ });
387
+
388
+ // Handle status changes
389
+ if (data.status && data.status !== currentAppointment.status) {
390
+ // Handle confirmation
391
+ if (
392
+ data.status === AppointmentStatus.CONFIRMED &&
393
+ !updateData.confirmationTime
394
+ ) {
395
+ updateData.confirmationTime = Timestamp.now();
396
+ }
397
+
398
+ // Update the related calendar event status if needed
399
+ if (currentAppointment.calendarEventId) {
400
+ await updateCalendarEventStatus(
401
+ db,
402
+ currentAppointment.calendarEventId,
403
+ data.status
404
+ );
405
+ }
406
+ }
407
+
408
+ // Update the appointment
409
+ await updateDoc(appointmentRef, updateData);
410
+
411
+ // Fetch the updated appointment
412
+ const updatedAppointmentDoc = await getDoc(appointmentRef);
413
+ if (!updatedAppointmentDoc.exists()) {
414
+ throw new Error(
415
+ `Failed to retrieve updated appointment ${appointmentId}`
416
+ );
417
+ }
418
+
419
+ return updatedAppointmentDoc.data() as Appointment;
420
+ } catch (error) {
421
+ console.error(`Error updating appointment ${appointmentId}:`, error);
422
+ throw error;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Updates the status of a calendar event based on appointment status changes.
428
+ *
429
+ * @param db Firestore instance
430
+ * @param calendarEventId ID of the calendar event
431
+ * @param appointmentStatus New appointment status
432
+ */
433
+ async function updateCalendarEventStatus(
434
+ db: Firestore,
435
+ calendarEventId: string,
436
+ appointmentStatus: AppointmentStatus
437
+ ): Promise<void> {
438
+ try {
439
+ const calendarEventRef = doc(db, CALENDAR_COLLECTION, calendarEventId);
440
+ const calendarEventDoc = await getDoc(calendarEventRef);
441
+
442
+ if (!calendarEventDoc.exists()) {
443
+ console.warn(`Calendar event with ID ${calendarEventId} not found`);
444
+ return;
445
+ }
446
+
447
+ // Map appointment status to calendar event status
448
+ let calendarStatus;
449
+ switch (appointmentStatus) {
450
+ case AppointmentStatus.CONFIRMED:
451
+ calendarStatus = "confirmed";
452
+ break;
453
+ case AppointmentStatus.CANCELED_PATIENT:
454
+ case AppointmentStatus.CANCELED_CLINIC:
455
+ calendarStatus = "canceled";
456
+ break;
457
+ case AppointmentStatus.RESCHEDULED:
458
+ calendarStatus = "rescheduled";
459
+ break;
460
+ case AppointmentStatus.COMPLETED:
461
+ calendarStatus = "completed";
462
+ break;
463
+ default:
464
+ // For other states, don't update the calendar status
465
+ return;
466
+ }
467
+
468
+ await updateDoc(calendarEventRef, {
469
+ status: calendarStatus,
470
+ updatedAt: serverTimestamp(),
471
+ });
472
+ } catch (error) {
473
+ console.error(`Error updating calendar event ${calendarEventId}:`, error);
474
+ // Don't throw error to avoid failing the appointment update
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Gets an appointment by its ID.
480
+ *
481
+ * @param db Firestore instance
482
+ * @param appointmentId Appointment ID
483
+ * @returns The appointment or null if not found
484
+ */
485
+ export async function getAppointmentByIdUtil(
486
+ db: Firestore,
487
+ appointmentId: string
488
+ ): Promise<Appointment | null> {
489
+ try {
490
+ const appointmentDoc = await getDoc(
491
+ doc(db, APPOINTMENTS_COLLECTION, appointmentId)
492
+ );
493
+
494
+ if (!appointmentDoc.exists()) {
495
+ return null;
496
+ }
497
+
498
+ return appointmentDoc.data() as Appointment;
499
+ } catch (error) {
500
+ console.error(`Error getting appointment ${appointmentId}:`, error);
501
+ throw error;
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Searches for appointments based on various criteria.
507
+ *
508
+ * @param db Firestore instance
509
+ * @param params Search parameters
510
+ * @returns Found appointments and the last document for pagination
511
+ */
512
+ export async function searchAppointmentsUtil(
513
+ db: Firestore,
514
+ params: SearchAppointmentsParams
515
+ ): Promise<{ appointments: Appointment[]; lastDoc: DocumentSnapshot | null }> {
516
+ try {
517
+ const constraints: QueryConstraint[] = [];
518
+
519
+ // Add filters based on provided params
520
+ if (params.patientId) {
521
+ constraints.push(where("patientId", "==", params.patientId));
522
+ }
523
+
524
+ if (params.practitionerId) {
525
+ constraints.push(where("practitionerId", "==", params.practitionerId));
526
+ }
527
+
528
+ if (params.clinicBranchId) {
529
+ constraints.push(where("clinicBranchId", "==", params.clinicBranchId));
530
+ }
531
+
532
+ if (params.startDate) {
533
+ constraints.push(
534
+ where(
535
+ "appointmentStartTime",
536
+ ">=",
537
+ Timestamp.fromDate(params.startDate)
538
+ )
539
+ );
540
+ }
541
+
542
+ if (params.endDate) {
543
+ constraints.push(
544
+ where("appointmentStartTime", "<=", Timestamp.fromDate(params.endDate))
545
+ );
546
+ }
547
+
548
+ if (params.status) {
549
+ if (Array.isArray(params.status)) {
550
+ // If multiple statuses, use in operator
551
+ constraints.push(where("status", "in", params.status));
552
+ } else {
553
+ // Single status
554
+ constraints.push(where("status", "==", params.status));
555
+ }
556
+ }
557
+
558
+ // Add ordering
559
+ constraints.push(orderBy("appointmentStartTime", "asc"));
560
+
561
+ // Add pagination if specified
562
+ if (params.limit) {
563
+ constraints.push(limit(params.limit));
564
+ }
565
+
566
+ if (params.startAfter) {
567
+ constraints.push(startAfter(params.startAfter));
568
+ }
569
+
570
+ // Execute query
571
+ const q = query(collection(db, APPOINTMENTS_COLLECTION), ...constraints);
572
+ const querySnapshot = await getDocs(q);
573
+
574
+ // Extract results
575
+ const appointments = querySnapshot.docs.map(
576
+ (doc) => doc.data() as Appointment
577
+ );
578
+
579
+ // Get last document for pagination
580
+ const lastDoc =
581
+ querySnapshot.docs.length > 0
582
+ ? querySnapshot.docs[querySnapshot.docs.length - 1]
583
+ : null;
584
+
585
+ return { appointments, lastDoc };
586
+ } catch (error) {
587
+ console.error("Error searching appointments:", error);
588
+ throw error;
589
+ }
590
+ }