@blackcode_sa/metaestetics-api 1.6.4 → 1.6.6

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.
Files changed (30) hide show
  1. package/dist/admin/index.d.mts +236 -2
  2. package/dist/admin/index.d.ts +236 -2
  3. package/dist/admin/index.js +11251 -10447
  4. package/dist/admin/index.mjs +11251 -10447
  5. package/dist/backoffice/index.d.mts +2 -0
  6. package/dist/backoffice/index.d.ts +2 -0
  7. package/dist/index.d.mts +50 -77
  8. package/dist/index.d.ts +50 -77
  9. package/dist/index.js +77 -305
  10. package/dist/index.mjs +78 -306
  11. package/package.json +1 -1
  12. package/src/admin/aggregation/appointment/README.md +128 -0
  13. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1053 -0
  14. package/src/admin/booking/README.md +125 -0
  15. package/src/admin/booking/booking.admin.ts +638 -3
  16. package/src/admin/calendar/calendar.admin.service.ts +183 -0
  17. package/src/admin/documentation-templates/document-manager.admin.ts +131 -0
  18. package/src/admin/mailing/appointment/appointment.mailing.service.ts +264 -0
  19. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -0
  20. package/src/admin/mailing/base.mailing.service.ts +1 -1
  21. package/src/admin/mailing/index.ts +2 -0
  22. package/src/admin/notifications/notifications.admin.ts +397 -1
  23. package/src/backoffice/types/product.types.ts +2 -0
  24. package/src/services/appointment/appointment.service.ts +89 -182
  25. package/src/services/procedure/procedure.service.ts +1 -0
  26. package/src/types/appointment/index.ts +3 -1
  27. package/src/types/notifications/index.ts +4 -2
  28. package/src/types/procedure/index.ts +7 -0
  29. package/src/validations/appointment.schema.ts +2 -3
  30. package/src/validations/procedure.schema.ts +3 -0
@@ -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,72 @@ 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 {
41
+ DocumentTemplate as AppDocumentTemplate,
42
+ FilledDocument,
43
+ FilledDocumentStatus,
44
+ USER_FORMS_SUBCOLLECTION,
45
+ DOCTOR_FORMS_SUBCOLLECTION,
46
+ } from "../../types/documentation-templates";
47
+ import {
48
+ ClinicInfo,
49
+ PractitionerProfileInfo,
50
+ PatientProfileInfo,
51
+ } from "../../types/profile";
52
+ import { Category } from "../../backoffice/types/category.types";
53
+ import { Subcategory } from "../../backoffice/types/subcategory.types";
54
+ import { Technology } from "../../backoffice/types/technology.types";
55
+ import { Product } from "../../backoffice/types/product.types";
56
+ import {
57
+ CalendarEvent,
58
+ CalendarEventTime,
59
+ CalendarSyncStatus,
60
+ ProcedureInfo as CalendarProcedureInfo,
61
+ CALENDAR_COLLECTION,
62
+ } from "../../types/calendar";
63
+ import { DocumentManagerAdminService } from "../documentation-templates/document-manager.admin";
64
+ import { LinkedFormInfo } from "../../types/appointment";
65
+
66
+ /**
67
+ * Interface for the data required by orchestrateAppointmentCreation
68
+ */
69
+ export interface OrchestrateAppointmentCreationData {
70
+ patientId: string;
71
+ procedureId: string;
72
+ appointmentStartTime: admin.firestore.Timestamp;
73
+ appointmentEndTime: admin.firestore.Timestamp;
74
+ patientNotes?: string | null;
75
+ }
12
76
 
13
77
  /**
14
78
  * Admin service for handling booking-related operations.
@@ -16,6 +80,7 @@ import { Procedure } from "../../types/procedure";
16
80
  */
17
81
  export class BookingAdmin {
18
82
  private db: admin.firestore.Firestore;
83
+ private documentManagerAdmin: DocumentManagerAdminService;
19
84
 
20
85
  /**
21
86
  * Creates a new BookingAdmin instance
@@ -23,6 +88,7 @@ export class BookingAdmin {
23
88
  */
24
89
  constructor(firestore?: admin.firestore.Firestore) {
25
90
  this.db = firestore || admin.firestore();
91
+ this.documentManagerAdmin = new DocumentManagerAdminService(this.db);
26
92
  }
27
93
 
28
94
  /**
@@ -231,4 +297,573 @@ export class BookingAdmin {
231
297
  return [];
232
298
  }
233
299
  }
300
+
301
+ private _generateCalendarProcedureInfo(
302
+ procedure: Procedure
303
+ ): CalendarProcedureInfo {
304
+ return {
305
+ name: procedure.name,
306
+ description: procedure.description,
307
+ duration: procedure.duration, // in minutes
308
+ price: procedure.price,
309
+ currency: procedure.currency,
310
+ };
311
+ }
312
+
313
+ /**
314
+ * Orchestrates the creation of a new appointment, including data aggregation.
315
+ * This method is intended to be called from a trusted backend environment (e.g., an Express route handler in a Cloud Function).
316
+ *
317
+ * @param data - Data required to create the appointment.
318
+ * @param authenticatedUserId - The ID of the user making the request (for auditing, and usually is the patientId).
319
+ * @returns Promise resolving to an object indicating success, and appointmentId or an error message.
320
+ */
321
+ async orchestrateAppointmentCreation(
322
+ data: OrchestrateAppointmentCreationData,
323
+ authenticatedUserId: string
324
+ ): Promise<{
325
+ success: boolean;
326
+ appointmentId?: string;
327
+ appointmentData?: Appointment;
328
+ practitionerCalendarEventId?: string;
329
+ patientCalendarEventId?: string;
330
+ clinicCalendarEventId?: string;
331
+ error?: string;
332
+ }> {
333
+ console.log(
334
+ `[BookingAdmin] Orchestrating appointment creation for patient ${data.patientId} by user ${authenticatedUserId}`
335
+ );
336
+ const batch = this.db.batch();
337
+ const adminTsNow = admin.firestore.Timestamp.now();
338
+ const serverTimestampValue = admin.firestore.FieldValue.serverTimestamp();
339
+
340
+ try {
341
+ // --- 1. Input Validation ---
342
+ if (
343
+ !data.patientId ||
344
+ !data.procedureId ||
345
+ !data.appointmentStartTime ||
346
+ !data.appointmentEndTime
347
+ ) {
348
+ return {
349
+ success: false,
350
+ error:
351
+ "Missing required fields: patientId, procedureId, appointmentStartTime, or appointmentEndTime.",
352
+ };
353
+ }
354
+ if (
355
+ data.appointmentEndTime.toMillis() <=
356
+ data.appointmentStartTime.toMillis()
357
+ ) {
358
+ return {
359
+ success: false,
360
+ error: "Appointment end time must be after start time.",
361
+ };
362
+ }
363
+ if (authenticatedUserId !== data.patientId) {
364
+ console.warn(
365
+ `[BookingAdmin] Authenticated user ${authenticatedUserId} is booking for a different patient ${data.patientId}. Review authorization if this is not intended.`
366
+ );
367
+ }
368
+
369
+ // --- 2. Fetch Core Procedure Data ---
370
+ const procedureRef = this.db
371
+ .collection(PROCEDURES_COLLECTION)
372
+ .doc(data.procedureId);
373
+ const procedureDoc = await procedureRef.get();
374
+ if (!procedureDoc.exists) {
375
+ return {
376
+ success: false,
377
+ error: `Procedure ${data.procedureId} not found.`,
378
+ };
379
+ }
380
+ const procedure = procedureDoc.data() as Procedure;
381
+
382
+ // --- 3. Fetch Clinic and then other Primary Documents ---
383
+ // Fetch clinic first to get its clinicGroupId
384
+ const clinicRef = this.db
385
+ .collection(CLINICS_COLLECTION)
386
+ .doc(procedure.clinicBranchId);
387
+ const clinicSnap = await clinicRef.get(); // Await here directly
388
+ if (!clinicSnap.exists) {
389
+ return {
390
+ success: false,
391
+ error: `Clinic ${procedure.clinicBranchId} not found.`,
392
+ };
393
+ }
394
+ const clinicData = clinicSnap.data() as Clinic; // Now clinicData is available
395
+
396
+ // Define other refs using clinicData for clinicGroupRef
397
+ const practitionerRef = this.db
398
+ .collection(PRACTITIONERS_COLLECTION)
399
+ .doc(procedure.practitionerId);
400
+ const patientProfileRef = this.db
401
+ .collection(PATIENTS_COLLECTION)
402
+ .doc(data.patientId);
403
+ const patientSensitiveRef = this.db
404
+ .collection(PATIENTS_COLLECTION)
405
+ .doc(data.patientId)
406
+ .collection(PATIENT_SENSITIVE_INFO_COLLECTION)
407
+ .doc(data.patientId);
408
+ const clinicGroupRef = this.db
409
+ .collection(CLINIC_GROUPS_COLLECTION)
410
+ .doc(clinicData.clinicGroupId); // Use clinicData here
411
+
412
+ const [
413
+ practitionerSnap,
414
+ patientProfileSnap,
415
+ patientSensitiveSnap,
416
+ clinicGroupSnap,
417
+ ] = await Promise.all([
418
+ // clinicRef.get() is already done via clinicSnap
419
+ practitionerRef.get(),
420
+ patientProfileRef.get(),
421
+ patientSensitiveRef.get(),
422
+ clinicGroupRef.get(), // Fetch the defined clinicGroupRef
423
+ ]);
424
+
425
+ if (!practitionerSnap.exists)
426
+ return {
427
+ success: false,
428
+ error: `Practitioner ${procedure.practitionerId} not found.`,
429
+ };
430
+ if (!patientProfileSnap.exists)
431
+ return {
432
+ success: false,
433
+ error: `PatientProfile ${data.patientId} not found.`,
434
+ };
435
+ if (!clinicGroupSnap.exists)
436
+ return {
437
+ success: false,
438
+ error: `ClinicGroup for clinic ${procedure.clinicBranchId} not found.`,
439
+ };
440
+
441
+ const practitionerData = practitionerSnap.data() as Practitioner;
442
+ const patientProfileData = patientProfileSnap.data() as PatientProfile;
443
+ const patientSensitiveData = patientSensitiveSnap.exists
444
+ ? (patientSensitiveSnap.data() as PatientSensitiveInfo)
445
+ : undefined;
446
+ const clinicGroupData = clinicGroupSnap.data() as ClinicGroup;
447
+
448
+ // --- 4. Determine initialStatus (based on clinic settings) ---
449
+ const autoConfirm = clinicGroupData.autoConfirmAppointments || false;
450
+ const initialAppointmentStatus = autoConfirm
451
+ ? AppointmentStatus.CONFIRMED
452
+ : AppointmentStatus.PENDING;
453
+ const initialCalendarEventStatus = autoConfirm
454
+ ? CalendarEventStatus.CONFIRMED
455
+ : CalendarEventStatus.PENDING;
456
+
457
+ // --- 5. Aggregate Information (Snapshots) ---
458
+ const clinicInfo: ClinicInfo = {
459
+ id: clinicSnap.id,
460
+ name: clinicData.name,
461
+ featuredPhoto: clinicData.coverPhoto || clinicData.logo || "",
462
+ description: clinicData.description,
463
+ location: clinicData.location,
464
+ contactInfo: clinicData.contactInfo,
465
+ };
466
+ const practitionerInfo: PractitionerProfileInfo = {
467
+ id: practitionerSnap.id,
468
+ practitionerPhoto: practitionerData.basicInfo.profileImageUrl || null,
469
+ name: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
470
+ email: practitionerData.basicInfo.email,
471
+ phone: practitionerData.basicInfo.phoneNumber || null,
472
+ certification: practitionerData.certification,
473
+ };
474
+ const patientInfo: PatientProfileInfo = {
475
+ id: patientProfileSnap.id,
476
+ fullName:
477
+ `${patientSensitiveData?.firstName || ""} ${
478
+ patientSensitiveData?.lastName || ""
479
+ }`.trim() || patientProfileData.displayName,
480
+ email: patientSensitiveData?.email || "",
481
+ phone:
482
+ patientSensitiveData?.phoneNumber ||
483
+ patientProfileData.phoneNumber ||
484
+ null,
485
+ dateOfBirth:
486
+ patientSensitiveData?.dateOfBirth ||
487
+ patientProfileData.dateOfBirth ||
488
+ admin.firestore.Timestamp.now(),
489
+ gender: patientSensitiveData?.gender || Gender.OTHER,
490
+ };
491
+ const procedureCategory = procedure.category as Category;
492
+ const procedureSubCategory = procedure.subcategory as Subcategory;
493
+ const procedureTechnology = procedure.technology as Technology;
494
+ const procedureProduct = procedure.product as Product;
495
+
496
+ const procedureInfo: ProcedureSummaryInfo = {
497
+ id: procedure.id,
498
+ name: procedure.name,
499
+ description: procedure.description,
500
+ family: procedure.family,
501
+ categoryName: procedureCategory?.name || "",
502
+ subcategoryName: procedureSubCategory?.name || "",
503
+ technologyName: procedureTechnology?.name || "",
504
+ price: procedure.price,
505
+ pricingMeasure: procedure.pricingMeasure,
506
+ currency: procedure.currency,
507
+ duration: procedure.duration,
508
+ clinicId: procedure.clinicBranchId,
509
+ clinicName: clinicData.name,
510
+ practitionerId: procedure.practitionerId,
511
+ practitionerName: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
512
+ photo: procedure.photos?.[0] || "",
513
+ brandName: procedureProduct?.brandName || "",
514
+ productName: procedureProduct?.name || "",
515
+ };
516
+ const procedureExtendedInfo: ProcedureExtendedInfo = {
517
+ id: procedure.id,
518
+ name: procedure.name,
519
+ description: procedure.description,
520
+ cost: procedure.price,
521
+ duration: procedure.duration,
522
+ procedureFamily: procedure.family,
523
+ procedureCategoryId: procedureCategory?.id || "",
524
+ procedureCategoryName: procedureCategory?.name || "",
525
+ procedureSubCategoryId: procedureSubCategory?.id || "",
526
+ procedureSubCategoryName: procedureSubCategory?.name || "",
527
+ procedureTechnologyId: procedureTechnology?.id || "",
528
+ procedureTechnologyName: procedureTechnology?.name || "",
529
+ procedureProductBrandId: procedureProduct.brandId || "",
530
+ procedureProductBrandName: procedureProduct.brandName || "",
531
+ procedureProductId: procedureProduct.id || "",
532
+ procedureProductName: procedureProduct.name || "",
533
+ };
534
+
535
+ // --- 6. Determine pendingUserFormsIds and linkedFormIds ---
536
+ let pendingUserFormsIds: string[] = [];
537
+ let linkedFormIds: string[] = [];
538
+ if (
539
+ procedure.documentationTemplates &&
540
+ Array.isArray(procedure.documentationTemplates)
541
+ ) {
542
+ pendingUserFormsIds = procedure.documentationTemplates
543
+ .filter(
544
+ (template: AppDocumentTemplate) =>
545
+ template.isUserForm && template.isRequired
546
+ )
547
+ .map((template: AppDocumentTemplate) => template.id);
548
+ linkedFormIds = procedure.documentationTemplates.map(
549
+ (template: AppDocumentTemplate) => template.id
550
+ );
551
+ }
552
+
553
+ // --- 7. Construct New Appointment Object ---
554
+ const newAppointmentId = this.db
555
+ .collection(APPOINTMENTS_COLLECTION)
556
+ .doc().id;
557
+
558
+ const eventTimeForCalendarEvents: CalendarEventTime = {
559
+ start: new FirebaseClientTimestamp(
560
+ data.appointmentStartTime.seconds,
561
+ data.appointmentStartTime.nanoseconds
562
+ ),
563
+ end: new FirebaseClientTimestamp(
564
+ data.appointmentEndTime.seconds,
565
+ data.appointmentEndTime.nanoseconds
566
+ ),
567
+ };
568
+
569
+ // Practitioner Calendar Event
570
+ const practitionerCalendarEventId = this.db
571
+ .collection(PRACTITIONERS_COLLECTION)
572
+ .doc(practitionerData.id)
573
+ .collection(CALENDAR_COLLECTION)
574
+ .doc().id;
575
+ const practitionerCalendarEventData: CalendarEvent = {
576
+ id: practitionerCalendarEventId,
577
+ appointmentId: newAppointmentId,
578
+ clinicBranchId: clinicData.id,
579
+ clinicBranchInfo: clinicInfo,
580
+ practitionerProfileId: practitionerData.id,
581
+ practitionerProfileInfo: practitionerInfo,
582
+ patientProfileId: patientProfileData.id,
583
+ patientProfileInfo: patientInfo,
584
+ procedureId: procedure.id,
585
+ procedureInfo: this._generateCalendarProcedureInfo(procedure),
586
+ eventName: `Appointment: ${procedure.name} with ${patientInfo.fullName}`,
587
+ eventLocation: clinicData.location,
588
+ eventTime: eventTimeForCalendarEvents,
589
+ description: procedure.description || undefined,
590
+ status: initialCalendarEventStatus,
591
+ syncStatus: CalendarSyncStatus.INTERNAL,
592
+ eventType: CalendarEventType.APPOINTMENT,
593
+ createdAt: serverTimestampValue as any,
594
+ updatedAt: serverTimestampValue as any,
595
+ };
596
+ batch.set(
597
+ this.db
598
+ .collection(PRACTITIONERS_COLLECTION)
599
+ .doc(practitionerData.id)
600
+ .collection(CALENDAR_COLLECTION)
601
+ .doc(practitionerCalendarEventId),
602
+ practitionerCalendarEventData
603
+ );
604
+
605
+ // Patient Calendar Event
606
+ const patientCalendarEventId = this.db
607
+ .collection(PATIENTS_COLLECTION)
608
+ .doc(patientProfileData.id)
609
+ .collection(CALENDAR_COLLECTION)
610
+ .doc().id;
611
+ const patientCalendarEventData: CalendarEvent = {
612
+ id: patientCalendarEventId,
613
+ appointmentId: newAppointmentId,
614
+ clinicBranchId: clinicData.id,
615
+ clinicBranchInfo: clinicInfo,
616
+ practitionerProfileId: practitionerData.id,
617
+ practitionerProfileInfo: practitionerInfo,
618
+ procedureId: procedure.id,
619
+ procedureInfo: this._generateCalendarProcedureInfo(procedure),
620
+ eventName: `Appointment: ${procedure.name} at ${clinicData.name}`,
621
+ eventLocation: clinicData.location,
622
+ eventTime: eventTimeForCalendarEvents,
623
+ description: data.patientNotes || undefined,
624
+ status: initialCalendarEventStatus,
625
+ syncStatus: CalendarSyncStatus.INTERNAL,
626
+ eventType: CalendarEventType.APPOINTMENT,
627
+ createdAt: serverTimestampValue as any,
628
+ updatedAt: serverTimestampValue as any,
629
+ };
630
+ batch.set(
631
+ this.db
632
+ .collection(PATIENTS_COLLECTION)
633
+ .doc(patientProfileData.id)
634
+ .collection(CALENDAR_COLLECTION)
635
+ .doc(patientCalendarEventId),
636
+ patientCalendarEventData
637
+ );
638
+
639
+ // Clinic Calendar Event
640
+ const clinicCalendarEventId = this.db
641
+ .collection(CLINICS_COLLECTION)
642
+ .doc(clinicData.id)
643
+ .collection(CALENDAR_COLLECTION)
644
+ .doc().id;
645
+ const clinicCalendarEventData: CalendarEvent = {
646
+ id: clinicCalendarEventId,
647
+ appointmentId: newAppointmentId,
648
+ clinicBranchId: clinicData.id,
649
+ clinicBranchInfo: clinicInfo,
650
+ practitionerProfileId: practitionerData.id,
651
+ practitionerProfileInfo: practitionerInfo,
652
+ patientProfileId: patientProfileData.id,
653
+ patientProfileInfo: patientInfo,
654
+ procedureId: procedure.id,
655
+ procedureInfo: this._generateCalendarProcedureInfo(procedure),
656
+ eventName: `Appointment: ${procedure.name} for ${patientInfo.fullName} with ${practitionerInfo.name}`,
657
+ eventLocation: clinicData.location,
658
+ eventTime: eventTimeForCalendarEvents,
659
+ description: data.patientNotes || undefined,
660
+ status: initialCalendarEventStatus,
661
+ syncStatus: CalendarSyncStatus.INTERNAL,
662
+ eventType: CalendarEventType.APPOINTMENT,
663
+ createdAt: serverTimestampValue as any,
664
+ updatedAt: serverTimestampValue as any,
665
+ };
666
+ batch.set(
667
+ this.db
668
+ .collection(CLINICS_COLLECTION)
669
+ .doc(clinicData.id)
670
+ .collection(CALENDAR_COLLECTION)
671
+ .doc(clinicCalendarEventId),
672
+ clinicCalendarEventData
673
+ );
674
+
675
+ // --- Initialize Pending/Draft Filled Documents and get form IDs ---
676
+ let initializedFormsInfo: LinkedFormInfo[] = [];
677
+ let pendingUserFormTemplateIds: string[] = [];
678
+ let allLinkedFormTemplateIds: string[] = [];
679
+
680
+ if (
681
+ procedure.documentationTemplates &&
682
+ Array.isArray(procedure.documentationTemplates) &&
683
+ procedure.documentationTemplates.length > 0
684
+ ) {
685
+ const formInitResult =
686
+ this.documentManagerAdmin.batchInitializeAppointmentForms(
687
+ batch,
688
+ newAppointmentId,
689
+ procedure.id, // Pass the actual procedureId for the forms
690
+ procedure.documentationTemplates as AppDocumentTemplate[],
691
+ data.patientId,
692
+ procedure.practitionerId,
693
+ procedure.clinicBranchId,
694
+ adminTsNow.toMillis()
695
+ );
696
+ initializedFormsInfo = formInitResult.initializedFormsInfo;
697
+ pendingUserFormTemplateIds = formInitResult.pendingUserFormsIds;
698
+ allLinkedFormTemplateIds = formInitResult.allLinkedTemplateIds;
699
+ }
700
+
701
+ // --- Construct Appointment Object ---
702
+ const newAppointmentData: Appointment = {
703
+ id: newAppointmentId,
704
+ calendarEventId: practitionerCalendarEventId,
705
+ clinicBranchId: procedure.clinicBranchId,
706
+ clinicInfo,
707
+ practitionerId: procedure.practitionerId,
708
+ practitionerInfo,
709
+ patientId: data.patientId,
710
+ patientInfo,
711
+ procedureId: data.procedureId,
712
+ procedureInfo: this._generateProcedureSummaryInfo(
713
+ procedure,
714
+ clinicData,
715
+ practitionerData
716
+ ),
717
+ procedureExtendedInfo: this._generateProcedureExtendedInfo(procedure),
718
+ status: initialAppointmentStatus,
719
+ bookingTime: new FirebaseClientTimestamp(
720
+ adminTsNow.seconds,
721
+ adminTsNow.nanoseconds
722
+ ),
723
+ confirmationTime:
724
+ initialAppointmentStatus === AppointmentStatus.CONFIRMED
725
+ ? new FirebaseClientTimestamp(
726
+ adminTsNow.seconds,
727
+ adminTsNow.nanoseconds
728
+ )
729
+ : null,
730
+ appointmentStartTime: new FirebaseClientTimestamp(
731
+ data.appointmentStartTime.seconds,
732
+ data.appointmentStartTime.nanoseconds
733
+ ),
734
+ appointmentEndTime: new FirebaseClientTimestamp(
735
+ data.appointmentEndTime.seconds,
736
+ data.appointmentEndTime.nanoseconds
737
+ ),
738
+ cost: procedure.price,
739
+ currency: procedure.currency,
740
+ paymentStatus:
741
+ procedure.price > 0
742
+ ? PaymentStatus.UNPAID
743
+ : PaymentStatus.NOT_APPLICABLE,
744
+ patientNotes: data.patientNotes || null,
745
+ blockingConditions: procedure.blockingConditions || [],
746
+ contraindications: (procedure as any).contraindications || [],
747
+ preProcedureRequirements: procedure.preRequirements || [],
748
+ postProcedureRequirements: procedure.postRequirements || [],
749
+ pendingUserFormsIds: pendingUserFormTemplateIds,
750
+ linkedFormIds: allLinkedFormTemplateIds,
751
+ completedPreRequirements: [],
752
+ completedPostRequirements: [],
753
+ linkedForms: initializedFormsInfo,
754
+ media: [],
755
+ reviewInfo: null,
756
+ finalizedDetails: undefined,
757
+ internalNotes: null,
758
+ cancellationReason: null,
759
+ cancellationTime: null,
760
+ canceledBy: undefined,
761
+ rescheduleTime: null,
762
+ procedureActualStartTime: null,
763
+ actualDurationMinutes: undefined,
764
+ isRecurring: false,
765
+ recurringAppointmentId: null,
766
+ isArchived: false,
767
+ createdAt: new FirebaseClientTimestamp(
768
+ adminTsNow.seconds,
769
+ adminTsNow.nanoseconds
770
+ ),
771
+ updatedAt: new FirebaseClientTimestamp(
772
+ adminTsNow.seconds,
773
+ adminTsNow.nanoseconds
774
+ ),
775
+ };
776
+
777
+ batch.set(
778
+ this.db.collection(APPOINTMENTS_COLLECTION).doc(newAppointmentId),
779
+ newAppointmentData
780
+ );
781
+
782
+ // Commit Batch
783
+ await batch.commit();
784
+
785
+ console.log(
786
+ `[BookingAdmin] Appointment ${newAppointmentId} and associated calendar events created successfully.`
787
+ );
788
+ return {
789
+ success: true,
790
+ appointmentId: newAppointmentId,
791
+ appointmentData: newAppointmentData,
792
+ practitionerCalendarEventId,
793
+ patientCalendarEventId,
794
+ clinicCalendarEventId,
795
+ };
796
+ } catch (error) {
797
+ console.error(
798
+ "[BookingAdmin] Critical error in orchestrateAppointmentCreation:",
799
+ error
800
+ );
801
+ const errorMessage =
802
+ error instanceof Error
803
+ ? error.message
804
+ : "Unknown server error during appointment creation.";
805
+ return { success: false, error: errorMessage };
806
+ }
807
+ }
808
+
809
+ private _generateProcedureSummaryInfo(
810
+ procedure: Procedure,
811
+ clinicData: Clinic,
812
+ practitionerData: Practitioner
813
+ ): ProcedureSummaryInfo {
814
+ const procedureCategory = procedure.category as Category;
815
+ const procedureSubCategory = procedure.subcategory as Subcategory;
816
+ const procedureTechnology = procedure.technology as Technology;
817
+ const procedureProduct = procedure.product as Product;
818
+ return {
819
+ id: procedure.id,
820
+ name: procedure.name,
821
+ description: procedure.description,
822
+ family: procedure.family,
823
+ categoryName: procedureCategory?.name || "",
824
+ subcategoryName: procedureSubCategory?.name || "",
825
+ technologyName: procedureTechnology?.name || "",
826
+ price: procedure.price,
827
+ pricingMeasure: procedure.pricingMeasure,
828
+ currency: procedure.currency,
829
+ duration: procedure.duration,
830
+ clinicId: procedure.clinicBranchId,
831
+ clinicName: clinicData.name,
832
+ practitionerId: procedure.practitionerId,
833
+ practitionerName: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
834
+ photo:
835
+ (procedureTechnology as any)?.photos?.[0]?.url ||
836
+ (procedureProduct as any)?.photos?.[0]?.url ||
837
+ "",
838
+ brandName: (procedureProduct as any)?.brand?.name || "",
839
+ productName: procedureProduct?.name || "",
840
+ };
841
+ }
842
+
843
+ private _generateProcedureExtendedInfo(
844
+ procedure: Procedure
845
+ ): ProcedureExtendedInfo {
846
+ const procedureCategory = procedure.category as Category;
847
+ const procedureSubCategory = procedure.subcategory as Subcategory;
848
+ const procedureTechnology = procedure.technology as Technology;
849
+ const procedureProduct = procedure.product as Product;
850
+ return {
851
+ id: procedure.id,
852
+ name: procedure.name,
853
+ description: procedure.description,
854
+ cost: procedure.price,
855
+ duration: procedure.duration,
856
+ procedureFamily: procedure.family,
857
+ procedureCategoryId: procedureCategory?.id || "",
858
+ procedureCategoryName: procedureCategory?.name || "",
859
+ procedureSubCategoryId: procedureSubCategory?.id || "",
860
+ procedureSubCategoryName: procedureSubCategory?.name || "",
861
+ procedureTechnologyId: procedureTechnology?.id || "",
862
+ procedureTechnologyName: procedureTechnology?.name || "",
863
+ procedureProductBrandId: (procedureProduct as any)?.brand?.id || "",
864
+ procedureProductBrandName: (procedureProduct as any)?.brand?.name || "",
865
+ procedureProductId: procedureProduct?.id || "",
866
+ procedureProductName: procedureProduct?.name || "",
867
+ };
868
+ }
234
869
  }