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