@blackcode_sa/metaestetics-api 1.13.3 → 1.13.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.
- package/dist/admin/index.d.mts +15 -28
- package/dist/admin/index.d.ts +15 -28
- package/dist/index.d.mts +18 -30
- package/dist/index.d.ts +18 -30
- package/dist/index.js +11 -3
- package/dist/index.mjs +11 -3
- package/package.json +121 -119
- package/src/__mocks__/firstore.ts +10 -10
- package/src/admin/aggregation/README.md +79 -79
- package/src/admin/aggregation/appointment/README.md +128 -128
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1984 -1984
- package/src/admin/aggregation/appointment/index.ts +1 -1
- package/src/admin/aggregation/clinic/README.md +52 -52
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
- package/src/admin/aggregation/clinic/index.ts +1 -1
- package/src/admin/aggregation/forms/README.md +13 -13
- package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
- package/src/admin/aggregation/forms/index.ts +1 -1
- package/src/admin/aggregation/index.ts +8 -8
- package/src/admin/aggregation/patient/README.md +27 -27
- package/src/admin/aggregation/patient/index.ts +1 -1
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
- package/src/admin/aggregation/practitioner/README.md +42 -42
- package/src/admin/aggregation/practitioner/index.ts +1 -1
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
- package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
- package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
- package/src/admin/aggregation/procedure/README.md +43 -43
- package/src/admin/aggregation/procedure/index.ts +1 -1
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
- package/src/admin/aggregation/reviews/index.ts +1 -1
- package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
- package/src/admin/analytics/analytics.admin.service.ts +278 -278
- package/src/admin/analytics/index.ts +2 -2
- package/src/admin/booking/README.md +125 -125
- package/src/admin/booking/booking.admin.ts +1037 -1037
- package/src/admin/booking/booking.calculator.ts +712 -712
- package/src/admin/booking/booking.types.ts +59 -59
- package/src/admin/booking/index.ts +3 -3
- package/src/admin/booking/timezones-problem.md +185 -185
- package/src/admin/calendar/README.md +7 -7
- package/src/admin/calendar/calendar.admin.service.ts +345 -345
- package/src/admin/calendar/index.ts +1 -1
- package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
- package/src/admin/documentation-templates/index.ts +1 -1
- package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
- package/src/admin/free-consultation/index.ts +1 -1
- package/src/admin/index.ts +81 -81
- package/src/admin/logger/index.ts +78 -78
- package/src/admin/mailing/README.md +95 -95
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
- package/src/admin/mailing/appointment/index.ts +1 -1
- package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
- package/src/admin/mailing/base.mailing.service.ts +208 -208
- package/src/admin/mailing/index.ts +3 -3
- package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
- package/src/admin/mailing/practitionerInvite/index.ts +2 -2
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
- package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
- package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
- package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
- package/src/admin/notifications/index.ts +1 -1
- package/src/admin/notifications/notifications.admin.ts +710 -710
- package/src/admin/requirements/README.md +128 -128
- package/src/admin/requirements/index.ts +1 -1
- package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
- package/src/admin/users/index.ts +1 -1
- package/src/admin/users/user-profile.admin.ts +405 -405
- package/src/backoffice/constants/certification.constants.ts +13 -13
- package/src/backoffice/constants/index.ts +1 -1
- package/src/backoffice/errors/backoffice.errors.ts +181 -181
- package/src/backoffice/errors/index.ts +1 -1
- package/src/backoffice/expo-safe/README.md +26 -26
- package/src/backoffice/expo-safe/index.ts +41 -41
- package/src/backoffice/index.ts +5 -5
- package/src/backoffice/services/FIXES_README.md +102 -102
- package/src/backoffice/services/README.md +57 -57
- package/src/backoffice/services/analytics.service.proposal.md +863 -863
- package/src/backoffice/services/analytics.service.summary.md +143 -143
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +384 -384
- package/src/backoffice/services/constants.service.ts +385 -385
- package/src/backoffice/services/documentation-template.service.ts +202 -202
- package/src/backoffice/services/index.ts +10 -10
- package/src/backoffice/services/migrate-products.ts +116 -116
- package/src/backoffice/services/product.service.ts +553 -553
- package/src/backoffice/services/requirement.service.ts +235 -235
- package/src/backoffice/services/subcategory.service.ts +461 -461
- package/src/backoffice/services/technology.service.ts +1151 -1151
- package/src/backoffice/types/README.md +12 -12
- package/src/backoffice/types/admin-constants.types.ts +69 -69
- package/src/backoffice/types/brand.types.ts +29 -29
- package/src/backoffice/types/category.types.ts +67 -67
- package/src/backoffice/types/documentation-templates.types.ts +28 -28
- package/src/backoffice/types/index.ts +10 -10
- package/src/backoffice/types/procedure-product.types.ts +38 -38
- package/src/backoffice/types/product.types.ts +240 -240
- package/src/backoffice/types/requirement.types.ts +63 -63
- package/src/backoffice/types/static/README.md +18 -18
- package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
- package/src/backoffice/types/static/certification.types.ts +37 -37
- package/src/backoffice/types/static/contraindication.types.ts +19 -19
- package/src/backoffice/types/static/index.ts +6 -6
- package/src/backoffice/types/static/pricing.types.ts +16 -16
- package/src/backoffice/types/static/procedure-family.types.ts +14 -14
- package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
- package/src/backoffice/types/subcategory.types.ts +34 -34
- package/src/backoffice/types/technology.types.ts +168 -168
- package/src/backoffice/validations/index.ts +1 -1
- package/src/backoffice/validations/schemas.ts +164 -164
- package/src/config/__mocks__/firebase.ts +99 -99
- package/src/config/firebase.ts +78 -78
- package/src/config/index.ts +9 -9
- package/src/errors/auth.error.ts +6 -6
- package/src/errors/auth.errors.ts +200 -200
- package/src/errors/clinic.errors.ts +32 -32
- package/src/errors/firebase.errors.ts +47 -47
- package/src/errors/user.errors.ts +99 -99
- package/src/index.backup.ts +407 -407
- package/src/index.ts +6 -6
- package/src/locales/en.ts +31 -31
- package/src/recommender/admin/index.ts +1 -1
- package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
- package/src/recommender/front/index.ts +1 -1
- package/src/recommender/front/services/onboarding.service.ts +5 -5
- package/src/recommender/front/services/recommender.service.ts +3 -3
- package/src/recommender/index.ts +1 -1
- package/src/services/PATIENTAUTH.MD +197 -197
- package/src/services/README.md +106 -106
- package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
- package/src/services/__tests__/auth/auth.setup.ts +293 -293
- package/src/services/__tests__/auth.service.test.ts +346 -346
- package/src/services/__tests__/base.service.test.ts +77 -77
- package/src/services/__tests__/user.service.test.ts +528 -528
- package/src/services/analytics/ARCHITECTURE.md +199 -199
- package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
- package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
- package/src/services/analytics/QUICK_START.md +393 -393
- package/src/services/analytics/README.md +304 -304
- package/src/services/analytics/SUMMARY.md +141 -141
- package/src/services/analytics/TRENDS.md +380 -380
- package/src/services/analytics/USAGE_GUIDE.md +518 -518
- package/src/services/analytics/analytics-cloud.service.ts +222 -222
- package/src/services/analytics/analytics.service.ts +2142 -2142
- package/src/services/analytics/index.ts +4 -4
- package/src/services/analytics/review-analytics.service.ts +941 -941
- package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
- package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
- package/src/services/analytics/utils/grouping.utils.ts +434 -434
- package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
- package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
- package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2558 -2558
- package/src/services/appointment/index.ts +1 -1
- package/src/services/appointment/utils/appointment.utils.ts +552 -552
- package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
- package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
- package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
- package/src/services/appointment/utils/zone-management.utils.ts +353 -353
- package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
- package/src/services/auth/auth.service.ts +989 -989
- package/src/services/auth/auth.v2.service.ts +961 -961
- package/src/services/auth/index.ts +7 -7
- package/src/services/auth/utils/error.utils.ts +90 -90
- package/src/services/auth/utils/firebase.utils.ts +49 -49
- package/src/services/auth/utils/index.ts +21 -21
- package/src/services/auth/utils/practitioner.utils.ts +125 -125
- package/src/services/base.service.ts +41 -41
- package/src/services/calendar/calendar.service.ts +1077 -1077
- package/src/services/calendar/calendar.v2.service.ts +1683 -1683
- package/src/services/calendar/calendar.v3.service.ts +313 -313
- package/src/services/calendar/externalCalendar.service.ts +178 -178
- package/src/services/calendar/index.ts +5 -5
- package/src/services/calendar/synced-calendars.service.ts +743 -743
- package/src/services/calendar/utils/appointment.utils.ts +265 -265
- package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
- package/src/services/calendar/utils/clinic.utils.ts +237 -237
- package/src/services/calendar/utils/docs.utils.ts +157 -157
- package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
- package/src/services/calendar/utils/index.ts +8 -8
- package/src/services/calendar/utils/patient.utils.ts +198 -198
- package/src/services/calendar/utils/practitioner.utils.ts +221 -221
- package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
- package/src/services/clinic/README.md +204 -204
- package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
- package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
- package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
- package/src/services/clinic/billing-transactions.service.ts +217 -217
- package/src/services/clinic/clinic-admin.service.ts +202 -202
- package/src/services/clinic/clinic-group.service.ts +310 -310
- package/src/services/clinic/clinic.service.ts +708 -708
- package/src/services/clinic/index.ts +5 -5
- package/src/services/clinic/practitioner-invite.service.ts +519 -519
- package/src/services/clinic/utils/admin.utils.ts +551 -551
- package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
- package/src/services/clinic/utils/clinic.utils.ts +949 -949
- package/src/services/clinic/utils/filter.utils.d.ts +23 -23
- package/src/services/clinic/utils/filter.utils.ts +446 -446
- package/src/services/clinic/utils/index.ts +11 -11
- package/src/services/clinic/utils/photos.utils.ts +188 -188
- package/src/services/clinic/utils/search.utils.ts +84 -84
- package/src/services/clinic/utils/tag.utils.ts +124 -124
- package/src/services/documentation-templates/documentation-template.service.ts +537 -537
- package/src/services/documentation-templates/filled-document.service.ts +587 -587
- package/src/services/documentation-templates/index.ts +2 -2
- package/src/services/index.ts +14 -14
- package/src/services/media/index.ts +1 -1
- package/src/services/media/media.service.ts +418 -418
- package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/notification.service.ts +215 -215
- package/src/services/patient/README.md +48 -48
- package/src/services/patient/To-Do.md +43 -43
- package/src/services/patient/__tests__/patient.service.test.ts +294 -294
- package/src/services/patient/index.ts +2 -2
- package/src/services/patient/patient.service.ts +883 -883
- package/src/services/patient/patientRequirements.service.ts +285 -285
- package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
- package/src/services/patient/utils/clinic.utils.ts +80 -80
- package/src/services/patient/utils/docs.utils.ts +142 -142
- package/src/services/patient/utils/index.ts +9 -9
- package/src/services/patient/utils/location.utils.ts +126 -126
- package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
- package/src/services/patient/utils/medical.utils.ts +458 -458
- package/src/services/patient/utils/practitioner.utils.ts +260 -260
- package/src/services/patient/utils/profile.utils.ts +510 -510
- package/src/services/patient/utils/sensitive.utils.ts +260 -260
- package/src/services/patient/utils/token.utils.ts +211 -211
- package/src/services/practitioner/README.md +145 -145
- package/src/services/practitioner/index.ts +1 -1
- package/src/services/practitioner/practitioner.service.ts +1742 -1742
- package/src/services/procedure/README.md +163 -163
- package/src/services/procedure/index.ts +1 -1
- package/src/services/procedure/procedure.service.ts +2200 -2191
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +734 -734
- package/src/services/user/index.ts +1 -1
- package/src/services/user/user.service.ts +489 -489
- package/src/services/user/user.v2.service.ts +466 -466
- package/src/types/analytics/analytics.types.ts +597 -597
- package/src/types/analytics/grouped-analytics.types.ts +173 -173
- package/src/types/analytics/index.ts +4 -4
- package/src/types/analytics/stored-analytics.types.ts +137 -137
- package/src/types/appointment/index.ts +480 -480
- package/src/types/calendar/index.ts +258 -258
- package/src/types/calendar/synced-calendar.types.ts +66 -66
- package/src/types/clinic/index.ts +498 -489
- package/src/types/clinic/practitioner-invite.types.ts +91 -91
- package/src/types/clinic/preferences.types.ts +159 -159
- package/src/types/clinic/to-do +3 -3
- package/src/types/documentation-templates/index.ts +308 -308
- package/src/types/index.ts +47 -47
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +286 -286
- package/src/types/patient/aesthetic-analysis.types.ts +66 -66
- package/src/types/patient/allergies.ts +58 -58
- package/src/types/patient/index.ts +275 -275
- package/src/types/patient/medical-info.types.ts +152 -152
- package/src/types/patient/patient-requirements.ts +92 -92
- package/src/types/patient/token.types.ts +61 -61
- package/src/types/practitioner/index.ts +206 -206
- package/src/types/procedure/index.ts +181 -181
- package/src/types/profile/index.ts +39 -39
- package/src/types/reviews/index.ts +132 -132
- package/src/types/tz-lookup.d.ts +4 -4
- package/src/types/user/index.ts +38 -38
- package/src/utils/TIMESTAMPS.md +176 -176
- package/src/utils/TimestampUtils.ts +241 -241
- package/src/utils/index.ts +1 -1
- package/src/validations/appointment.schema.ts +574 -574
- package/src/validations/calendar.schema.ts +225 -225
- package/src/validations/clinic.schema.ts +494 -493
- package/src/validations/common.schema.ts +25 -25
- package/src/validations/documentation-templates/index.ts +1 -1
- package/src/validations/documentation-templates/template.schema.ts +220 -220
- package/src/validations/documentation-templates.schema.ts +10 -10
- package/src/validations/index.ts +20 -20
- package/src/validations/media.schema.ts +10 -10
- package/src/validations/notification.schema.ts +90 -90
- package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
- package/src/validations/patient/medical-info.schema.ts +125 -125
- package/src/validations/patient/patient-requirements.schema.ts +84 -84
- package/src/validations/patient/token.schema.ts +29 -29
- package/src/validations/patient.schema.ts +217 -217
- package/src/validations/practitioner.schema.ts +222 -222
- package/src/validations/procedure-product.schema.ts +41 -41
- package/src/validations/procedure.schema.ts +124 -124
- package/src/validations/profile-info.schema.ts +41 -41
- package/src/validations/reviews.schema.ts +195 -195
- package/src/validations/schemas.ts +104 -104
- package/src/validations/shared.schema.ts +78 -78
|
@@ -1,1037 +1,1037 @@
|
|
|
1
|
-
import * as admin from "firebase-admin";
|
|
2
|
-
import { Timestamp as FirebaseClientTimestamp } from "@firebase/firestore";
|
|
3
|
-
import {
|
|
4
|
-
BookingAvailabilityCalculator,
|
|
5
|
-
BookingAvailabilityRequest,
|
|
6
|
-
BookingAvailabilityResponse,
|
|
7
|
-
AvailableSlot,
|
|
8
|
-
} from "./";
|
|
9
|
-
import { CalendarEventStatus, CalendarEventType } from "../../types/calendar";
|
|
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 { TechnologyDocumentationTemplate } from "../../backoffice/types/technology.types";
|
|
48
|
-
import {
|
|
49
|
-
ClinicInfo,
|
|
50
|
-
PractitionerProfileInfo,
|
|
51
|
-
PatientProfileInfo,
|
|
52
|
-
} from "../../types/profile";
|
|
53
|
-
import { Category } from "../../backoffice/types/category.types";
|
|
54
|
-
import { Subcategory } from "../../backoffice/types/subcategory.types";
|
|
55
|
-
import { Technology } from "../../backoffice/types/technology.types";
|
|
56
|
-
import { Product } from "../../backoffice/types/product.types";
|
|
57
|
-
import {
|
|
58
|
-
CalendarEvent,
|
|
59
|
-
CalendarEventTime,
|
|
60
|
-
CalendarSyncStatus,
|
|
61
|
-
ProcedureInfo as CalendarProcedureInfo,
|
|
62
|
-
CALENDAR_COLLECTION,
|
|
63
|
-
} from "../../types/calendar";
|
|
64
|
-
import { DocumentManagerAdminService } from "../documentation-templates/document-manager.admin";
|
|
65
|
-
import { LinkedFormInfo } from "../../types/appointment";
|
|
66
|
-
import { TimestampUtils } from "../../utils/TimestampUtils";
|
|
67
|
-
import { Logger } from "../logger";
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Interface for the data required by orchestrateAppointmentCreation
|
|
71
|
-
*/
|
|
72
|
-
export interface OrchestrateAppointmentCreationData {
|
|
73
|
-
patientId: string;
|
|
74
|
-
procedureId: string;
|
|
75
|
-
appointmentStartTime: admin.firestore.Timestamp;
|
|
76
|
-
appointmentEndTime: admin.firestore.Timestamp;
|
|
77
|
-
patientNotes?: string | null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Admin service for handling booking-related operations.
|
|
82
|
-
* This is the cloud-based implementation that will be used in Cloud Functions.
|
|
83
|
-
*/
|
|
84
|
-
export class BookingAdmin {
|
|
85
|
-
private db: admin.firestore.Firestore;
|
|
86
|
-
private documentManagerAdmin: DocumentManagerAdminService;
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Creates a new BookingAdmin instance
|
|
90
|
-
* @param firestore - Firestore instance provided by the caller
|
|
91
|
-
*/
|
|
92
|
-
constructor(firestore?: admin.firestore.Firestore) {
|
|
93
|
-
this.db = firestore || admin.firestore();
|
|
94
|
-
this.documentManagerAdmin = new DocumentManagerAdminService(this.db);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Gets available booking time slots for a specific clinic, practitioner, and procedure
|
|
99
|
-
*
|
|
100
|
-
* @param clinicId - ID of the clinic
|
|
101
|
-
* @param practitionerId - ID of the practitioner
|
|
102
|
-
* @param procedureId - ID of the procedure
|
|
103
|
-
* @param timeframe - Time range to check for availability
|
|
104
|
-
* @returns Promise resolving to an array of available booking slots
|
|
105
|
-
*/
|
|
106
|
-
async getAvailableBookingSlots(
|
|
107
|
-
clinicId: string,
|
|
108
|
-
practitionerId: string,
|
|
109
|
-
procedureId: string,
|
|
110
|
-
timeframe: {
|
|
111
|
-
start: Date | admin.firestore.Timestamp;
|
|
112
|
-
end: Date | admin.firestore.Timestamp;
|
|
113
|
-
}
|
|
114
|
-
): Promise<{ availableSlots: { start: admin.firestore.Timestamp }[] }> {
|
|
115
|
-
try {
|
|
116
|
-
Logger.info("[BookingAdmin] Starting availability calculation", {
|
|
117
|
-
clinicId,
|
|
118
|
-
practitionerId,
|
|
119
|
-
procedureId,
|
|
120
|
-
timeframeStart:
|
|
121
|
-
timeframe.start instanceof Date
|
|
122
|
-
? timeframe.start.toISOString()
|
|
123
|
-
: timeframe.start.toDate().toISOString(),
|
|
124
|
-
timeframeEnd:
|
|
125
|
-
timeframe.end instanceof Date
|
|
126
|
-
? timeframe.end.toISOString()
|
|
127
|
-
: timeframe.end.toDate().toISOString(),
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// Convert timeframe dates to Firestore Timestamps if needed
|
|
131
|
-
const start =
|
|
132
|
-
timeframe.start instanceof Date
|
|
133
|
-
? admin.firestore.Timestamp.fromDate(timeframe.start)
|
|
134
|
-
: timeframe.start;
|
|
135
|
-
|
|
136
|
-
const end =
|
|
137
|
-
timeframe.end instanceof Date
|
|
138
|
-
? admin.firestore.Timestamp.fromDate(timeframe.end)
|
|
139
|
-
: timeframe.end;
|
|
140
|
-
|
|
141
|
-
// 1. Fetch clinic data
|
|
142
|
-
Logger.debug("[BookingAdmin] Fetching clinic data", { clinicId });
|
|
143
|
-
const clinicDoc = await this.db.collection("clinics").doc(clinicId).get();
|
|
144
|
-
if (!clinicDoc.exists) {
|
|
145
|
-
Logger.error("[BookingAdmin] Clinic not found", { clinicId });
|
|
146
|
-
throw new Error(`Clinic ${clinicId} not found`);
|
|
147
|
-
}
|
|
148
|
-
const clinic = clinicDoc.data() as unknown as Clinic;
|
|
149
|
-
Logger.debug("[BookingAdmin] Retrieved clinic data", {
|
|
150
|
-
clinicName: clinic.name,
|
|
151
|
-
clinicHasWorkingHours: !!clinic.workingHours,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// 2. Fetch practitioner data
|
|
155
|
-
Logger.debug("[BookingAdmin] Fetching practitioner data", {
|
|
156
|
-
practitionerId,
|
|
157
|
-
});
|
|
158
|
-
const practitionerDoc = await this.db
|
|
159
|
-
.collection("practitioners")
|
|
160
|
-
.doc(practitionerId)
|
|
161
|
-
.get();
|
|
162
|
-
if (!practitionerDoc.exists) {
|
|
163
|
-
Logger.error("[BookingAdmin] Practitioner not found", {
|
|
164
|
-
practitionerId,
|
|
165
|
-
});
|
|
166
|
-
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
167
|
-
}
|
|
168
|
-
const practitioner = practitionerDoc.data() as unknown as Practitioner;
|
|
169
|
-
Logger.debug("[BookingAdmin] Retrieved practitioner data", {
|
|
170
|
-
practitionerName: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
171
|
-
pracWorkingHoursCount: practitioner.clinicWorkingHours?.length || 0,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// 3. Fetch procedure data
|
|
175
|
-
Logger.debug("[BookingAdmin] Fetching procedure data", { procedureId });
|
|
176
|
-
const procedureDoc = await this.db
|
|
177
|
-
.collection("procedures")
|
|
178
|
-
.doc(procedureId)
|
|
179
|
-
.get();
|
|
180
|
-
if (!procedureDoc.exists) {
|
|
181
|
-
Logger.error("[BookingAdmin] Procedure not found", { procedureId });
|
|
182
|
-
throw new Error(`Procedure ${procedureId} not found`);
|
|
183
|
-
}
|
|
184
|
-
const procedure = procedureDoc.data() as unknown as Procedure;
|
|
185
|
-
Logger.debug("[BookingAdmin] Retrieved procedure data", {
|
|
186
|
-
procedureName: procedure.name,
|
|
187
|
-
procedureDuration: procedure.duration,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// 4. Fetch clinic calendar events
|
|
191
|
-
Logger.debug("[BookingAdmin] Fetching clinic calendar events", {
|
|
192
|
-
clinicId,
|
|
193
|
-
startTime: start.toDate().toISOString(),
|
|
194
|
-
endTime: end.toDate().toISOString(),
|
|
195
|
-
});
|
|
196
|
-
const clinicCalendarEvents = await this.getClinicCalendarEvents(
|
|
197
|
-
clinicId,
|
|
198
|
-
start,
|
|
199
|
-
end
|
|
200
|
-
);
|
|
201
|
-
Logger.debug("[BookingAdmin] Retrieved clinic calendar events", {
|
|
202
|
-
count: clinicCalendarEvents.length,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// 5. Fetch practitioner calendar events
|
|
206
|
-
Logger.debug("[BookingAdmin] Fetching practitioner calendar events", {
|
|
207
|
-
practitionerId,
|
|
208
|
-
startTime: start.toDate().toISOString(),
|
|
209
|
-
endTime: end.toDate().toISOString(),
|
|
210
|
-
});
|
|
211
|
-
const practitionerCalendarEvents =
|
|
212
|
-
await this.getPractitionerCalendarEvents(practitionerId, start, end);
|
|
213
|
-
Logger.debug("[BookingAdmin] Retrieved practitioner calendar events", {
|
|
214
|
-
count: practitionerCalendarEvents.length,
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// Since we're working with two different Timestamp implementations (admin vs client),
|
|
218
|
-
// we need to convert our timestamps to the client-side format expected by the calculator
|
|
219
|
-
// Create client Timestamp objects from admin Timestamp objects
|
|
220
|
-
const convertedTimeframe = {
|
|
221
|
-
start: this.adminTimestampToClientTimestamp(start),
|
|
222
|
-
end: this.adminTimestampToClientTimestamp(end),
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
// Create the request object for the calculator
|
|
226
|
-
const request: BookingAvailabilityRequest = {
|
|
227
|
-
clinic,
|
|
228
|
-
practitioner,
|
|
229
|
-
procedure,
|
|
230
|
-
timeframe: convertedTimeframe,
|
|
231
|
-
clinicCalendarEvents:
|
|
232
|
-
this.convertEventsTimestamps(clinicCalendarEvents),
|
|
233
|
-
practitionerCalendarEvents: this.convertEventsTimestamps(
|
|
234
|
-
practitionerCalendarEvents
|
|
235
|
-
),
|
|
236
|
-
tz: clinic.location.tz || "UTC",
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
Logger.info("[BookingAdmin] Calling availability calculator", {
|
|
240
|
-
calculatorInputReady: true,
|
|
241
|
-
timeframeDurationHours: Math.round(
|
|
242
|
-
(end.toMillis() - start.toMillis()) / (1000 * 60 * 60)
|
|
243
|
-
),
|
|
244
|
-
clinicEventsCount: clinicCalendarEvents.length,
|
|
245
|
-
practitionerEventsCount: practitionerCalendarEvents.length,
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// Use the calculator to compute available slots
|
|
249
|
-
const result = BookingAvailabilityCalculator.calculateSlots(request);
|
|
250
|
-
|
|
251
|
-
// Convert the client Timestamps to admin Timestamps before returning
|
|
252
|
-
const availableSlotsResult = {
|
|
253
|
-
availableSlots: result.availableSlots.map((slot) => ({
|
|
254
|
-
start: admin.firestore.Timestamp.fromMillis(slot.start.toMillis()),
|
|
255
|
-
})),
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
Logger.info(
|
|
259
|
-
"[BookingAdmin] Availability calculation completed successfully",
|
|
260
|
-
{
|
|
261
|
-
availableSlotsCount: availableSlotsResult.availableSlots.length,
|
|
262
|
-
firstSlotTime:
|
|
263
|
-
availableSlotsResult.availableSlots.length > 0
|
|
264
|
-
? availableSlotsResult.availableSlots[0].start
|
|
265
|
-
.toDate()
|
|
266
|
-
.toISOString()
|
|
267
|
-
: "none",
|
|
268
|
-
lastSlotTime:
|
|
269
|
-
availableSlotsResult.availableSlots.length > 0
|
|
270
|
-
? availableSlotsResult.availableSlots[
|
|
271
|
-
availableSlotsResult.availableSlots.length - 1
|
|
272
|
-
].start
|
|
273
|
-
.toDate()
|
|
274
|
-
.toISOString()
|
|
275
|
-
: "none",
|
|
276
|
-
}
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
return availableSlotsResult;
|
|
280
|
-
} catch (error) {
|
|
281
|
-
const errorMessage =
|
|
282
|
-
error instanceof Error ? error.message : String(error);
|
|
283
|
-
Logger.error("[BookingAdmin] Error getting available slots", {
|
|
284
|
-
errorMessage,
|
|
285
|
-
clinicId,
|
|
286
|
-
practitionerId,
|
|
287
|
-
procedureId,
|
|
288
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
289
|
-
});
|
|
290
|
-
throw error;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Converts an admin Firestore Timestamp to a client Firestore Timestamp
|
|
296
|
-
*/
|
|
297
|
-
private adminTimestampToClientTimestamp(
|
|
298
|
-
timestamp: admin.firestore.Timestamp
|
|
299
|
-
): FirebaseClientTimestamp {
|
|
300
|
-
// Use TimestampUtils instead of custom implementation
|
|
301
|
-
return TimestampUtils.adminToClient(timestamp) as FirebaseClientTimestamp;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Converts timestamps in calendar events from admin Firestore Timestamps to client Firestore Timestamps
|
|
306
|
-
*/
|
|
307
|
-
private convertEventsTimestamps(events: any[]): any[] {
|
|
308
|
-
return events.map((event) => ({
|
|
309
|
-
...event,
|
|
310
|
-
eventTime: {
|
|
311
|
-
start: TimestampUtils.adminToClient(
|
|
312
|
-
event.eventTime.start
|
|
313
|
-
) as FirebaseClientTimestamp,
|
|
314
|
-
end: TimestampUtils.adminToClient(
|
|
315
|
-
event.eventTime.end
|
|
316
|
-
) as FirebaseClientTimestamp,
|
|
317
|
-
},
|
|
318
|
-
// Convert any other timestamps in the event if needed
|
|
319
|
-
}));
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Fetches clinic calendar events for a specific time range
|
|
324
|
-
*
|
|
325
|
-
* @param clinicId - ID of the clinic
|
|
326
|
-
* @param start - Start time of the range
|
|
327
|
-
* @param end - End time of the range
|
|
328
|
-
* @returns Promise resolving to an array of calendar events
|
|
329
|
-
*/
|
|
330
|
-
private async getClinicCalendarEvents(
|
|
331
|
-
clinicId: string,
|
|
332
|
-
start: admin.firestore.Timestamp,
|
|
333
|
-
end: admin.firestore.Timestamp
|
|
334
|
-
): Promise<any[]> {
|
|
335
|
-
try {
|
|
336
|
-
Logger.debug("[BookingAdmin] Querying clinic calendar events", {
|
|
337
|
-
clinicId,
|
|
338
|
-
startTime: start.toDate().toISOString(),
|
|
339
|
-
endTime: end.toDate().toISOString(),
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
const MAX_EVENT_DURATION_MS = 24 * 60 * 60 * 1000;
|
|
343
|
-
const queryStart = admin.firestore.Timestamp.fromMillis(
|
|
344
|
-
start.toMillis() - MAX_EVENT_DURATION_MS
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
const eventsRef = this.db
|
|
348
|
-
.collection(`clinics/${clinicId}/calendar`)
|
|
349
|
-
.where("eventTime.start", ">=", queryStart)
|
|
350
|
-
.where("eventTime.start", "<", end)
|
|
351
|
-
.orderBy("eventTime.start");
|
|
352
|
-
|
|
353
|
-
const snapshot = await eventsRef.get();
|
|
354
|
-
|
|
355
|
-
const events = snapshot.docs
|
|
356
|
-
.map((doc) => ({
|
|
357
|
-
...doc.data(),
|
|
358
|
-
id: doc.id,
|
|
359
|
-
}))
|
|
360
|
-
.filter((event: any) => {
|
|
361
|
-
return event.eventTime.end.toMillis() > start.toMillis();
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
Logger.debug("[BookingAdmin] Retrieved clinic calendar events", {
|
|
365
|
-
clinicId,
|
|
366
|
-
eventsCount: events.length,
|
|
367
|
-
eventsTypes: this.summarizeEventTypes(events),
|
|
368
|
-
queryStartTime: queryStart.toDate().toISOString(),
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
return events;
|
|
372
|
-
} catch (error) {
|
|
373
|
-
const errorMessage =
|
|
374
|
-
error instanceof Error ? error.message : String(error);
|
|
375
|
-
Logger.error("[BookingAdmin] Error fetching clinic calendar events", {
|
|
376
|
-
errorMessage,
|
|
377
|
-
clinicId,
|
|
378
|
-
startTime: start.toDate().toISOString(),
|
|
379
|
-
endTime: end.toDate().toISOString(),
|
|
380
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
381
|
-
});
|
|
382
|
-
return [];
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Fetches practitioner calendar events for a specific time range
|
|
388
|
-
*
|
|
389
|
-
* @param practitionerId - ID of the practitioner
|
|
390
|
-
* @param start - Start time of the range
|
|
391
|
-
* @param end - End time of the range
|
|
392
|
-
* @returns Promise resolving to an array of calendar events
|
|
393
|
-
*/
|
|
394
|
-
private async getPractitionerCalendarEvents(
|
|
395
|
-
practitionerId: string,
|
|
396
|
-
start: admin.firestore.Timestamp,
|
|
397
|
-
end: admin.firestore.Timestamp
|
|
398
|
-
): Promise<any[]> {
|
|
399
|
-
try {
|
|
400
|
-
Logger.debug("[BookingAdmin] Querying practitioner calendar events", {
|
|
401
|
-
practitionerId,
|
|
402
|
-
startTime: start.toDate().toISOString(),
|
|
403
|
-
endTime: end.toDate().toISOString(),
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
const MAX_EVENT_DURATION_MS = 24 * 60 * 60 * 1000;
|
|
407
|
-
const queryStart = admin.firestore.Timestamp.fromMillis(
|
|
408
|
-
start.toMillis() - MAX_EVENT_DURATION_MS
|
|
409
|
-
);
|
|
410
|
-
|
|
411
|
-
const eventsRef = this.db
|
|
412
|
-
.collection(`practitioners/${practitionerId}/calendar`)
|
|
413
|
-
.where("eventTime.start", ">=", queryStart)
|
|
414
|
-
.where("eventTime.start", "<", end)
|
|
415
|
-
.orderBy("eventTime.start");
|
|
416
|
-
|
|
417
|
-
const snapshot = await eventsRef.get();
|
|
418
|
-
|
|
419
|
-
const events = snapshot.docs
|
|
420
|
-
.map((doc) => ({
|
|
421
|
-
...doc.data(),
|
|
422
|
-
id: doc.id,
|
|
423
|
-
}))
|
|
424
|
-
.filter((event: any) => {
|
|
425
|
-
return event.eventTime.end.toMillis() > start.toMillis();
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
Logger.debug("[BookingAdmin] Retrieved practitioner calendar events", {
|
|
429
|
-
practitionerId,
|
|
430
|
-
eventsCount: events.length,
|
|
431
|
-
eventsTypes: this.summarizeEventTypes(events),
|
|
432
|
-
queryStartTime: queryStart.toDate().toISOString(),
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
return events;
|
|
436
|
-
} catch (error) {
|
|
437
|
-
const errorMessage =
|
|
438
|
-
error instanceof Error ? error.message : String(error);
|
|
439
|
-
Logger.error(
|
|
440
|
-
"[BookingAdmin] Error fetching practitioner calendar events",
|
|
441
|
-
{
|
|
442
|
-
errorMessage,
|
|
443
|
-
practitionerId,
|
|
444
|
-
startTime: start.toDate().toISOString(),
|
|
445
|
-
endTime: end.toDate().toISOString(),
|
|
446
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
447
|
-
}
|
|
448
|
-
);
|
|
449
|
-
return [];
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Summarizes event types for logging purposes
|
|
455
|
-
* @param events Array of calendar events
|
|
456
|
-
* @returns Object with counts of each event type
|
|
457
|
-
*/
|
|
458
|
-
private summarizeEventTypes(events: any[]): Record<string, number> {
|
|
459
|
-
const typeCounts: Record<string, number> = {};
|
|
460
|
-
|
|
461
|
-
events.forEach((event) => {
|
|
462
|
-
const eventType = event.eventType || "unknown";
|
|
463
|
-
typeCounts[eventType] = (typeCounts[eventType] || 0) + 1;
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
return typeCounts;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
private _generateCalendarProcedureInfo(
|
|
470
|
-
procedure: Procedure
|
|
471
|
-
): CalendarProcedureInfo {
|
|
472
|
-
return {
|
|
473
|
-
name: procedure.name,
|
|
474
|
-
description: procedure.description,
|
|
475
|
-
duration: procedure.duration, // in minutes
|
|
476
|
-
price: procedure.price,
|
|
477
|
-
currency: procedure.currency,
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Orchestrates the creation of a new appointment, including data aggregation.
|
|
483
|
-
* This method is intended to be called from a trusted backend environment (e.g., an Express route handler in a Cloud Function).
|
|
484
|
-
*
|
|
485
|
-
* @param data - Data required to create the appointment.
|
|
486
|
-
* @param authenticatedUserId - The ID of the user making the request (for auditing, and usually is the patientId).
|
|
487
|
-
* @returns Promise resolving to an object indicating success, and appointmentId or an error message.
|
|
488
|
-
*/
|
|
489
|
-
async orchestrateAppointmentCreation(
|
|
490
|
-
data: OrchestrateAppointmentCreationData,
|
|
491
|
-
authenticatedUserId: string
|
|
492
|
-
): Promise<{
|
|
493
|
-
success: boolean;
|
|
494
|
-
appointmentId?: string;
|
|
495
|
-
appointmentData?: Appointment;
|
|
496
|
-
calendarEventId?: string;
|
|
497
|
-
error?: string;
|
|
498
|
-
}> {
|
|
499
|
-
console.log(
|
|
500
|
-
`[BookingAdmin] Orchestrating appointment creation for patient ${data.patientId} by user ${authenticatedUserId}`
|
|
501
|
-
);
|
|
502
|
-
const batch = this.db.batch();
|
|
503
|
-
const adminTsNow = admin.firestore.Timestamp.now();
|
|
504
|
-
const serverTimestampValue = admin.firestore.FieldValue.serverTimestamp();
|
|
505
|
-
|
|
506
|
-
try {
|
|
507
|
-
// --- 1. Input Validation ---
|
|
508
|
-
if (
|
|
509
|
-
!data.patientId ||
|
|
510
|
-
!data.procedureId ||
|
|
511
|
-
!data.appointmentStartTime ||
|
|
512
|
-
!data.appointmentEndTime
|
|
513
|
-
) {
|
|
514
|
-
return {
|
|
515
|
-
success: false,
|
|
516
|
-
error:
|
|
517
|
-
"Missing required fields: patientId, procedureId, appointmentStartTime, or appointmentEndTime.",
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
if (
|
|
521
|
-
data.appointmentEndTime.toMillis() <=
|
|
522
|
-
data.appointmentStartTime.toMillis()
|
|
523
|
-
) {
|
|
524
|
-
return {
|
|
525
|
-
success: false,
|
|
526
|
-
error: "Appointment end time must be after start time.",
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
if (authenticatedUserId !== data.patientId) {
|
|
530
|
-
console.warn(
|
|
531
|
-
`[BookingAdmin] Authenticated user ${authenticatedUserId} is booking for a different patient ${data.patientId}. Review authorization if this is not intended.`
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// --- 2. Fetch Core Procedure Data ---
|
|
536
|
-
const procedureRef = this.db
|
|
537
|
-
.collection(PROCEDURES_COLLECTION)
|
|
538
|
-
.doc(data.procedureId);
|
|
539
|
-
const procedureDoc = await procedureRef.get();
|
|
540
|
-
if (!procedureDoc.exists) {
|
|
541
|
-
return {
|
|
542
|
-
success: false,
|
|
543
|
-
error: `Procedure ${data.procedureId} not found.`,
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
const procedure = procedureDoc.data() as Procedure;
|
|
547
|
-
|
|
548
|
-
// --- 3. Fetch Clinic and then other Primary Documents ---
|
|
549
|
-
// Fetch clinic first to get its clinicGroupId
|
|
550
|
-
const clinicRef = this.db
|
|
551
|
-
.collection(CLINICS_COLLECTION)
|
|
552
|
-
.doc(procedure.clinicBranchId);
|
|
553
|
-
const clinicSnap = await clinicRef.get(); // Await here directly
|
|
554
|
-
if (!clinicSnap.exists) {
|
|
555
|
-
return {
|
|
556
|
-
success: false,
|
|
557
|
-
error: `Clinic ${procedure.clinicBranchId} not found.`,
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
const clinicData = clinicSnap.data() as Clinic; // Now clinicData is available
|
|
561
|
-
|
|
562
|
-
// Define other refs using clinicData for clinicGroupRef
|
|
563
|
-
const practitionerRef = this.db
|
|
564
|
-
.collection(PRACTITIONERS_COLLECTION)
|
|
565
|
-
.doc(procedure.practitionerId);
|
|
566
|
-
const patientProfileRef = this.db
|
|
567
|
-
.collection(PATIENTS_COLLECTION)
|
|
568
|
-
.doc(data.patientId);
|
|
569
|
-
const patientSensitiveRef = this.db
|
|
570
|
-
.collection(PATIENTS_COLLECTION)
|
|
571
|
-
.doc(data.patientId)
|
|
572
|
-
.collection(PATIENT_SENSITIVE_INFO_COLLECTION)
|
|
573
|
-
.doc(data.patientId);
|
|
574
|
-
const clinicGroupRef = this.db
|
|
575
|
-
.collection(CLINIC_GROUPS_COLLECTION)
|
|
576
|
-
.doc(clinicData.clinicGroupId); // Use clinicData here
|
|
577
|
-
|
|
578
|
-
const [
|
|
579
|
-
practitionerSnap,
|
|
580
|
-
patientProfileSnap,
|
|
581
|
-
patientSensitiveSnap,
|
|
582
|
-
clinicGroupSnap,
|
|
583
|
-
] = await Promise.all([
|
|
584
|
-
// clinicRef.get() is already done via clinicSnap
|
|
585
|
-
practitionerRef.get(),
|
|
586
|
-
patientProfileRef.get(),
|
|
587
|
-
patientSensitiveRef.get(),
|
|
588
|
-
clinicGroupRef.get(), // Fetch the defined clinicGroupRef
|
|
589
|
-
]);
|
|
590
|
-
|
|
591
|
-
if (!practitionerSnap.exists)
|
|
592
|
-
return {
|
|
593
|
-
success: false,
|
|
594
|
-
error: `Practitioner ${procedure.practitionerId} not found.`,
|
|
595
|
-
};
|
|
596
|
-
if (!patientProfileSnap.exists)
|
|
597
|
-
return {
|
|
598
|
-
success: false,
|
|
599
|
-
error: `PatientProfile ${data.patientId} not found.`,
|
|
600
|
-
};
|
|
601
|
-
if (!clinicGroupSnap.exists)
|
|
602
|
-
return {
|
|
603
|
-
success: false,
|
|
604
|
-
error: `ClinicGroup for clinic ${procedure.clinicBranchId} not found.`,
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
const practitionerData = practitionerSnap.data() as Practitioner;
|
|
608
|
-
const patientProfileData = patientProfileSnap.data() as PatientProfile;
|
|
609
|
-
const patientSensitiveData = patientSensitiveSnap.exists
|
|
610
|
-
? (patientSensitiveSnap.data() as PatientSensitiveInfo)
|
|
611
|
-
: undefined;
|
|
612
|
-
const clinicGroupData = clinicGroupSnap.data() as ClinicGroup;
|
|
613
|
-
|
|
614
|
-
// --- 4. Determine initialStatus (based on clinic settings) ---
|
|
615
|
-
const autoConfirm = clinicGroupData.autoConfirmAppointments || false;
|
|
616
|
-
const initialAppointmentStatus = autoConfirm
|
|
617
|
-
? AppointmentStatus.CONFIRMED
|
|
618
|
-
: AppointmentStatus.PENDING;
|
|
619
|
-
const initialCalendarEventStatus = autoConfirm
|
|
620
|
-
? CalendarEventStatus.CONFIRMED
|
|
621
|
-
: CalendarEventStatus.PENDING;
|
|
622
|
-
|
|
623
|
-
// --- 5. Aggregate Information (Snapshots) ---
|
|
624
|
-
const clinicInfo: ClinicInfo = {
|
|
625
|
-
id: clinicSnap.id,
|
|
626
|
-
name: clinicData.name,
|
|
627
|
-
featuredPhoto:
|
|
628
|
-
(typeof clinicData.coverPhoto === "string"
|
|
629
|
-
? clinicData.coverPhoto
|
|
630
|
-
: "") ||
|
|
631
|
-
(typeof clinicData.logo === "string" ? clinicData.logo : "") ||
|
|
632
|
-
"",
|
|
633
|
-
description: clinicData.description,
|
|
634
|
-
location: clinicData.location,
|
|
635
|
-
contactInfo: clinicData.contactInfo,
|
|
636
|
-
};
|
|
637
|
-
const practitionerInfo: PractitionerProfileInfo = {
|
|
638
|
-
id: practitionerSnap.id,
|
|
639
|
-
practitionerPhoto:
|
|
640
|
-
typeof practitionerData.basicInfo.profileImageUrl === "string"
|
|
641
|
-
? practitionerData.basicInfo.profileImageUrl
|
|
642
|
-
: null,
|
|
643
|
-
name: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
|
|
644
|
-
email: practitionerData.basicInfo.email,
|
|
645
|
-
phone: practitionerData.basicInfo.phoneNumber || null,
|
|
646
|
-
certification: practitionerData.certification,
|
|
647
|
-
};
|
|
648
|
-
const patientInfo: PatientProfileInfo = {
|
|
649
|
-
id: patientProfileSnap.id,
|
|
650
|
-
fullName:
|
|
651
|
-
`${patientSensitiveData?.firstName || ""} ${
|
|
652
|
-
patientSensitiveData?.lastName || ""
|
|
653
|
-
}`.trim() || patientProfileData.displayName,
|
|
654
|
-
email: patientSensitiveData?.email || "",
|
|
655
|
-
phone:
|
|
656
|
-
patientSensitiveData?.phoneNumber ||
|
|
657
|
-
patientProfileData.phoneNumber ||
|
|
658
|
-
null,
|
|
659
|
-
dateOfBirth:
|
|
660
|
-
patientSensitiveData?.dateOfBirth ||
|
|
661
|
-
patientProfileData.dateOfBirth ||
|
|
662
|
-
admin.firestore.Timestamp.now(),
|
|
663
|
-
gender: patientSensitiveData?.gender || Gender.OTHER,
|
|
664
|
-
};
|
|
665
|
-
// const procedureCategory = procedure.category as Category;
|
|
666
|
-
// const procedureSubCategory = procedure.subcategory as Subcategory;
|
|
667
|
-
// const procedureTechnology = procedure.technology as Technology;
|
|
668
|
-
// const procedureProduct = procedure.product as Product;
|
|
669
|
-
|
|
670
|
-
// const procedureExtendedInfo: ProcedureExtendedInfo = {
|
|
671
|
-
// id: procedure.id,
|
|
672
|
-
// name: procedure.name,
|
|
673
|
-
// description: procedure.description,
|
|
674
|
-
// cost: procedure.price,
|
|
675
|
-
// duration: procedure.duration,
|
|
676
|
-
// procedureFamily: procedure.family,
|
|
677
|
-
// procedureCategoryId: procedureCategory?.id || "",
|
|
678
|
-
// procedureCategoryName: procedureCategory?.name || "",
|
|
679
|
-
// procedureSubCategoryId: procedureSubCategory?.id || "",
|
|
680
|
-
// procedureSubCategoryName: procedureSubCategory?.name || "",
|
|
681
|
-
// procedureTechnologyId: procedureTechnology?.id || "",
|
|
682
|
-
// procedureTechnologyName: procedureTechnology?.name || "",
|
|
683
|
-
// procedureProductBrandId: procedureProduct.brandId || "",
|
|
684
|
-
// procedureProductBrandName: procedureProduct.brandName || "",
|
|
685
|
-
// procedureProductId: procedureProduct.id || "",
|
|
686
|
-
// procedureProductName: procedureProduct.name || "",
|
|
687
|
-
// };
|
|
688
|
-
|
|
689
|
-
// // --- 6. Determine pendingUserFormsIds and linkedFormIds ---
|
|
690
|
-
// let pendingUserFormsIds: string[] = [];
|
|
691
|
-
// let linkedFormIds: string[] = [];
|
|
692
|
-
// if (
|
|
693
|
-
// procedure.documentationTemplates &&
|
|
694
|
-
// Array.isArray(procedure.documentationTemplates)
|
|
695
|
-
// ) {
|
|
696
|
-
// pendingUserFormsIds = procedure.documentationTemplates
|
|
697
|
-
// .filter(
|
|
698
|
-
// (template: TechnologyDocumentationTemplate) =>
|
|
699
|
-
// template.isUserForm && template.isRequired
|
|
700
|
-
// )
|
|
701
|
-
// .map(
|
|
702
|
-
// (template: TechnologyDocumentationTemplate) => template.templateId
|
|
703
|
-
// );
|
|
704
|
-
// linkedFormIds = procedure.documentationTemplates.map(
|
|
705
|
-
// (template: TechnologyDocumentationTemplate) => template.templateId
|
|
706
|
-
// );
|
|
707
|
-
// }
|
|
708
|
-
|
|
709
|
-
// --- 7. Construct New Appointment Object ---
|
|
710
|
-
const newAppointmentId = this.db
|
|
711
|
-
.collection(APPOINTMENTS_COLLECTION)
|
|
712
|
-
.doc().id;
|
|
713
|
-
|
|
714
|
-
const eventTimeForCalendarEvents: CalendarEventTime = {
|
|
715
|
-
start: data.appointmentStartTime as any,
|
|
716
|
-
end: data.appointmentEndTime as any,
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
// Practitioner Calendar Event
|
|
720
|
-
const practitionerCalendarEventId = this.db
|
|
721
|
-
.collection(PRACTITIONERS_COLLECTION)
|
|
722
|
-
.doc(practitionerData.id)
|
|
723
|
-
.collection(CALENDAR_COLLECTION)
|
|
724
|
-
.doc().id;
|
|
725
|
-
const practitionerCalendarEventData: CalendarEvent = {
|
|
726
|
-
id: practitionerCalendarEventId,
|
|
727
|
-
appointmentId: newAppointmentId,
|
|
728
|
-
clinicBranchId: clinicData.id,
|
|
729
|
-
clinicBranchInfo: clinicInfo,
|
|
730
|
-
practitionerProfileId: practitionerData.id,
|
|
731
|
-
practitionerProfileInfo: practitionerInfo,
|
|
732
|
-
patientProfileId: patientProfileData.id,
|
|
733
|
-
patientProfileInfo: patientInfo,
|
|
734
|
-
procedureId: procedure.id,
|
|
735
|
-
procedureInfo: this._generateCalendarProcedureInfo(procedure),
|
|
736
|
-
eventName: `Appointment: ${procedure.name} with ${patientInfo.fullName}`,
|
|
737
|
-
eventLocation: clinicData.location,
|
|
738
|
-
eventTime: eventTimeForCalendarEvents,
|
|
739
|
-
description: procedure.description || "",
|
|
740
|
-
status: initialCalendarEventStatus,
|
|
741
|
-
syncStatus: CalendarSyncStatus.INTERNAL,
|
|
742
|
-
eventType: CalendarEventType.APPOINTMENT,
|
|
743
|
-
createdAt: serverTimestampValue as any,
|
|
744
|
-
updatedAt: serverTimestampValue as any,
|
|
745
|
-
};
|
|
746
|
-
batch.set(
|
|
747
|
-
this.db
|
|
748
|
-
.collection(PRACTITIONERS_COLLECTION)
|
|
749
|
-
.doc(practitionerData.id)
|
|
750
|
-
.collection(CALENDAR_COLLECTION)
|
|
751
|
-
.doc(practitionerCalendarEventId),
|
|
752
|
-
practitionerCalendarEventData
|
|
753
|
-
);
|
|
754
|
-
|
|
755
|
-
// Patient Calendar Event
|
|
756
|
-
// Use same ID as practitionerCalendarEventId
|
|
757
|
-
const patientCalendarEventData: CalendarEvent = {
|
|
758
|
-
id: practitionerCalendarEventId,
|
|
759
|
-
appointmentId: newAppointmentId,
|
|
760
|
-
clinicBranchId: clinicData.id,
|
|
761
|
-
clinicBranchInfo: clinicInfo,
|
|
762
|
-
practitionerProfileId: practitionerData.id,
|
|
763
|
-
practitionerProfileInfo: practitionerInfo,
|
|
764
|
-
procedureId: procedure.id,
|
|
765
|
-
procedureInfo: this._generateCalendarProcedureInfo(procedure),
|
|
766
|
-
eventName: `Appointment: ${procedure.name} at ${clinicData.name}`,
|
|
767
|
-
eventLocation: clinicData.location,
|
|
768
|
-
eventTime: eventTimeForCalendarEvents,
|
|
769
|
-
description: data.patientNotes || "",
|
|
770
|
-
status: initialCalendarEventStatus,
|
|
771
|
-
syncStatus: CalendarSyncStatus.INTERNAL,
|
|
772
|
-
eventType: CalendarEventType.APPOINTMENT,
|
|
773
|
-
createdAt: serverTimestampValue as any,
|
|
774
|
-
updatedAt: serverTimestampValue as any,
|
|
775
|
-
};
|
|
776
|
-
batch.set(
|
|
777
|
-
this.db
|
|
778
|
-
.collection(PATIENTS_COLLECTION)
|
|
779
|
-
.doc(patientProfileData.id)
|
|
780
|
-
.collection(CALENDAR_COLLECTION)
|
|
781
|
-
.doc(practitionerCalendarEventId),
|
|
782
|
-
patientCalendarEventData
|
|
783
|
-
);
|
|
784
|
-
|
|
785
|
-
// Clinic Calendar Event
|
|
786
|
-
// Use same ID as practitionerCalendarEventId
|
|
787
|
-
const clinicCalendarEventData: CalendarEvent = {
|
|
788
|
-
id: practitionerCalendarEventId,
|
|
789
|
-
appointmentId: newAppointmentId,
|
|
790
|
-
clinicBranchId: clinicData.id,
|
|
791
|
-
clinicBranchInfo: clinicInfo,
|
|
792
|
-
practitionerProfileId: practitionerData.id,
|
|
793
|
-
practitionerProfileInfo: practitionerInfo,
|
|
794
|
-
patientProfileId: patientProfileData.id,
|
|
795
|
-
patientProfileInfo: patientInfo,
|
|
796
|
-
procedureId: procedure.id,
|
|
797
|
-
procedureInfo: this._generateCalendarProcedureInfo(procedure),
|
|
798
|
-
eventName: `Appointment: ${procedure.name} for ${patientInfo.fullName} with ${practitionerInfo.name}`,
|
|
799
|
-
eventLocation: clinicData.location,
|
|
800
|
-
eventTime: eventTimeForCalendarEvents,
|
|
801
|
-
description: data.patientNotes || "",
|
|
802
|
-
status: initialCalendarEventStatus,
|
|
803
|
-
syncStatus: CalendarSyncStatus.INTERNAL,
|
|
804
|
-
eventType: CalendarEventType.APPOINTMENT,
|
|
805
|
-
createdAt: serverTimestampValue as any,
|
|
806
|
-
updatedAt: serverTimestampValue as any,
|
|
807
|
-
};
|
|
808
|
-
batch.set(
|
|
809
|
-
this.db
|
|
810
|
-
.collection(CLINICS_COLLECTION)
|
|
811
|
-
.doc(clinicData.id)
|
|
812
|
-
.collection(CALENDAR_COLLECTION)
|
|
813
|
-
.doc(practitionerCalendarEventId),
|
|
814
|
-
clinicCalendarEventData
|
|
815
|
-
);
|
|
816
|
-
|
|
817
|
-
// --- Initialize Pending/Draft Filled Documents and get form IDs ---
|
|
818
|
-
let initializedFormsInfo: LinkedFormInfo[] = [];
|
|
819
|
-
let pendingUserFormTemplateIds: string[] = [];
|
|
820
|
-
let allLinkedFormTemplateIds: string[] = [];
|
|
821
|
-
|
|
822
|
-
if (
|
|
823
|
-
procedure.documentationTemplates &&
|
|
824
|
-
Array.isArray(procedure.documentationTemplates) &&
|
|
825
|
-
procedure.documentationTemplates.length > 0
|
|
826
|
-
) {
|
|
827
|
-
const formInitResult =
|
|
828
|
-
await this.documentManagerAdmin.batchInitializeAppointmentFormsFromTechnologyTemplates(
|
|
829
|
-
batch,
|
|
830
|
-
newAppointmentId,
|
|
831
|
-
procedure.id, // Pass the actual procedureId for the forms
|
|
832
|
-
procedure.documentationTemplates,
|
|
833
|
-
data.patientId,
|
|
834
|
-
procedure.practitionerId,
|
|
835
|
-
procedure.clinicBranchId,
|
|
836
|
-
adminTsNow.toMillis()
|
|
837
|
-
);
|
|
838
|
-
initializedFormsInfo = formInitResult.initializedFormsInfo;
|
|
839
|
-
pendingUserFormTemplateIds = formInitResult.pendingUserFormsIds;
|
|
840
|
-
allLinkedFormTemplateIds = formInitResult.allLinkedTemplateIds;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// --- Generate appointment products from procedure ---
|
|
844
|
-
const appointmentProducts = this._generateAppointmentProductsFromProcedure(procedure);
|
|
845
|
-
|
|
846
|
-
// --- Construct Appointment Object ---
|
|
847
|
-
const newAppointmentData: Appointment = {
|
|
848
|
-
id: newAppointmentId,
|
|
849
|
-
calendarEventId: practitionerCalendarEventId,
|
|
850
|
-
clinicBranchId: procedure.clinicBranchId,
|
|
851
|
-
clinicInfo,
|
|
852
|
-
clinic_tz: clinicData.location.tz || "UTC",
|
|
853
|
-
practitionerId: procedure.practitionerId,
|
|
854
|
-
practitionerInfo,
|
|
855
|
-
patientId: data.patientId,
|
|
856
|
-
patientInfo,
|
|
857
|
-
procedureId: data.procedureId,
|
|
858
|
-
procedureInfo: this._generateProcedureSummaryInfo(
|
|
859
|
-
procedure,
|
|
860
|
-
clinicData,
|
|
861
|
-
practitionerData
|
|
862
|
-
),
|
|
863
|
-
procedureExtendedInfo: this._generateProcedureExtendedInfo(procedure),
|
|
864
|
-
status: initialAppointmentStatus,
|
|
865
|
-
bookingTime: adminTsNow as any,
|
|
866
|
-
confirmationTime:
|
|
867
|
-
initialAppointmentStatus === AppointmentStatus.CONFIRMED
|
|
868
|
-
? (adminTsNow as any)
|
|
869
|
-
: null,
|
|
870
|
-
appointmentStartTime: data.appointmentStartTime as any,
|
|
871
|
-
appointmentEndTime: data.appointmentEndTime as any,
|
|
872
|
-
cost: procedure.price,
|
|
873
|
-
currency: procedure.currency,
|
|
874
|
-
paymentStatus:
|
|
875
|
-
procedure.price > 0
|
|
876
|
-
? PaymentStatus.UNPAID
|
|
877
|
-
: PaymentStatus.NOT_APPLICABLE,
|
|
878
|
-
patientNotes: data.patientNotes || null,
|
|
879
|
-
blockingConditions: procedure.blockingConditions || [],
|
|
880
|
-
contraindications: (procedure as any).contraindications || [],
|
|
881
|
-
preProcedureRequirements: procedure.preRequirements || [],
|
|
882
|
-
postProcedureRequirements: procedure.postRequirements || [],
|
|
883
|
-
pendingUserFormsIds: pendingUserFormTemplateIds,
|
|
884
|
-
linkedFormIds: allLinkedFormTemplateIds,
|
|
885
|
-
completedPreRequirements: [],
|
|
886
|
-
completedPostRequirements: [],
|
|
887
|
-
linkedForms: initializedFormsInfo,
|
|
888
|
-
media: [],
|
|
889
|
-
reviewInfo: null,
|
|
890
|
-
metadata: {
|
|
891
|
-
selectedZones: null,
|
|
892
|
-
zonePhotos: null,
|
|
893
|
-
zonesData: null,
|
|
894
|
-
appointmentProducts: appointmentProducts,
|
|
895
|
-
extendedProcedures: [],
|
|
896
|
-
recommendedProcedures: [],
|
|
897
|
-
finalbilling: null,
|
|
898
|
-
finalizationNotes: null,
|
|
899
|
-
},
|
|
900
|
-
finalizedDetails: {
|
|
901
|
-
by: "",
|
|
902
|
-
at: adminTsNow as any,
|
|
903
|
-
notes: "",
|
|
904
|
-
},
|
|
905
|
-
internalNotes: null,
|
|
906
|
-
cancellationReason: null,
|
|
907
|
-
cancellationTime: null,
|
|
908
|
-
canceledBy: undefined,
|
|
909
|
-
rescheduleTime: null,
|
|
910
|
-
procedureActualStartTime: null,
|
|
911
|
-
actualDurationMinutes: undefined,
|
|
912
|
-
isRecurring: false,
|
|
913
|
-
recurringAppointmentId: null,
|
|
914
|
-
isArchived: false,
|
|
915
|
-
createdAt: adminTsNow as any,
|
|
916
|
-
updatedAt: adminTsNow as any,
|
|
917
|
-
};
|
|
918
|
-
|
|
919
|
-
batch.set(
|
|
920
|
-
this.db.collection(APPOINTMENTS_COLLECTION).doc(newAppointmentId),
|
|
921
|
-
newAppointmentData
|
|
922
|
-
);
|
|
923
|
-
|
|
924
|
-
// Commit Batch
|
|
925
|
-
await batch.commit();
|
|
926
|
-
|
|
927
|
-
console.log(
|
|
928
|
-
`[BookingAdmin] Appointment ${newAppointmentId} and associated calendar events created successfully.`
|
|
929
|
-
);
|
|
930
|
-
return {
|
|
931
|
-
success: true,
|
|
932
|
-
appointmentId: newAppointmentId,
|
|
933
|
-
appointmentData: newAppointmentData,
|
|
934
|
-
calendarEventId: practitionerCalendarEventId,
|
|
935
|
-
};
|
|
936
|
-
} catch (error) {
|
|
937
|
-
console.error(
|
|
938
|
-
"[BookingAdmin] Critical error in orchestrateAppointmentCreation:",
|
|
939
|
-
error
|
|
940
|
-
);
|
|
941
|
-
const errorMessage =
|
|
942
|
-
error instanceof Error
|
|
943
|
-
? error.message
|
|
944
|
-
: "Unknown server error during appointment creation.";
|
|
945
|
-
return { success: false, error: errorMessage };
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
private _generateProcedureSummaryInfo(
|
|
950
|
-
procedure: Procedure,
|
|
951
|
-
clinicData: Clinic,
|
|
952
|
-
practitionerData: Practitioner
|
|
953
|
-
): ProcedureSummaryInfo {
|
|
954
|
-
const procedureCategory = procedure.category as Category;
|
|
955
|
-
const procedureSubCategory = procedure.subcategory as Subcategory;
|
|
956
|
-
const procedureTechnology = procedure.technology as Technology;
|
|
957
|
-
const procedureProduct = procedure.product as Product;
|
|
958
|
-
return {
|
|
959
|
-
id: procedure.id,
|
|
960
|
-
name: procedure.name,
|
|
961
|
-
description: procedure.description,
|
|
962
|
-
family: procedure.family,
|
|
963
|
-
categoryName: procedureCategory?.name || "",
|
|
964
|
-
subcategoryName: procedureSubCategory?.name || "",
|
|
965
|
-
technologyName: procedureTechnology?.name || "",
|
|
966
|
-
price: procedure.price,
|
|
967
|
-
pricingMeasure: procedure.pricingMeasure,
|
|
968
|
-
currency: procedure.currency,
|
|
969
|
-
duration: procedure.duration,
|
|
970
|
-
clinicId: procedure.clinicBranchId,
|
|
971
|
-
clinicName: clinicData.name,
|
|
972
|
-
practitionerId: procedure.practitionerId,
|
|
973
|
-
practitionerName: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
|
|
974
|
-
photo:
|
|
975
|
-
(procedureTechnology as any)?.photos?.[0]?.url ||
|
|
976
|
-
(procedureProduct as any)?.photos?.[0]?.url ||
|
|
977
|
-
"",
|
|
978
|
-
brandName: (procedureProduct as any)?.brand?.name || "",
|
|
979
|
-
productName: procedureProduct?.name || "",
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
private _generateProcedureExtendedInfo(
|
|
984
|
-
procedure: Procedure
|
|
985
|
-
): ProcedureExtendedInfo {
|
|
986
|
-
const procedureCategory = procedure.category as Category;
|
|
987
|
-
const procedureSubCategory = procedure.subcategory as Subcategory;
|
|
988
|
-
const procedureTechnology = procedure.technology as Technology;
|
|
989
|
-
const procedureProduct = procedure.product as Product;
|
|
990
|
-
const productsMetadata = procedure.productsMetadata || [];
|
|
991
|
-
|
|
992
|
-
return {
|
|
993
|
-
id: procedure.id,
|
|
994
|
-
name: procedure.name,
|
|
995
|
-
description: procedure.description,
|
|
996
|
-
cost: procedure.price,
|
|
997
|
-
duration: procedure.duration,
|
|
998
|
-
procedureFamily: procedure.family,
|
|
999
|
-
procedureCategoryId: procedureCategory?.id || "",
|
|
1000
|
-
procedureCategoryName: procedureCategory?.name || "",
|
|
1001
|
-
procedureSubCategoryId: procedureSubCategory?.id || "",
|
|
1002
|
-
procedureSubCategoryName: procedureSubCategory?.name || "",
|
|
1003
|
-
procedureTechnologyId: procedureTechnology?.id || "",
|
|
1004
|
-
procedureTechnologyName: procedureTechnology?.name || "",
|
|
1005
|
-
procedureProductBrandId: procedureProduct?.brandId || "",
|
|
1006
|
-
procedureProductBrandName: procedureProduct?.brandName || "",
|
|
1007
|
-
procedureProducts: productsMetadata
|
|
1008
|
-
.filter((pp: any) => pp && pp.product) // Safety check for product-free procedures
|
|
1009
|
-
.map((pp: any) => ({
|
|
1010
|
-
productId: pp.product.id,
|
|
1011
|
-
productName: pp.product.name,
|
|
1012
|
-
brandId: pp.product.brandId,
|
|
1013
|
-
brandName: pp.product.brandName,
|
|
1014
|
-
})),
|
|
1015
|
-
};
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
private _generateAppointmentProductsFromProcedure(procedure: Procedure): any[] {
|
|
1019
|
-
const productsMetadata = procedure.productsMetadata || [];
|
|
1020
|
-
|
|
1021
|
-
return productsMetadata
|
|
1022
|
-
.filter((pp: any) => pp && pp.product) // Safety check for product-free procedures
|
|
1023
|
-
.map((pp: any) => {
|
|
1024
|
-
const product = pp.product;
|
|
1025
|
-
return {
|
|
1026
|
-
productId: product.id,
|
|
1027
|
-
productName: product.name,
|
|
1028
|
-
brandId: product.brandId,
|
|
1029
|
-
brandName: product.brandName,
|
|
1030
|
-
procedureId: procedure.id,
|
|
1031
|
-
price: pp.price,
|
|
1032
|
-
currency: pp.currency,
|
|
1033
|
-
unitOfMeasurement: pp.pricingMeasure,
|
|
1034
|
-
};
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1
|
+
import * as admin from "firebase-admin";
|
|
2
|
+
import { Timestamp as FirebaseClientTimestamp } from "@firebase/firestore";
|
|
3
|
+
import {
|
|
4
|
+
BookingAvailabilityCalculator,
|
|
5
|
+
BookingAvailabilityRequest,
|
|
6
|
+
BookingAvailabilityResponse,
|
|
7
|
+
AvailableSlot,
|
|
8
|
+
} from "./";
|
|
9
|
+
import { CalendarEventStatus, CalendarEventType } from "../../types/calendar";
|
|
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 { TechnologyDocumentationTemplate } from "../../backoffice/types/technology.types";
|
|
48
|
+
import {
|
|
49
|
+
ClinicInfo,
|
|
50
|
+
PractitionerProfileInfo,
|
|
51
|
+
PatientProfileInfo,
|
|
52
|
+
} from "../../types/profile";
|
|
53
|
+
import { Category } from "../../backoffice/types/category.types";
|
|
54
|
+
import { Subcategory } from "../../backoffice/types/subcategory.types";
|
|
55
|
+
import { Technology } from "../../backoffice/types/technology.types";
|
|
56
|
+
import { Product } from "../../backoffice/types/product.types";
|
|
57
|
+
import {
|
|
58
|
+
CalendarEvent,
|
|
59
|
+
CalendarEventTime,
|
|
60
|
+
CalendarSyncStatus,
|
|
61
|
+
ProcedureInfo as CalendarProcedureInfo,
|
|
62
|
+
CALENDAR_COLLECTION,
|
|
63
|
+
} from "../../types/calendar";
|
|
64
|
+
import { DocumentManagerAdminService } from "../documentation-templates/document-manager.admin";
|
|
65
|
+
import { LinkedFormInfo } from "../../types/appointment";
|
|
66
|
+
import { TimestampUtils } from "../../utils/TimestampUtils";
|
|
67
|
+
import { Logger } from "../logger";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Interface for the data required by orchestrateAppointmentCreation
|
|
71
|
+
*/
|
|
72
|
+
export interface OrchestrateAppointmentCreationData {
|
|
73
|
+
patientId: string;
|
|
74
|
+
procedureId: string;
|
|
75
|
+
appointmentStartTime: admin.firestore.Timestamp;
|
|
76
|
+
appointmentEndTime: admin.firestore.Timestamp;
|
|
77
|
+
patientNotes?: string | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Admin service for handling booking-related operations.
|
|
82
|
+
* This is the cloud-based implementation that will be used in Cloud Functions.
|
|
83
|
+
*/
|
|
84
|
+
export class BookingAdmin {
|
|
85
|
+
private db: admin.firestore.Firestore;
|
|
86
|
+
private documentManagerAdmin: DocumentManagerAdminService;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Creates a new BookingAdmin instance
|
|
90
|
+
* @param firestore - Firestore instance provided by the caller
|
|
91
|
+
*/
|
|
92
|
+
constructor(firestore?: admin.firestore.Firestore) {
|
|
93
|
+
this.db = firestore || admin.firestore();
|
|
94
|
+
this.documentManagerAdmin = new DocumentManagerAdminService(this.db);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Gets available booking time slots for a specific clinic, practitioner, and procedure
|
|
99
|
+
*
|
|
100
|
+
* @param clinicId - ID of the clinic
|
|
101
|
+
* @param practitionerId - ID of the practitioner
|
|
102
|
+
* @param procedureId - ID of the procedure
|
|
103
|
+
* @param timeframe - Time range to check for availability
|
|
104
|
+
* @returns Promise resolving to an array of available booking slots
|
|
105
|
+
*/
|
|
106
|
+
async getAvailableBookingSlots(
|
|
107
|
+
clinicId: string,
|
|
108
|
+
practitionerId: string,
|
|
109
|
+
procedureId: string,
|
|
110
|
+
timeframe: {
|
|
111
|
+
start: Date | admin.firestore.Timestamp;
|
|
112
|
+
end: Date | admin.firestore.Timestamp;
|
|
113
|
+
}
|
|
114
|
+
): Promise<{ availableSlots: { start: admin.firestore.Timestamp }[] }> {
|
|
115
|
+
try {
|
|
116
|
+
Logger.info("[BookingAdmin] Starting availability calculation", {
|
|
117
|
+
clinicId,
|
|
118
|
+
practitionerId,
|
|
119
|
+
procedureId,
|
|
120
|
+
timeframeStart:
|
|
121
|
+
timeframe.start instanceof Date
|
|
122
|
+
? timeframe.start.toISOString()
|
|
123
|
+
: timeframe.start.toDate().toISOString(),
|
|
124
|
+
timeframeEnd:
|
|
125
|
+
timeframe.end instanceof Date
|
|
126
|
+
? timeframe.end.toISOString()
|
|
127
|
+
: timeframe.end.toDate().toISOString(),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Convert timeframe dates to Firestore Timestamps if needed
|
|
131
|
+
const start =
|
|
132
|
+
timeframe.start instanceof Date
|
|
133
|
+
? admin.firestore.Timestamp.fromDate(timeframe.start)
|
|
134
|
+
: timeframe.start;
|
|
135
|
+
|
|
136
|
+
const end =
|
|
137
|
+
timeframe.end instanceof Date
|
|
138
|
+
? admin.firestore.Timestamp.fromDate(timeframe.end)
|
|
139
|
+
: timeframe.end;
|
|
140
|
+
|
|
141
|
+
// 1. Fetch clinic data
|
|
142
|
+
Logger.debug("[BookingAdmin] Fetching clinic data", { clinicId });
|
|
143
|
+
const clinicDoc = await this.db.collection("clinics").doc(clinicId).get();
|
|
144
|
+
if (!clinicDoc.exists) {
|
|
145
|
+
Logger.error("[BookingAdmin] Clinic not found", { clinicId });
|
|
146
|
+
throw new Error(`Clinic ${clinicId} not found`);
|
|
147
|
+
}
|
|
148
|
+
const clinic = clinicDoc.data() as unknown as Clinic;
|
|
149
|
+
Logger.debug("[BookingAdmin] Retrieved clinic data", {
|
|
150
|
+
clinicName: clinic.name,
|
|
151
|
+
clinicHasWorkingHours: !!clinic.workingHours,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// 2. Fetch practitioner data
|
|
155
|
+
Logger.debug("[BookingAdmin] Fetching practitioner data", {
|
|
156
|
+
practitionerId,
|
|
157
|
+
});
|
|
158
|
+
const practitionerDoc = await this.db
|
|
159
|
+
.collection("practitioners")
|
|
160
|
+
.doc(practitionerId)
|
|
161
|
+
.get();
|
|
162
|
+
if (!practitionerDoc.exists) {
|
|
163
|
+
Logger.error("[BookingAdmin] Practitioner not found", {
|
|
164
|
+
practitionerId,
|
|
165
|
+
});
|
|
166
|
+
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
167
|
+
}
|
|
168
|
+
const practitioner = practitionerDoc.data() as unknown as Practitioner;
|
|
169
|
+
Logger.debug("[BookingAdmin] Retrieved practitioner data", {
|
|
170
|
+
practitionerName: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
171
|
+
pracWorkingHoursCount: practitioner.clinicWorkingHours?.length || 0,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// 3. Fetch procedure data
|
|
175
|
+
Logger.debug("[BookingAdmin] Fetching procedure data", { procedureId });
|
|
176
|
+
const procedureDoc = await this.db
|
|
177
|
+
.collection("procedures")
|
|
178
|
+
.doc(procedureId)
|
|
179
|
+
.get();
|
|
180
|
+
if (!procedureDoc.exists) {
|
|
181
|
+
Logger.error("[BookingAdmin] Procedure not found", { procedureId });
|
|
182
|
+
throw new Error(`Procedure ${procedureId} not found`);
|
|
183
|
+
}
|
|
184
|
+
const procedure = procedureDoc.data() as unknown as Procedure;
|
|
185
|
+
Logger.debug("[BookingAdmin] Retrieved procedure data", {
|
|
186
|
+
procedureName: procedure.name,
|
|
187
|
+
procedureDuration: procedure.duration,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// 4. Fetch clinic calendar events
|
|
191
|
+
Logger.debug("[BookingAdmin] Fetching clinic calendar events", {
|
|
192
|
+
clinicId,
|
|
193
|
+
startTime: start.toDate().toISOString(),
|
|
194
|
+
endTime: end.toDate().toISOString(),
|
|
195
|
+
});
|
|
196
|
+
const clinicCalendarEvents = await this.getClinicCalendarEvents(
|
|
197
|
+
clinicId,
|
|
198
|
+
start,
|
|
199
|
+
end
|
|
200
|
+
);
|
|
201
|
+
Logger.debug("[BookingAdmin] Retrieved clinic calendar events", {
|
|
202
|
+
count: clinicCalendarEvents.length,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// 5. Fetch practitioner calendar events
|
|
206
|
+
Logger.debug("[BookingAdmin] Fetching practitioner calendar events", {
|
|
207
|
+
practitionerId,
|
|
208
|
+
startTime: start.toDate().toISOString(),
|
|
209
|
+
endTime: end.toDate().toISOString(),
|
|
210
|
+
});
|
|
211
|
+
const practitionerCalendarEvents =
|
|
212
|
+
await this.getPractitionerCalendarEvents(practitionerId, start, end);
|
|
213
|
+
Logger.debug("[BookingAdmin] Retrieved practitioner calendar events", {
|
|
214
|
+
count: practitionerCalendarEvents.length,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Since we're working with two different Timestamp implementations (admin vs client),
|
|
218
|
+
// we need to convert our timestamps to the client-side format expected by the calculator
|
|
219
|
+
// Create client Timestamp objects from admin Timestamp objects
|
|
220
|
+
const convertedTimeframe = {
|
|
221
|
+
start: this.adminTimestampToClientTimestamp(start),
|
|
222
|
+
end: this.adminTimestampToClientTimestamp(end),
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Create the request object for the calculator
|
|
226
|
+
const request: BookingAvailabilityRequest = {
|
|
227
|
+
clinic,
|
|
228
|
+
practitioner,
|
|
229
|
+
procedure,
|
|
230
|
+
timeframe: convertedTimeframe,
|
|
231
|
+
clinicCalendarEvents:
|
|
232
|
+
this.convertEventsTimestamps(clinicCalendarEvents),
|
|
233
|
+
practitionerCalendarEvents: this.convertEventsTimestamps(
|
|
234
|
+
practitionerCalendarEvents
|
|
235
|
+
),
|
|
236
|
+
tz: clinic.location.tz || "UTC",
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
Logger.info("[BookingAdmin] Calling availability calculator", {
|
|
240
|
+
calculatorInputReady: true,
|
|
241
|
+
timeframeDurationHours: Math.round(
|
|
242
|
+
(end.toMillis() - start.toMillis()) / (1000 * 60 * 60)
|
|
243
|
+
),
|
|
244
|
+
clinicEventsCount: clinicCalendarEvents.length,
|
|
245
|
+
practitionerEventsCount: practitionerCalendarEvents.length,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Use the calculator to compute available slots
|
|
249
|
+
const result = BookingAvailabilityCalculator.calculateSlots(request);
|
|
250
|
+
|
|
251
|
+
// Convert the client Timestamps to admin Timestamps before returning
|
|
252
|
+
const availableSlotsResult = {
|
|
253
|
+
availableSlots: result.availableSlots.map((slot) => ({
|
|
254
|
+
start: admin.firestore.Timestamp.fromMillis(slot.start.toMillis()),
|
|
255
|
+
})),
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
Logger.info(
|
|
259
|
+
"[BookingAdmin] Availability calculation completed successfully",
|
|
260
|
+
{
|
|
261
|
+
availableSlotsCount: availableSlotsResult.availableSlots.length,
|
|
262
|
+
firstSlotTime:
|
|
263
|
+
availableSlotsResult.availableSlots.length > 0
|
|
264
|
+
? availableSlotsResult.availableSlots[0].start
|
|
265
|
+
.toDate()
|
|
266
|
+
.toISOString()
|
|
267
|
+
: "none",
|
|
268
|
+
lastSlotTime:
|
|
269
|
+
availableSlotsResult.availableSlots.length > 0
|
|
270
|
+
? availableSlotsResult.availableSlots[
|
|
271
|
+
availableSlotsResult.availableSlots.length - 1
|
|
272
|
+
].start
|
|
273
|
+
.toDate()
|
|
274
|
+
.toISOString()
|
|
275
|
+
: "none",
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
return availableSlotsResult;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
const errorMessage =
|
|
282
|
+
error instanceof Error ? error.message : String(error);
|
|
283
|
+
Logger.error("[BookingAdmin] Error getting available slots", {
|
|
284
|
+
errorMessage,
|
|
285
|
+
clinicId,
|
|
286
|
+
practitionerId,
|
|
287
|
+
procedureId,
|
|
288
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
289
|
+
});
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Converts an admin Firestore Timestamp to a client Firestore Timestamp
|
|
296
|
+
*/
|
|
297
|
+
private adminTimestampToClientTimestamp(
|
|
298
|
+
timestamp: admin.firestore.Timestamp
|
|
299
|
+
): FirebaseClientTimestamp {
|
|
300
|
+
// Use TimestampUtils instead of custom implementation
|
|
301
|
+
return TimestampUtils.adminToClient(timestamp) as FirebaseClientTimestamp;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Converts timestamps in calendar events from admin Firestore Timestamps to client Firestore Timestamps
|
|
306
|
+
*/
|
|
307
|
+
private convertEventsTimestamps(events: any[]): any[] {
|
|
308
|
+
return events.map((event) => ({
|
|
309
|
+
...event,
|
|
310
|
+
eventTime: {
|
|
311
|
+
start: TimestampUtils.adminToClient(
|
|
312
|
+
event.eventTime.start
|
|
313
|
+
) as FirebaseClientTimestamp,
|
|
314
|
+
end: TimestampUtils.adminToClient(
|
|
315
|
+
event.eventTime.end
|
|
316
|
+
) as FirebaseClientTimestamp,
|
|
317
|
+
},
|
|
318
|
+
// Convert any other timestamps in the event if needed
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Fetches clinic calendar events for a specific time range
|
|
324
|
+
*
|
|
325
|
+
* @param clinicId - ID of the clinic
|
|
326
|
+
* @param start - Start time of the range
|
|
327
|
+
* @param end - End time of the range
|
|
328
|
+
* @returns Promise resolving to an array of calendar events
|
|
329
|
+
*/
|
|
330
|
+
private async getClinicCalendarEvents(
|
|
331
|
+
clinicId: string,
|
|
332
|
+
start: admin.firestore.Timestamp,
|
|
333
|
+
end: admin.firestore.Timestamp
|
|
334
|
+
): Promise<any[]> {
|
|
335
|
+
try {
|
|
336
|
+
Logger.debug("[BookingAdmin] Querying clinic calendar events", {
|
|
337
|
+
clinicId,
|
|
338
|
+
startTime: start.toDate().toISOString(),
|
|
339
|
+
endTime: end.toDate().toISOString(),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const MAX_EVENT_DURATION_MS = 24 * 60 * 60 * 1000;
|
|
343
|
+
const queryStart = admin.firestore.Timestamp.fromMillis(
|
|
344
|
+
start.toMillis() - MAX_EVENT_DURATION_MS
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const eventsRef = this.db
|
|
348
|
+
.collection(`clinics/${clinicId}/calendar`)
|
|
349
|
+
.where("eventTime.start", ">=", queryStart)
|
|
350
|
+
.where("eventTime.start", "<", end)
|
|
351
|
+
.orderBy("eventTime.start");
|
|
352
|
+
|
|
353
|
+
const snapshot = await eventsRef.get();
|
|
354
|
+
|
|
355
|
+
const events = snapshot.docs
|
|
356
|
+
.map((doc) => ({
|
|
357
|
+
...doc.data(),
|
|
358
|
+
id: doc.id,
|
|
359
|
+
}))
|
|
360
|
+
.filter((event: any) => {
|
|
361
|
+
return event.eventTime.end.toMillis() > start.toMillis();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
Logger.debug("[BookingAdmin] Retrieved clinic calendar events", {
|
|
365
|
+
clinicId,
|
|
366
|
+
eventsCount: events.length,
|
|
367
|
+
eventsTypes: this.summarizeEventTypes(events),
|
|
368
|
+
queryStartTime: queryStart.toDate().toISOString(),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
return events;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
const errorMessage =
|
|
374
|
+
error instanceof Error ? error.message : String(error);
|
|
375
|
+
Logger.error("[BookingAdmin] Error fetching clinic calendar events", {
|
|
376
|
+
errorMessage,
|
|
377
|
+
clinicId,
|
|
378
|
+
startTime: start.toDate().toISOString(),
|
|
379
|
+
endTime: end.toDate().toISOString(),
|
|
380
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
381
|
+
});
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Fetches practitioner calendar events for a specific time range
|
|
388
|
+
*
|
|
389
|
+
* @param practitionerId - ID of the practitioner
|
|
390
|
+
* @param start - Start time of the range
|
|
391
|
+
* @param end - End time of the range
|
|
392
|
+
* @returns Promise resolving to an array of calendar events
|
|
393
|
+
*/
|
|
394
|
+
private async getPractitionerCalendarEvents(
|
|
395
|
+
practitionerId: string,
|
|
396
|
+
start: admin.firestore.Timestamp,
|
|
397
|
+
end: admin.firestore.Timestamp
|
|
398
|
+
): Promise<any[]> {
|
|
399
|
+
try {
|
|
400
|
+
Logger.debug("[BookingAdmin] Querying practitioner calendar events", {
|
|
401
|
+
practitionerId,
|
|
402
|
+
startTime: start.toDate().toISOString(),
|
|
403
|
+
endTime: end.toDate().toISOString(),
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const MAX_EVENT_DURATION_MS = 24 * 60 * 60 * 1000;
|
|
407
|
+
const queryStart = admin.firestore.Timestamp.fromMillis(
|
|
408
|
+
start.toMillis() - MAX_EVENT_DURATION_MS
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const eventsRef = this.db
|
|
412
|
+
.collection(`practitioners/${practitionerId}/calendar`)
|
|
413
|
+
.where("eventTime.start", ">=", queryStart)
|
|
414
|
+
.where("eventTime.start", "<", end)
|
|
415
|
+
.orderBy("eventTime.start");
|
|
416
|
+
|
|
417
|
+
const snapshot = await eventsRef.get();
|
|
418
|
+
|
|
419
|
+
const events = snapshot.docs
|
|
420
|
+
.map((doc) => ({
|
|
421
|
+
...doc.data(),
|
|
422
|
+
id: doc.id,
|
|
423
|
+
}))
|
|
424
|
+
.filter((event: any) => {
|
|
425
|
+
return event.eventTime.end.toMillis() > start.toMillis();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
Logger.debug("[BookingAdmin] Retrieved practitioner calendar events", {
|
|
429
|
+
practitionerId,
|
|
430
|
+
eventsCount: events.length,
|
|
431
|
+
eventsTypes: this.summarizeEventTypes(events),
|
|
432
|
+
queryStartTime: queryStart.toDate().toISOString(),
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
return events;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
const errorMessage =
|
|
438
|
+
error instanceof Error ? error.message : String(error);
|
|
439
|
+
Logger.error(
|
|
440
|
+
"[BookingAdmin] Error fetching practitioner calendar events",
|
|
441
|
+
{
|
|
442
|
+
errorMessage,
|
|
443
|
+
practitionerId,
|
|
444
|
+
startTime: start.toDate().toISOString(),
|
|
445
|
+
endTime: end.toDate().toISOString(),
|
|
446
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
return [];
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Summarizes event types for logging purposes
|
|
455
|
+
* @param events Array of calendar events
|
|
456
|
+
* @returns Object with counts of each event type
|
|
457
|
+
*/
|
|
458
|
+
private summarizeEventTypes(events: any[]): Record<string, number> {
|
|
459
|
+
const typeCounts: Record<string, number> = {};
|
|
460
|
+
|
|
461
|
+
events.forEach((event) => {
|
|
462
|
+
const eventType = event.eventType || "unknown";
|
|
463
|
+
typeCounts[eventType] = (typeCounts[eventType] || 0) + 1;
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
return typeCounts;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private _generateCalendarProcedureInfo(
|
|
470
|
+
procedure: Procedure
|
|
471
|
+
): CalendarProcedureInfo {
|
|
472
|
+
return {
|
|
473
|
+
name: procedure.name,
|
|
474
|
+
description: procedure.description,
|
|
475
|
+
duration: procedure.duration, // in minutes
|
|
476
|
+
price: procedure.price,
|
|
477
|
+
currency: procedure.currency,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Orchestrates the creation of a new appointment, including data aggregation.
|
|
483
|
+
* This method is intended to be called from a trusted backend environment (e.g., an Express route handler in a Cloud Function).
|
|
484
|
+
*
|
|
485
|
+
* @param data - Data required to create the appointment.
|
|
486
|
+
* @param authenticatedUserId - The ID of the user making the request (for auditing, and usually is the patientId).
|
|
487
|
+
* @returns Promise resolving to an object indicating success, and appointmentId or an error message.
|
|
488
|
+
*/
|
|
489
|
+
async orchestrateAppointmentCreation(
|
|
490
|
+
data: OrchestrateAppointmentCreationData,
|
|
491
|
+
authenticatedUserId: string
|
|
492
|
+
): Promise<{
|
|
493
|
+
success: boolean;
|
|
494
|
+
appointmentId?: string;
|
|
495
|
+
appointmentData?: Appointment;
|
|
496
|
+
calendarEventId?: string;
|
|
497
|
+
error?: string;
|
|
498
|
+
}> {
|
|
499
|
+
console.log(
|
|
500
|
+
`[BookingAdmin] Orchestrating appointment creation for patient ${data.patientId} by user ${authenticatedUserId}`
|
|
501
|
+
);
|
|
502
|
+
const batch = this.db.batch();
|
|
503
|
+
const adminTsNow = admin.firestore.Timestamp.now();
|
|
504
|
+
const serverTimestampValue = admin.firestore.FieldValue.serverTimestamp();
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
// --- 1. Input Validation ---
|
|
508
|
+
if (
|
|
509
|
+
!data.patientId ||
|
|
510
|
+
!data.procedureId ||
|
|
511
|
+
!data.appointmentStartTime ||
|
|
512
|
+
!data.appointmentEndTime
|
|
513
|
+
) {
|
|
514
|
+
return {
|
|
515
|
+
success: false,
|
|
516
|
+
error:
|
|
517
|
+
"Missing required fields: patientId, procedureId, appointmentStartTime, or appointmentEndTime.",
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (
|
|
521
|
+
data.appointmentEndTime.toMillis() <=
|
|
522
|
+
data.appointmentStartTime.toMillis()
|
|
523
|
+
) {
|
|
524
|
+
return {
|
|
525
|
+
success: false,
|
|
526
|
+
error: "Appointment end time must be after start time.",
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
if (authenticatedUserId !== data.patientId) {
|
|
530
|
+
console.warn(
|
|
531
|
+
`[BookingAdmin] Authenticated user ${authenticatedUserId} is booking for a different patient ${data.patientId}. Review authorization if this is not intended.`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// --- 2. Fetch Core Procedure Data ---
|
|
536
|
+
const procedureRef = this.db
|
|
537
|
+
.collection(PROCEDURES_COLLECTION)
|
|
538
|
+
.doc(data.procedureId);
|
|
539
|
+
const procedureDoc = await procedureRef.get();
|
|
540
|
+
if (!procedureDoc.exists) {
|
|
541
|
+
return {
|
|
542
|
+
success: false,
|
|
543
|
+
error: `Procedure ${data.procedureId} not found.`,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
const procedure = procedureDoc.data() as Procedure;
|
|
547
|
+
|
|
548
|
+
// --- 3. Fetch Clinic and then other Primary Documents ---
|
|
549
|
+
// Fetch clinic first to get its clinicGroupId
|
|
550
|
+
const clinicRef = this.db
|
|
551
|
+
.collection(CLINICS_COLLECTION)
|
|
552
|
+
.doc(procedure.clinicBranchId);
|
|
553
|
+
const clinicSnap = await clinicRef.get(); // Await here directly
|
|
554
|
+
if (!clinicSnap.exists) {
|
|
555
|
+
return {
|
|
556
|
+
success: false,
|
|
557
|
+
error: `Clinic ${procedure.clinicBranchId} not found.`,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
const clinicData = clinicSnap.data() as Clinic; // Now clinicData is available
|
|
561
|
+
|
|
562
|
+
// Define other refs using clinicData for clinicGroupRef
|
|
563
|
+
const practitionerRef = this.db
|
|
564
|
+
.collection(PRACTITIONERS_COLLECTION)
|
|
565
|
+
.doc(procedure.practitionerId);
|
|
566
|
+
const patientProfileRef = this.db
|
|
567
|
+
.collection(PATIENTS_COLLECTION)
|
|
568
|
+
.doc(data.patientId);
|
|
569
|
+
const patientSensitiveRef = this.db
|
|
570
|
+
.collection(PATIENTS_COLLECTION)
|
|
571
|
+
.doc(data.patientId)
|
|
572
|
+
.collection(PATIENT_SENSITIVE_INFO_COLLECTION)
|
|
573
|
+
.doc(data.patientId);
|
|
574
|
+
const clinicGroupRef = this.db
|
|
575
|
+
.collection(CLINIC_GROUPS_COLLECTION)
|
|
576
|
+
.doc(clinicData.clinicGroupId); // Use clinicData here
|
|
577
|
+
|
|
578
|
+
const [
|
|
579
|
+
practitionerSnap,
|
|
580
|
+
patientProfileSnap,
|
|
581
|
+
patientSensitiveSnap,
|
|
582
|
+
clinicGroupSnap,
|
|
583
|
+
] = await Promise.all([
|
|
584
|
+
// clinicRef.get() is already done via clinicSnap
|
|
585
|
+
practitionerRef.get(),
|
|
586
|
+
patientProfileRef.get(),
|
|
587
|
+
patientSensitiveRef.get(),
|
|
588
|
+
clinicGroupRef.get(), // Fetch the defined clinicGroupRef
|
|
589
|
+
]);
|
|
590
|
+
|
|
591
|
+
if (!practitionerSnap.exists)
|
|
592
|
+
return {
|
|
593
|
+
success: false,
|
|
594
|
+
error: `Practitioner ${procedure.practitionerId} not found.`,
|
|
595
|
+
};
|
|
596
|
+
if (!patientProfileSnap.exists)
|
|
597
|
+
return {
|
|
598
|
+
success: false,
|
|
599
|
+
error: `PatientProfile ${data.patientId} not found.`,
|
|
600
|
+
};
|
|
601
|
+
if (!clinicGroupSnap.exists)
|
|
602
|
+
return {
|
|
603
|
+
success: false,
|
|
604
|
+
error: `ClinicGroup for clinic ${procedure.clinicBranchId} not found.`,
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const practitionerData = practitionerSnap.data() as Practitioner;
|
|
608
|
+
const patientProfileData = patientProfileSnap.data() as PatientProfile;
|
|
609
|
+
const patientSensitiveData = patientSensitiveSnap.exists
|
|
610
|
+
? (patientSensitiveSnap.data() as PatientSensitiveInfo)
|
|
611
|
+
: undefined;
|
|
612
|
+
const clinicGroupData = clinicGroupSnap.data() as ClinicGroup;
|
|
613
|
+
|
|
614
|
+
// --- 4. Determine initialStatus (based on clinic settings) ---
|
|
615
|
+
const autoConfirm = clinicGroupData.autoConfirmAppointments || false;
|
|
616
|
+
const initialAppointmentStatus = autoConfirm
|
|
617
|
+
? AppointmentStatus.CONFIRMED
|
|
618
|
+
: AppointmentStatus.PENDING;
|
|
619
|
+
const initialCalendarEventStatus = autoConfirm
|
|
620
|
+
? CalendarEventStatus.CONFIRMED
|
|
621
|
+
: CalendarEventStatus.PENDING;
|
|
622
|
+
|
|
623
|
+
// --- 5. Aggregate Information (Snapshots) ---
|
|
624
|
+
const clinicInfo: ClinicInfo = {
|
|
625
|
+
id: clinicSnap.id,
|
|
626
|
+
name: clinicData.name,
|
|
627
|
+
featuredPhoto:
|
|
628
|
+
(typeof clinicData.coverPhoto === "string"
|
|
629
|
+
? clinicData.coverPhoto
|
|
630
|
+
: "") ||
|
|
631
|
+
(typeof clinicData.logo === "string" ? clinicData.logo : "") ||
|
|
632
|
+
"",
|
|
633
|
+
description: clinicData.description,
|
|
634
|
+
location: clinicData.location,
|
|
635
|
+
contactInfo: clinicData.contactInfo,
|
|
636
|
+
};
|
|
637
|
+
const practitionerInfo: PractitionerProfileInfo = {
|
|
638
|
+
id: practitionerSnap.id,
|
|
639
|
+
practitionerPhoto:
|
|
640
|
+
typeof practitionerData.basicInfo.profileImageUrl === "string"
|
|
641
|
+
? practitionerData.basicInfo.profileImageUrl
|
|
642
|
+
: null,
|
|
643
|
+
name: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
|
|
644
|
+
email: practitionerData.basicInfo.email,
|
|
645
|
+
phone: practitionerData.basicInfo.phoneNumber || null,
|
|
646
|
+
certification: practitionerData.certification,
|
|
647
|
+
};
|
|
648
|
+
const patientInfo: PatientProfileInfo = {
|
|
649
|
+
id: patientProfileSnap.id,
|
|
650
|
+
fullName:
|
|
651
|
+
`${patientSensitiveData?.firstName || ""} ${
|
|
652
|
+
patientSensitiveData?.lastName || ""
|
|
653
|
+
}`.trim() || patientProfileData.displayName,
|
|
654
|
+
email: patientSensitiveData?.email || "",
|
|
655
|
+
phone:
|
|
656
|
+
patientSensitiveData?.phoneNumber ||
|
|
657
|
+
patientProfileData.phoneNumber ||
|
|
658
|
+
null,
|
|
659
|
+
dateOfBirth:
|
|
660
|
+
patientSensitiveData?.dateOfBirth ||
|
|
661
|
+
patientProfileData.dateOfBirth ||
|
|
662
|
+
admin.firestore.Timestamp.now(),
|
|
663
|
+
gender: patientSensitiveData?.gender || Gender.OTHER,
|
|
664
|
+
};
|
|
665
|
+
// const procedureCategory = procedure.category as Category;
|
|
666
|
+
// const procedureSubCategory = procedure.subcategory as Subcategory;
|
|
667
|
+
// const procedureTechnology = procedure.technology as Technology;
|
|
668
|
+
// const procedureProduct = procedure.product as Product;
|
|
669
|
+
|
|
670
|
+
// const procedureExtendedInfo: ProcedureExtendedInfo = {
|
|
671
|
+
// id: procedure.id,
|
|
672
|
+
// name: procedure.name,
|
|
673
|
+
// description: procedure.description,
|
|
674
|
+
// cost: procedure.price,
|
|
675
|
+
// duration: procedure.duration,
|
|
676
|
+
// procedureFamily: procedure.family,
|
|
677
|
+
// procedureCategoryId: procedureCategory?.id || "",
|
|
678
|
+
// procedureCategoryName: procedureCategory?.name || "",
|
|
679
|
+
// procedureSubCategoryId: procedureSubCategory?.id || "",
|
|
680
|
+
// procedureSubCategoryName: procedureSubCategory?.name || "",
|
|
681
|
+
// procedureTechnologyId: procedureTechnology?.id || "",
|
|
682
|
+
// procedureTechnologyName: procedureTechnology?.name || "",
|
|
683
|
+
// procedureProductBrandId: procedureProduct.brandId || "",
|
|
684
|
+
// procedureProductBrandName: procedureProduct.brandName || "",
|
|
685
|
+
// procedureProductId: procedureProduct.id || "",
|
|
686
|
+
// procedureProductName: procedureProduct.name || "",
|
|
687
|
+
// };
|
|
688
|
+
|
|
689
|
+
// // --- 6. Determine pendingUserFormsIds and linkedFormIds ---
|
|
690
|
+
// let pendingUserFormsIds: string[] = [];
|
|
691
|
+
// let linkedFormIds: string[] = [];
|
|
692
|
+
// if (
|
|
693
|
+
// procedure.documentationTemplates &&
|
|
694
|
+
// Array.isArray(procedure.documentationTemplates)
|
|
695
|
+
// ) {
|
|
696
|
+
// pendingUserFormsIds = procedure.documentationTemplates
|
|
697
|
+
// .filter(
|
|
698
|
+
// (template: TechnologyDocumentationTemplate) =>
|
|
699
|
+
// template.isUserForm && template.isRequired
|
|
700
|
+
// )
|
|
701
|
+
// .map(
|
|
702
|
+
// (template: TechnologyDocumentationTemplate) => template.templateId
|
|
703
|
+
// );
|
|
704
|
+
// linkedFormIds = procedure.documentationTemplates.map(
|
|
705
|
+
// (template: TechnologyDocumentationTemplate) => template.templateId
|
|
706
|
+
// );
|
|
707
|
+
// }
|
|
708
|
+
|
|
709
|
+
// --- 7. Construct New Appointment Object ---
|
|
710
|
+
const newAppointmentId = this.db
|
|
711
|
+
.collection(APPOINTMENTS_COLLECTION)
|
|
712
|
+
.doc().id;
|
|
713
|
+
|
|
714
|
+
const eventTimeForCalendarEvents: CalendarEventTime = {
|
|
715
|
+
start: data.appointmentStartTime as any,
|
|
716
|
+
end: data.appointmentEndTime as any,
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
// Practitioner Calendar Event
|
|
720
|
+
const practitionerCalendarEventId = this.db
|
|
721
|
+
.collection(PRACTITIONERS_COLLECTION)
|
|
722
|
+
.doc(practitionerData.id)
|
|
723
|
+
.collection(CALENDAR_COLLECTION)
|
|
724
|
+
.doc().id;
|
|
725
|
+
const practitionerCalendarEventData: CalendarEvent = {
|
|
726
|
+
id: practitionerCalendarEventId,
|
|
727
|
+
appointmentId: newAppointmentId,
|
|
728
|
+
clinicBranchId: clinicData.id,
|
|
729
|
+
clinicBranchInfo: clinicInfo,
|
|
730
|
+
practitionerProfileId: practitionerData.id,
|
|
731
|
+
practitionerProfileInfo: practitionerInfo,
|
|
732
|
+
patientProfileId: patientProfileData.id,
|
|
733
|
+
patientProfileInfo: patientInfo,
|
|
734
|
+
procedureId: procedure.id,
|
|
735
|
+
procedureInfo: this._generateCalendarProcedureInfo(procedure),
|
|
736
|
+
eventName: `Appointment: ${procedure.name} with ${patientInfo.fullName}`,
|
|
737
|
+
eventLocation: clinicData.location,
|
|
738
|
+
eventTime: eventTimeForCalendarEvents,
|
|
739
|
+
description: procedure.description || "",
|
|
740
|
+
status: initialCalendarEventStatus,
|
|
741
|
+
syncStatus: CalendarSyncStatus.INTERNAL,
|
|
742
|
+
eventType: CalendarEventType.APPOINTMENT,
|
|
743
|
+
createdAt: serverTimestampValue as any,
|
|
744
|
+
updatedAt: serverTimestampValue as any,
|
|
745
|
+
};
|
|
746
|
+
batch.set(
|
|
747
|
+
this.db
|
|
748
|
+
.collection(PRACTITIONERS_COLLECTION)
|
|
749
|
+
.doc(practitionerData.id)
|
|
750
|
+
.collection(CALENDAR_COLLECTION)
|
|
751
|
+
.doc(practitionerCalendarEventId),
|
|
752
|
+
practitionerCalendarEventData
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
// Patient Calendar Event
|
|
756
|
+
// Use same ID as practitionerCalendarEventId
|
|
757
|
+
const patientCalendarEventData: CalendarEvent = {
|
|
758
|
+
id: practitionerCalendarEventId,
|
|
759
|
+
appointmentId: newAppointmentId,
|
|
760
|
+
clinicBranchId: clinicData.id,
|
|
761
|
+
clinicBranchInfo: clinicInfo,
|
|
762
|
+
practitionerProfileId: practitionerData.id,
|
|
763
|
+
practitionerProfileInfo: practitionerInfo,
|
|
764
|
+
procedureId: procedure.id,
|
|
765
|
+
procedureInfo: this._generateCalendarProcedureInfo(procedure),
|
|
766
|
+
eventName: `Appointment: ${procedure.name} at ${clinicData.name}`,
|
|
767
|
+
eventLocation: clinicData.location,
|
|
768
|
+
eventTime: eventTimeForCalendarEvents,
|
|
769
|
+
description: data.patientNotes || "",
|
|
770
|
+
status: initialCalendarEventStatus,
|
|
771
|
+
syncStatus: CalendarSyncStatus.INTERNAL,
|
|
772
|
+
eventType: CalendarEventType.APPOINTMENT,
|
|
773
|
+
createdAt: serverTimestampValue as any,
|
|
774
|
+
updatedAt: serverTimestampValue as any,
|
|
775
|
+
};
|
|
776
|
+
batch.set(
|
|
777
|
+
this.db
|
|
778
|
+
.collection(PATIENTS_COLLECTION)
|
|
779
|
+
.doc(patientProfileData.id)
|
|
780
|
+
.collection(CALENDAR_COLLECTION)
|
|
781
|
+
.doc(practitionerCalendarEventId),
|
|
782
|
+
patientCalendarEventData
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
// Clinic Calendar Event
|
|
786
|
+
// Use same ID as practitionerCalendarEventId
|
|
787
|
+
const clinicCalendarEventData: CalendarEvent = {
|
|
788
|
+
id: practitionerCalendarEventId,
|
|
789
|
+
appointmentId: newAppointmentId,
|
|
790
|
+
clinicBranchId: clinicData.id,
|
|
791
|
+
clinicBranchInfo: clinicInfo,
|
|
792
|
+
practitionerProfileId: practitionerData.id,
|
|
793
|
+
practitionerProfileInfo: practitionerInfo,
|
|
794
|
+
patientProfileId: patientProfileData.id,
|
|
795
|
+
patientProfileInfo: patientInfo,
|
|
796
|
+
procedureId: procedure.id,
|
|
797
|
+
procedureInfo: this._generateCalendarProcedureInfo(procedure),
|
|
798
|
+
eventName: `Appointment: ${procedure.name} for ${patientInfo.fullName} with ${practitionerInfo.name}`,
|
|
799
|
+
eventLocation: clinicData.location,
|
|
800
|
+
eventTime: eventTimeForCalendarEvents,
|
|
801
|
+
description: data.patientNotes || "",
|
|
802
|
+
status: initialCalendarEventStatus,
|
|
803
|
+
syncStatus: CalendarSyncStatus.INTERNAL,
|
|
804
|
+
eventType: CalendarEventType.APPOINTMENT,
|
|
805
|
+
createdAt: serverTimestampValue as any,
|
|
806
|
+
updatedAt: serverTimestampValue as any,
|
|
807
|
+
};
|
|
808
|
+
batch.set(
|
|
809
|
+
this.db
|
|
810
|
+
.collection(CLINICS_COLLECTION)
|
|
811
|
+
.doc(clinicData.id)
|
|
812
|
+
.collection(CALENDAR_COLLECTION)
|
|
813
|
+
.doc(practitionerCalendarEventId),
|
|
814
|
+
clinicCalendarEventData
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
// --- Initialize Pending/Draft Filled Documents and get form IDs ---
|
|
818
|
+
let initializedFormsInfo: LinkedFormInfo[] = [];
|
|
819
|
+
let pendingUserFormTemplateIds: string[] = [];
|
|
820
|
+
let allLinkedFormTemplateIds: string[] = [];
|
|
821
|
+
|
|
822
|
+
if (
|
|
823
|
+
procedure.documentationTemplates &&
|
|
824
|
+
Array.isArray(procedure.documentationTemplates) &&
|
|
825
|
+
procedure.documentationTemplates.length > 0
|
|
826
|
+
) {
|
|
827
|
+
const formInitResult =
|
|
828
|
+
await this.documentManagerAdmin.batchInitializeAppointmentFormsFromTechnologyTemplates(
|
|
829
|
+
batch,
|
|
830
|
+
newAppointmentId,
|
|
831
|
+
procedure.id, // Pass the actual procedureId for the forms
|
|
832
|
+
procedure.documentationTemplates,
|
|
833
|
+
data.patientId,
|
|
834
|
+
procedure.practitionerId,
|
|
835
|
+
procedure.clinicBranchId,
|
|
836
|
+
adminTsNow.toMillis()
|
|
837
|
+
);
|
|
838
|
+
initializedFormsInfo = formInitResult.initializedFormsInfo;
|
|
839
|
+
pendingUserFormTemplateIds = formInitResult.pendingUserFormsIds;
|
|
840
|
+
allLinkedFormTemplateIds = formInitResult.allLinkedTemplateIds;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// --- Generate appointment products from procedure ---
|
|
844
|
+
const appointmentProducts = this._generateAppointmentProductsFromProcedure(procedure);
|
|
845
|
+
|
|
846
|
+
// --- Construct Appointment Object ---
|
|
847
|
+
const newAppointmentData: Appointment = {
|
|
848
|
+
id: newAppointmentId,
|
|
849
|
+
calendarEventId: practitionerCalendarEventId,
|
|
850
|
+
clinicBranchId: procedure.clinicBranchId,
|
|
851
|
+
clinicInfo,
|
|
852
|
+
clinic_tz: clinicData.location.tz || "UTC",
|
|
853
|
+
practitionerId: procedure.practitionerId,
|
|
854
|
+
practitionerInfo,
|
|
855
|
+
patientId: data.patientId,
|
|
856
|
+
patientInfo,
|
|
857
|
+
procedureId: data.procedureId,
|
|
858
|
+
procedureInfo: this._generateProcedureSummaryInfo(
|
|
859
|
+
procedure,
|
|
860
|
+
clinicData,
|
|
861
|
+
practitionerData
|
|
862
|
+
),
|
|
863
|
+
procedureExtendedInfo: this._generateProcedureExtendedInfo(procedure),
|
|
864
|
+
status: initialAppointmentStatus,
|
|
865
|
+
bookingTime: adminTsNow as any,
|
|
866
|
+
confirmationTime:
|
|
867
|
+
initialAppointmentStatus === AppointmentStatus.CONFIRMED
|
|
868
|
+
? (adminTsNow as any)
|
|
869
|
+
: null,
|
|
870
|
+
appointmentStartTime: data.appointmentStartTime as any,
|
|
871
|
+
appointmentEndTime: data.appointmentEndTime as any,
|
|
872
|
+
cost: procedure.price,
|
|
873
|
+
currency: procedure.currency,
|
|
874
|
+
paymentStatus:
|
|
875
|
+
procedure.price > 0
|
|
876
|
+
? PaymentStatus.UNPAID
|
|
877
|
+
: PaymentStatus.NOT_APPLICABLE,
|
|
878
|
+
patientNotes: data.patientNotes || null,
|
|
879
|
+
blockingConditions: procedure.blockingConditions || [],
|
|
880
|
+
contraindications: (procedure as any).contraindications || [],
|
|
881
|
+
preProcedureRequirements: procedure.preRequirements || [],
|
|
882
|
+
postProcedureRequirements: procedure.postRequirements || [],
|
|
883
|
+
pendingUserFormsIds: pendingUserFormTemplateIds,
|
|
884
|
+
linkedFormIds: allLinkedFormTemplateIds,
|
|
885
|
+
completedPreRequirements: [],
|
|
886
|
+
completedPostRequirements: [],
|
|
887
|
+
linkedForms: initializedFormsInfo,
|
|
888
|
+
media: [],
|
|
889
|
+
reviewInfo: null,
|
|
890
|
+
metadata: {
|
|
891
|
+
selectedZones: null,
|
|
892
|
+
zonePhotos: null,
|
|
893
|
+
zonesData: null,
|
|
894
|
+
appointmentProducts: appointmentProducts,
|
|
895
|
+
extendedProcedures: [],
|
|
896
|
+
recommendedProcedures: [],
|
|
897
|
+
finalbilling: null,
|
|
898
|
+
finalizationNotes: null,
|
|
899
|
+
},
|
|
900
|
+
finalizedDetails: {
|
|
901
|
+
by: "",
|
|
902
|
+
at: adminTsNow as any,
|
|
903
|
+
notes: "",
|
|
904
|
+
},
|
|
905
|
+
internalNotes: null,
|
|
906
|
+
cancellationReason: null,
|
|
907
|
+
cancellationTime: null,
|
|
908
|
+
canceledBy: undefined,
|
|
909
|
+
rescheduleTime: null,
|
|
910
|
+
procedureActualStartTime: null,
|
|
911
|
+
actualDurationMinutes: undefined,
|
|
912
|
+
isRecurring: false,
|
|
913
|
+
recurringAppointmentId: null,
|
|
914
|
+
isArchived: false,
|
|
915
|
+
createdAt: adminTsNow as any,
|
|
916
|
+
updatedAt: adminTsNow as any,
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
batch.set(
|
|
920
|
+
this.db.collection(APPOINTMENTS_COLLECTION).doc(newAppointmentId),
|
|
921
|
+
newAppointmentData
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
// Commit Batch
|
|
925
|
+
await batch.commit();
|
|
926
|
+
|
|
927
|
+
console.log(
|
|
928
|
+
`[BookingAdmin] Appointment ${newAppointmentId} and associated calendar events created successfully.`
|
|
929
|
+
);
|
|
930
|
+
return {
|
|
931
|
+
success: true,
|
|
932
|
+
appointmentId: newAppointmentId,
|
|
933
|
+
appointmentData: newAppointmentData,
|
|
934
|
+
calendarEventId: practitionerCalendarEventId,
|
|
935
|
+
};
|
|
936
|
+
} catch (error) {
|
|
937
|
+
console.error(
|
|
938
|
+
"[BookingAdmin] Critical error in orchestrateAppointmentCreation:",
|
|
939
|
+
error
|
|
940
|
+
);
|
|
941
|
+
const errorMessage =
|
|
942
|
+
error instanceof Error
|
|
943
|
+
? error.message
|
|
944
|
+
: "Unknown server error during appointment creation.";
|
|
945
|
+
return { success: false, error: errorMessage };
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
private _generateProcedureSummaryInfo(
|
|
950
|
+
procedure: Procedure,
|
|
951
|
+
clinicData: Clinic,
|
|
952
|
+
practitionerData: Practitioner
|
|
953
|
+
): ProcedureSummaryInfo {
|
|
954
|
+
const procedureCategory = procedure.category as Category;
|
|
955
|
+
const procedureSubCategory = procedure.subcategory as Subcategory;
|
|
956
|
+
const procedureTechnology = procedure.technology as Technology;
|
|
957
|
+
const procedureProduct = procedure.product as Product;
|
|
958
|
+
return {
|
|
959
|
+
id: procedure.id,
|
|
960
|
+
name: procedure.name,
|
|
961
|
+
description: procedure.description,
|
|
962
|
+
family: procedure.family,
|
|
963
|
+
categoryName: procedureCategory?.name || "",
|
|
964
|
+
subcategoryName: procedureSubCategory?.name || "",
|
|
965
|
+
technologyName: procedureTechnology?.name || "",
|
|
966
|
+
price: procedure.price,
|
|
967
|
+
pricingMeasure: procedure.pricingMeasure,
|
|
968
|
+
currency: procedure.currency,
|
|
969
|
+
duration: procedure.duration,
|
|
970
|
+
clinicId: procedure.clinicBranchId,
|
|
971
|
+
clinicName: clinicData.name,
|
|
972
|
+
practitionerId: procedure.practitionerId,
|
|
973
|
+
practitionerName: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
|
|
974
|
+
photo:
|
|
975
|
+
(procedureTechnology as any)?.photos?.[0]?.url ||
|
|
976
|
+
(procedureProduct as any)?.photos?.[0]?.url ||
|
|
977
|
+
"",
|
|
978
|
+
brandName: (procedureProduct as any)?.brand?.name || "",
|
|
979
|
+
productName: procedureProduct?.name || "",
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
private _generateProcedureExtendedInfo(
|
|
984
|
+
procedure: Procedure
|
|
985
|
+
): ProcedureExtendedInfo {
|
|
986
|
+
const procedureCategory = procedure.category as Category;
|
|
987
|
+
const procedureSubCategory = procedure.subcategory as Subcategory;
|
|
988
|
+
const procedureTechnology = procedure.technology as Technology;
|
|
989
|
+
const procedureProduct = procedure.product as Product;
|
|
990
|
+
const productsMetadata = procedure.productsMetadata || [];
|
|
991
|
+
|
|
992
|
+
return {
|
|
993
|
+
id: procedure.id,
|
|
994
|
+
name: procedure.name,
|
|
995
|
+
description: procedure.description,
|
|
996
|
+
cost: procedure.price,
|
|
997
|
+
duration: procedure.duration,
|
|
998
|
+
procedureFamily: procedure.family,
|
|
999
|
+
procedureCategoryId: procedureCategory?.id || "",
|
|
1000
|
+
procedureCategoryName: procedureCategory?.name || "",
|
|
1001
|
+
procedureSubCategoryId: procedureSubCategory?.id || "",
|
|
1002
|
+
procedureSubCategoryName: procedureSubCategory?.name || "",
|
|
1003
|
+
procedureTechnologyId: procedureTechnology?.id || "",
|
|
1004
|
+
procedureTechnologyName: procedureTechnology?.name || "",
|
|
1005
|
+
procedureProductBrandId: procedureProduct?.brandId || "",
|
|
1006
|
+
procedureProductBrandName: procedureProduct?.brandName || "",
|
|
1007
|
+
procedureProducts: productsMetadata
|
|
1008
|
+
.filter((pp: any) => pp && pp.product) // Safety check for product-free procedures
|
|
1009
|
+
.map((pp: any) => ({
|
|
1010
|
+
productId: pp.product.id,
|
|
1011
|
+
productName: pp.product.name,
|
|
1012
|
+
brandId: pp.product.brandId,
|
|
1013
|
+
brandName: pp.product.brandName,
|
|
1014
|
+
})),
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
private _generateAppointmentProductsFromProcedure(procedure: Procedure): any[] {
|
|
1019
|
+
const productsMetadata = procedure.productsMetadata || [];
|
|
1020
|
+
|
|
1021
|
+
return productsMetadata
|
|
1022
|
+
.filter((pp: any) => pp && pp.product) // Safety check for product-free procedures
|
|
1023
|
+
.map((pp: any) => {
|
|
1024
|
+
const product = pp.product;
|
|
1025
|
+
return {
|
|
1026
|
+
productId: product.id,
|
|
1027
|
+
productName: product.name,
|
|
1028
|
+
brandId: product.brandId,
|
|
1029
|
+
brandName: product.brandName,
|
|
1030
|
+
procedureId: procedure.id,
|
|
1031
|
+
price: pp.price,
|
|
1032
|
+
currency: pp.currency,
|
|
1033
|
+
unitOfMeasurement: pp.pricingMeasure,
|
|
1034
|
+
};
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
}
|