@blackcode_sa/metaestetics-api 1.6.4 → 1.6.5

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.
@@ -1,4 +1,5 @@
1
1
  import * as admin from "firebase-admin";
2
+ import { Timestamp as FirebaseClientTimestamp } from "@firebase/firestore";
2
3
  import {
3
4
  BookingAvailabilityCalculator,
4
5
  BookingAvailabilityRequest,
@@ -6,9 +7,57 @@ import {
6
7
  AvailableSlot,
7
8
  } from "./";
8
9
  import { CalendarEventStatus, CalendarEventType } from "../../types/calendar";
9
- import { Clinic } from "../../types/clinic";
10
- import { Practitioner } from "../../types/practitioner";
11
- import { Procedure } from "../../types/procedure";
10
+ import {
11
+ Clinic,
12
+ CLINICS_COLLECTION,
13
+ ClinicGroup,
14
+ CLINIC_GROUPS_COLLECTION,
15
+ } from "../../types/clinic";
16
+ import {
17
+ Practitioner,
18
+ PRACTITIONERS_COLLECTION,
19
+ } from "../../types/practitioner";
20
+ import {
21
+ Procedure,
22
+ PROCEDURES_COLLECTION,
23
+ ProcedureSummaryInfo,
24
+ } from "../../types/procedure";
25
+ import {
26
+ PatientProfile,
27
+ PATIENTS_COLLECTION,
28
+ PatientSensitiveInfo,
29
+ PATIENT_SENSITIVE_INFO_COLLECTION,
30
+ Gender,
31
+ } from "../../types/patient";
32
+ import {
33
+ Appointment,
34
+ AppointmentStatus,
35
+ PaymentStatus,
36
+ APPOINTMENTS_COLLECTION,
37
+ ProcedureExtendedInfo,
38
+ } from "../../types/appointment";
39
+ import { Currency } from "../../backoffice/types/static/pricing.types";
40
+ import { DocumentTemplate } from "../../types/documentation-templates";
41
+ import {
42
+ ClinicInfo,
43
+ PractitionerProfileInfo,
44
+ PatientProfileInfo,
45
+ } from "../../types/profile";
46
+ import { Category } from "../../backoffice/types/category.types";
47
+ import { Subcategory } from "../../backoffice/types/subcategory.types";
48
+ import { Technology } from "../../backoffice/types/technology.types";
49
+ import { Product } from "../../backoffice/types/product.types";
50
+
51
+ /**
52
+ * Interface for the data required by orchestrateAppointmentCreation
53
+ */
54
+ export interface OrchestrateAppointmentCreationData {
55
+ patientId: string;
56
+ procedureId: string;
57
+ appointmentStartTime: admin.firestore.Timestamp;
58
+ appointmentEndTime: admin.firestore.Timestamp;
59
+ patientNotes?: string | null;
60
+ }
12
61
 
13
62
  /**
14
63
  * Admin service for handling booking-related operations.
@@ -231,4 +280,328 @@ export class BookingAdmin {
231
280
  return [];
232
281
  }
233
282
  }
283
+
284
+ /**
285
+ * Orchestrates the creation of a new appointment, including data aggregation.
286
+ * This method is intended to be called from a trusted backend environment (e.g., an Express route handler in a Cloud Function).
287
+ *
288
+ * @param data - Data required to create the appointment.
289
+ * @param authenticatedUserId - The ID of the user making the request (for auditing, and usually is the patientId).
290
+ * @returns Promise resolving to an object indicating success, and appointmentId or an error message.
291
+ */
292
+ async orchestrateAppointmentCreation(
293
+ data: OrchestrateAppointmentCreationData,
294
+ authenticatedUserId: string
295
+ ): Promise<{
296
+ success: boolean;
297
+ appointmentId?: string;
298
+ appointmentData?: Appointment;
299
+ error?: string;
300
+ }> {
301
+ console.log(
302
+ `[BookingAdmin] Orchestrating appointment creation for patient ${data.patientId} by user ${authenticatedUserId}`
303
+ );
304
+
305
+ try {
306
+ // --- 1. Input Validation ---
307
+ if (
308
+ !data.patientId ||
309
+ !data.procedureId ||
310
+ !data.appointmentStartTime ||
311
+ !data.appointmentEndTime
312
+ ) {
313
+ return {
314
+ success: false,
315
+ error:
316
+ "Missing required fields: patientId, procedureId, appointmentStartTime, or appointmentEndTime.",
317
+ };
318
+ }
319
+ if (
320
+ data.appointmentEndTime.toMillis() <=
321
+ data.appointmentStartTime.toMillis()
322
+ ) {
323
+ return {
324
+ success: false,
325
+ error: "Appointment end time must be after start time.",
326
+ };
327
+ }
328
+ if (authenticatedUserId !== data.patientId) {
329
+ console.warn(
330
+ `[BookingAdmin] Authenticated user ${authenticatedUserId} is booking for a different patient ${data.patientId}. Review authorization if this is not intended.`
331
+ );
332
+ }
333
+
334
+ // --- 2. Fetch Core Procedure Data ---
335
+ const procedureRef = this.db
336
+ .collection(PROCEDURES_COLLECTION)
337
+ .doc(data.procedureId);
338
+ const procedureDoc = await procedureRef.get();
339
+ if (!procedureDoc.exists) {
340
+ return {
341
+ success: false,
342
+ error: `Procedure ${data.procedureId} not found.`,
343
+ };
344
+ }
345
+ const procedure = procedureDoc.data() as Procedure;
346
+
347
+ // --- 3. Fetch Clinic and then other Primary Documents ---
348
+ // Fetch clinic first to get its clinicGroupId
349
+ const clinicRef = this.db
350
+ .collection(CLINICS_COLLECTION)
351
+ .doc(procedure.clinicBranchId);
352
+ const clinicSnap = await clinicRef.get(); // Await here directly
353
+ if (!clinicSnap.exists) {
354
+ return {
355
+ success: false,
356
+ error: `Clinic ${procedure.clinicBranchId} not found.`,
357
+ };
358
+ }
359
+ const clinicData = clinicSnap.data() as Clinic; // Now clinicData is available
360
+
361
+ // Define other refs using clinicData for clinicGroupRef
362
+ const practitionerRef = this.db
363
+ .collection(PRACTITIONERS_COLLECTION)
364
+ .doc(procedure.practitionerId);
365
+ const patientProfileRef = this.db
366
+ .collection(PATIENTS_COLLECTION)
367
+ .doc(data.patientId);
368
+ const patientSensitiveRef = this.db
369
+ .collection(PATIENTS_COLLECTION)
370
+ .doc(data.patientId)
371
+ .collection(PATIENT_SENSITIVE_INFO_COLLECTION)
372
+ .doc(data.patientId);
373
+ const clinicGroupRef = this.db
374
+ .collection(CLINIC_GROUPS_COLLECTION)
375
+ .doc(clinicData.clinicGroupId); // Use clinicData here
376
+
377
+ const [
378
+ practitionerSnap,
379
+ patientProfileSnap,
380
+ patientSensitiveSnap,
381
+ clinicGroupSnap,
382
+ ] = await Promise.all([
383
+ // clinicRef.get() is already done via clinicSnap
384
+ practitionerRef.get(),
385
+ patientProfileRef.get(),
386
+ patientSensitiveRef.get(),
387
+ clinicGroupRef.get(), // Fetch the defined clinicGroupRef
388
+ ]);
389
+
390
+ if (!practitionerSnap.exists)
391
+ return {
392
+ success: false,
393
+ error: `Practitioner ${procedure.practitionerId} not found.`,
394
+ };
395
+ if (!patientProfileSnap.exists)
396
+ return {
397
+ success: false,
398
+ error: `PatientProfile ${data.patientId} not found.`,
399
+ };
400
+ if (!clinicGroupSnap.exists)
401
+ return {
402
+ success: false,
403
+ error: `ClinicGroup for clinic ${procedure.clinicBranchId} not found.`,
404
+ };
405
+
406
+ const practitionerData = practitionerSnap.data() as Practitioner;
407
+ const patientProfileData = patientProfileSnap.data() as PatientProfile;
408
+ const patientSensitiveData = patientSensitiveSnap.exists
409
+ ? (patientSensitiveSnap.data() as PatientSensitiveInfo)
410
+ : undefined;
411
+ const clinicGroupData = clinicGroupSnap.data() as ClinicGroup;
412
+
413
+ // --- 4. Determine initialStatus (based on clinic settings) ---
414
+ const autoConfirm = clinicGroupData.autoConfirmAppointments || false;
415
+ const initialStatus = autoConfirm
416
+ ? AppointmentStatus.CONFIRMED
417
+ : AppointmentStatus.PENDING;
418
+
419
+ // --- 5. Aggregate Information (Snapshots) ---
420
+ const clinicInfo: ClinicInfo = {
421
+ id: clinicSnap.id,
422
+ name: clinicData.name,
423
+ featuredPhoto: clinicData.coverPhoto || clinicData.logo || "",
424
+ description: clinicData.description,
425
+ location: clinicData.location,
426
+ contactInfo: clinicData.contactInfo,
427
+ };
428
+ const practitionerInfo: PractitionerProfileInfo = {
429
+ id: practitionerSnap.id,
430
+ practitionerPhoto: practitionerData.basicInfo.profileImageUrl || null,
431
+ name: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
432
+ email: practitionerData.basicInfo.email,
433
+ phone: practitionerData.basicInfo.phoneNumber || null,
434
+ certification: practitionerData.certification,
435
+ };
436
+ const patientInfo: PatientProfileInfo = {
437
+ id: patientProfileSnap.id,
438
+ fullName:
439
+ `${patientSensitiveData?.firstName || ""} ${
440
+ patientSensitiveData?.lastName || ""
441
+ }`.trim() || patientProfileData.displayName,
442
+ email: patientSensitiveData?.email || "",
443
+ phone:
444
+ patientSensitiveData?.phoneNumber ||
445
+ patientProfileData.phoneNumber ||
446
+ null,
447
+ dateOfBirth:
448
+ patientSensitiveData?.dateOfBirth ||
449
+ patientProfileData.dateOfBirth ||
450
+ admin.firestore.Timestamp.now(),
451
+ gender: patientSensitiveData?.gender || Gender.OTHER,
452
+ };
453
+ const procedureCategory = procedure.category as Category;
454
+ const procedureSubCategory = procedure.subcategory as Subcategory;
455
+ const procedureTechnology = procedure.technology as Technology;
456
+ const procedureProduct = procedure.product as Product;
457
+
458
+ const procedureInfo: ProcedureSummaryInfo = {
459
+ id: procedure.id,
460
+ name: procedure.name,
461
+ description: procedure.description,
462
+ family: procedure.family,
463
+ categoryName: procedureCategory?.name || "",
464
+ subcategoryName: procedureSubCategory?.name || "",
465
+ technologyName: procedureTechnology?.name || "",
466
+ price: procedure.price,
467
+ pricingMeasure: procedure.pricingMeasure,
468
+ currency: procedure.currency,
469
+ duration: procedure.duration,
470
+ clinicId: procedure.clinicBranchId,
471
+ clinicName: clinicData.name,
472
+ practitionerId: procedure.practitionerId,
473
+ practitionerName: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
474
+ photo: procedure.photos?.[0] || "",
475
+ brandName: procedureProduct?.brandName || "",
476
+ productName: procedureProduct?.name || "",
477
+ };
478
+ const procedureExtendedInfo: ProcedureExtendedInfo = {
479
+ id: procedure.id,
480
+ name: procedure.name,
481
+ description: procedure.description,
482
+ cost: procedure.price,
483
+ duration: procedure.duration,
484
+ procedureFamily: procedure.family,
485
+ procedureCategoryId: procedureCategory?.id || "",
486
+ procedureCategoryName: procedureCategory?.name || "",
487
+ procedureSubCategoryId: procedureSubCategory?.id || "",
488
+ procedureSubCategoryName: procedureSubCategory?.name || "",
489
+ procedureTechnologyId: procedureTechnology?.id || "",
490
+ procedureTechnologyName: procedureTechnology?.name || "",
491
+ procedureProductBrandId: procedureProduct.brandId || "",
492
+ procedureProductBrandName: procedureProduct.brandName || "",
493
+ procedureProductId: procedureProduct.id || "",
494
+ procedureProductName: procedureProduct.name || "",
495
+ };
496
+
497
+ // --- 6. Determine pendingUserFormsIds and linkedFormIds ---
498
+ let pendingUserFormsIds: string[] = [];
499
+ let linkedFormIds: string[] = [];
500
+ if (
501
+ procedure.documentationTemplates &&
502
+ Array.isArray(procedure.documentationTemplates)
503
+ ) {
504
+ pendingUserFormsIds = procedure.documentationTemplates
505
+ .filter(
506
+ (template: DocumentTemplate) =>
507
+ template.isUserForm && template.isRequired
508
+ )
509
+ .map((template: DocumentTemplate) => template.id);
510
+ linkedFormIds = procedure.documentationTemplates.map(
511
+ (template: DocumentTemplate) => template.id
512
+ );
513
+ }
514
+
515
+ // --- 7. Construct New Appointment Object ---
516
+ const newAppointmentId = this.db
517
+ .collection(APPOINTMENTS_COLLECTION)
518
+ .doc().id;
519
+ const serverTimestampValue = admin.firestore.FieldValue.serverTimestamp();
520
+ const adminTsNow = admin.firestore.Timestamp.now();
521
+
522
+ const newAppointmentData: Appointment = {
523
+ id: newAppointmentId,
524
+ calendarEventId: "",
525
+ clinicBranchId: procedure.clinicBranchId,
526
+ clinicInfo,
527
+ practitionerId: procedure.practitionerId,
528
+ practitionerInfo,
529
+ patientId: data.patientId,
530
+ patientInfo,
531
+ procedureId: data.procedureId,
532
+ procedureInfo,
533
+ procedureExtendedInfo,
534
+ status: initialStatus,
535
+ bookingTime: serverTimestampValue as any,
536
+ confirmationTime:
537
+ initialStatus === AppointmentStatus.CONFIRMED
538
+ ? (serverTimestampValue as any)
539
+ : null,
540
+ appointmentStartTime: new FirebaseClientTimestamp(
541
+ data.appointmentStartTime.seconds,
542
+ data.appointmentStartTime.nanoseconds
543
+ ),
544
+ appointmentEndTime: new FirebaseClientTimestamp(
545
+ data.appointmentEndTime.seconds,
546
+ data.appointmentEndTime.nanoseconds
547
+ ),
548
+ cost: procedure.price,
549
+ currency: procedure.currency,
550
+ paymentStatus:
551
+ procedure.price > 0
552
+ ? PaymentStatus.UNPAID
553
+ : PaymentStatus.NOT_APPLICABLE,
554
+ patientNotes: data.patientNotes || null,
555
+ blockingConditions: procedure.blockingConditions || [],
556
+ contraindications: procedure.contraindications || [],
557
+ preProcedureRequirements: procedure.preRequirements || [],
558
+ postProcedureRequirements: procedure.postRequirements || [],
559
+ pendingUserFormsIds: pendingUserFormsIds,
560
+ completedPreRequirements: [],
561
+ completedPostRequirements: [],
562
+ linkedFormIds: linkedFormIds,
563
+ linkedForms: [],
564
+ media: [],
565
+ reviewInfo: null,
566
+ finalizedDetails: undefined,
567
+ internalNotes: null,
568
+ cancellationReason: null,
569
+ cancellationTime: null,
570
+ canceledBy: undefined,
571
+ rescheduleTime: null,
572
+ procedureActualStartTime: null,
573
+ actualDurationMinutes: undefined,
574
+ isRecurring: false,
575
+ recurringAppointmentId: null,
576
+ isArchived: false,
577
+ createdAt: serverTimestampValue as any,
578
+ updatedAt: serverTimestampValue as any,
579
+ };
580
+
581
+ // --- 8. Save New Appointment ---
582
+ await this.db
583
+ .collection(APPOINTMENTS_COLLECTION)
584
+ .doc(newAppointmentId)
585
+ .set(newAppointmentData);
586
+
587
+ console.log(
588
+ `[BookingAdmin] Appointment ${newAppointmentId} created successfully with status ${initialStatus}.`
589
+ );
590
+ return {
591
+ success: true,
592
+ appointmentId: newAppointmentId,
593
+ appointmentData: newAppointmentData,
594
+ };
595
+ } catch (error) {
596
+ console.error(
597
+ "[BookingAdmin] Critical error in orchestrateAppointmentCreation:",
598
+ error
599
+ );
600
+ const errorMessage =
601
+ error instanceof Error
602
+ ? error.message
603
+ : "Unknown server error during appointment creation.";
604
+ return { success: false, error: errorMessage };
605
+ }
606
+ }
234
607
  }
@@ -21,7 +21,9 @@ export interface Product {
21
21
  id?: string;
22
22
  name: string;
23
23
  brandId: string;
24
+ brandName: string;
24
25
  technologyId: string;
26
+ technologyName: string;
25
27
  createdAt: Date;
26
28
  updatedAt: Date;
27
29
  isActive: boolean;