@blackcode_sa/metaestetics-api 1.5.31 → 1.5.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ import { Timestamp } from "firebase/firestore";
2
+ import { Clinic } from "../../types/clinic";
3
+ import { Practitioner } from "../../types/practitioner";
4
+ import { Procedure } from "../../types/procedure";
5
+ import { CalendarEvent } from "../../types/calendar";
6
+
7
+ /**
8
+ * Request parameters for calculating available booking slots
9
+ */
10
+ export interface BookingAvailabilityRequest {
11
+ /** The clinic for which to calculate booking slots */
12
+ clinic: Clinic;
13
+
14
+ /** The practitioner for which to calculate booking slots */
15
+ practitioner: Practitioner;
16
+
17
+ /** The procedure for which to calculate booking slots */
18
+ procedure: Procedure;
19
+
20
+ /** The timeframe for which to calculate booking slots */
21
+ timeframe: {
22
+ start: Timestamp;
23
+ end: Timestamp;
24
+ };
25
+
26
+ /** Calendar events for the clinic during the specified timeframe */
27
+ clinicCalendarEvents: CalendarEvent[];
28
+
29
+ /** Calendar events for the practitioner during the specified timeframe */
30
+ practitionerCalendarEvents: CalendarEvent[];
31
+ }
32
+
33
+ /**
34
+ * Represents a single available booking slot
35
+ */
36
+ export interface AvailableSlot {
37
+ /** Start time of the available booking slot */
38
+ start: Timestamp;
39
+ }
40
+
41
+ /**
42
+ * Response with available booking slots
43
+ */
44
+ export interface BookingAvailabilityResponse {
45
+ /** Array of available booking slots */
46
+ availableSlots: AvailableSlot[];
47
+ }
48
+
49
+ /**
50
+ * Represents a time interval with start and end times
51
+ * Used internally for availability calculations
52
+ */
53
+ export interface TimeInterval {
54
+ start: Timestamp;
55
+ end: Timestamp;
56
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./booking.types";
2
+ export * from "./booking.calculator";
3
+ export { BookingAdmin } from "./booking.admin";
@@ -69,6 +69,9 @@ export {
69
69
  // Export mailing services
70
70
  export { BaseMailingService, PractitionerInviteMailingService };
71
71
 
72
+ // Export booking module
73
+ export * from "./booking";
74
+
72
75
  /**
73
76
  * Main entry point for the Admin module.
74
77
  * This module contains services and utilities intended for administrative tasks,
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export {
26
26
  } from "./services/documentation-templates";
27
27
  export { CalendarServiceV2 } from "./services/calendar/calendar-refactored.service";
28
28
  export { SyncedCalendarsService } from "./services/calendar/synced-calendars.service";
29
+ export { ReviewService } from "./services/reviews/reviews.service";
29
30
 
30
31
  // Backoffice services
31
32
  export { BrandService } from "./backoffice/services/brand.service";
@@ -259,6 +260,10 @@ export {
259
260
  export { Contraindication } from "./backoffice/types/static/contraindication.types";
260
261
  export { ProcedureFamily } from "./backoffice/types/static/procedure-family.types";
261
262
  export { TreatmentBenefit } from "./backoffice/types/static/treatment-benefit.types";
263
+ export {
264
+ RequirementType,
265
+ TimeUnit,
266
+ } from "./backoffice/types/requirement.types";
262
267
 
263
268
  // Documentation Templates types
264
269
  export type {
@@ -0,0 +1,603 @@
1
+ import {
2
+ Firestore,
3
+ Timestamp,
4
+ DocumentSnapshot,
5
+ serverTimestamp,
6
+ } from "firebase/firestore";
7
+ import { Auth } from "firebase/auth";
8
+ import { FirebaseApp } from "firebase/app";
9
+ import { BaseService } from "../base.service";
10
+ import {
11
+ Appointment,
12
+ AppointmentStatus,
13
+ CreateAppointmentData,
14
+ UpdateAppointmentData,
15
+ SearchAppointmentsParams,
16
+ PaymentStatus,
17
+ } from "../../types/appointment";
18
+ import {
19
+ createAppointmentSchema,
20
+ updateAppointmentSchema,
21
+ searchAppointmentsSchema,
22
+ } from "../../validations/appointment.schema";
23
+
24
+ // Import other services needed (dependency injection pattern)
25
+ import { CalendarServiceV2 } from "../calendar/calendar-refactored.service";
26
+ import { PatientService } from "../patient/patient.service";
27
+ import { PractitionerService } from "../practitioner/practitioner.service";
28
+ import { ClinicService } from "../clinic/clinic.service";
29
+
30
+ // Import utility functions
31
+ import {
32
+ fetchAggregatedInfoUtil,
33
+ createAppointmentUtil,
34
+ updateAppointmentUtil,
35
+ getAppointmentByIdUtil,
36
+ searchAppointmentsUtil,
37
+ } from "./utils/appointment.utils";
38
+
39
+ /**
40
+ * AppointmentService is responsible for managing appointments,
41
+ * including creating, updating, retrieving, and searching appointments.
42
+ * It serves as the main entry point for working with appointment data.
43
+ */
44
+ export class AppointmentService extends BaseService {
45
+ private calendarService: CalendarServiceV2;
46
+ private patientService: PatientService;
47
+ private practitionerService: PractitionerService;
48
+ private clinicService: ClinicService;
49
+
50
+ /**
51
+ * Creates a new AppointmentService instance.
52
+ *
53
+ * @param db Firestore instance
54
+ * @param auth Firebase Auth instance
55
+ * @param app Firebase App instance
56
+ * @param calendarService Calendar service instance
57
+ * @param patientService Patient service instance
58
+ * @param practitionerService Practitioner service instance
59
+ * @param clinicService Clinic service instance
60
+ */
61
+ constructor(
62
+ db: Firestore,
63
+ auth: Auth,
64
+ app: FirebaseApp,
65
+ calendarService: CalendarServiceV2,
66
+ patientService: PatientService,
67
+ practitionerService: PractitionerService,
68
+ clinicService: ClinicService
69
+ ) {
70
+ super(db, auth, app);
71
+ this.calendarService = calendarService;
72
+ this.patientService = patientService;
73
+ this.practitionerService = practitionerService;
74
+ this.clinicService = clinicService;
75
+ }
76
+
77
+ /**
78
+ * Creates a new appointment.
79
+ *
80
+ * @param data Data needed to create the appointment
81
+ * @returns The created appointment
82
+ */
83
+ async createAppointment(data: CreateAppointmentData): Promise<Appointment> {
84
+ try {
85
+ console.log("[APPOINTMENT_SERVICE] Creating appointment");
86
+
87
+ // Validate input data
88
+ const validatedData = await createAppointmentSchema.parseAsync(data);
89
+
90
+ // Fetch all required aggregated information
91
+ const aggregatedInfo = await fetchAggregatedInfoUtil(
92
+ this.db,
93
+ validatedData.clinicBranchId,
94
+ validatedData.practitionerId,
95
+ validatedData.patientId,
96
+ validatedData.procedureId
97
+ );
98
+
99
+ // Create the appointment using the utility function
100
+ const appointment = await createAppointmentUtil(
101
+ this.db,
102
+ validatedData as CreateAppointmentData,
103
+ aggregatedInfo,
104
+ this.generateId.bind(this)
105
+ );
106
+
107
+ console.log(
108
+ `[APPOINTMENT_SERVICE] Appointment created with ID: ${appointment.id}`
109
+ );
110
+
111
+ return appointment;
112
+ } catch (error) {
113
+ console.error("[APPOINTMENT_SERVICE] Error creating appointment:", error);
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Gets an appointment by ID.
120
+ *
121
+ * @param appointmentId ID of the appointment to retrieve
122
+ * @returns The appointment or null if not found
123
+ */
124
+ async getAppointmentById(appointmentId: string): Promise<Appointment | null> {
125
+ try {
126
+ console.log(
127
+ `[APPOINTMENT_SERVICE] Getting appointment with ID: ${appointmentId}`
128
+ );
129
+
130
+ const appointment = await getAppointmentByIdUtil(this.db, appointmentId);
131
+
132
+ console.log(
133
+ `[APPOINTMENT_SERVICE] Appointment ${appointmentId} ${
134
+ appointment ? "found" : "not found"
135
+ }`
136
+ );
137
+
138
+ return appointment;
139
+ } catch (error) {
140
+ console.error(
141
+ `[APPOINTMENT_SERVICE] Error getting appointment ${appointmentId}:`,
142
+ error
143
+ );
144
+ throw error;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Updates an existing appointment.
150
+ *
151
+ * @param appointmentId ID of the appointment to update
152
+ * @param data Update data for the appointment
153
+ * @returns The updated appointment
154
+ */
155
+ async updateAppointment(
156
+ appointmentId: string,
157
+ data: UpdateAppointmentData
158
+ ): Promise<Appointment> {
159
+ try {
160
+ console.log(
161
+ `[APPOINTMENT_SERVICE] Updating appointment with ID: ${appointmentId}`
162
+ );
163
+
164
+ // Validate input data
165
+ const validatedData = await updateAppointmentSchema.parseAsync(data);
166
+
167
+ // Update the appointment using the utility function
168
+ const updatedAppointment = await updateAppointmentUtil(
169
+ this.db,
170
+ appointmentId,
171
+ validatedData
172
+ );
173
+
174
+ console.log(
175
+ `[APPOINTMENT_SERVICE] Appointment ${appointmentId} updated successfully`
176
+ );
177
+
178
+ return updatedAppointment;
179
+ } catch (error) {
180
+ console.error(
181
+ `[APPOINTMENT_SERVICE] Error updating appointment ${appointmentId}:`,
182
+ error
183
+ );
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Searches for appointments based on various criteria.
190
+ *
191
+ * @param params Search parameters
192
+ * @returns Found appointments and the last document for pagination
193
+ */
194
+ async searchAppointments(params: SearchAppointmentsParams): Promise<{
195
+ appointments: Appointment[];
196
+ lastDoc: DocumentSnapshot | null;
197
+ }> {
198
+ try {
199
+ console.log(
200
+ "[APPOINTMENT_SERVICE] Searching appointments with params:",
201
+ params
202
+ );
203
+
204
+ // Validate search parameters
205
+ await searchAppointmentsSchema.parseAsync(params);
206
+
207
+ // Search for appointments using the utility function
208
+ const result = await searchAppointmentsUtil(this.db, params);
209
+
210
+ console.log(
211
+ `[APPOINTMENT_SERVICE] Found ${result.appointments.length} appointments`
212
+ );
213
+
214
+ return result;
215
+ } catch (error) {
216
+ console.error(
217
+ "[APPOINTMENT_SERVICE] Error searching appointments:",
218
+ error
219
+ );
220
+ throw error;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Gets appointments for a specific patient.
226
+ *
227
+ * @param patientId ID of the patient
228
+ * @param options Optional parameters for filtering and pagination
229
+ * @returns Found appointments and the last document for pagination
230
+ */
231
+ async getPatientAppointments(
232
+ patientId: string,
233
+ options?: {
234
+ startDate?: Date;
235
+ endDate?: Date;
236
+ status?: AppointmentStatus | AppointmentStatus[];
237
+ limit?: number;
238
+ startAfter?: DocumentSnapshot;
239
+ }
240
+ ): Promise<{
241
+ appointments: Appointment[];
242
+ lastDoc: DocumentSnapshot | null;
243
+ }> {
244
+ console.log(
245
+ `[APPOINTMENT_SERVICE] Getting appointments for patient: ${patientId}`
246
+ );
247
+
248
+ const searchParams: SearchAppointmentsParams = {
249
+ patientId,
250
+ startDate: options?.startDate,
251
+ endDate: options?.endDate,
252
+ status: options?.status,
253
+ limit: options?.limit,
254
+ startAfter: options?.startAfter,
255
+ };
256
+
257
+ return this.searchAppointments(searchParams);
258
+ }
259
+
260
+ /**
261
+ * Gets appointments for a specific practitioner.
262
+ *
263
+ * @param practitionerId ID of the practitioner
264
+ * @param options Optional parameters for filtering and pagination
265
+ * @returns Found appointments and the last document for pagination
266
+ */
267
+ async getPractitionerAppointments(
268
+ practitionerId: string,
269
+ options?: {
270
+ startDate?: Date;
271
+ endDate?: Date;
272
+ status?: AppointmentStatus | AppointmentStatus[];
273
+ limit?: number;
274
+ startAfter?: DocumentSnapshot;
275
+ }
276
+ ): Promise<{
277
+ appointments: Appointment[];
278
+ lastDoc: DocumentSnapshot | null;
279
+ }> {
280
+ console.log(
281
+ `[APPOINTMENT_SERVICE] Getting appointments for practitioner: ${practitionerId}`
282
+ );
283
+
284
+ const searchParams: SearchAppointmentsParams = {
285
+ practitionerId,
286
+ startDate: options?.startDate,
287
+ endDate: options?.endDate,
288
+ status: options?.status,
289
+ limit: options?.limit,
290
+ startAfter: options?.startAfter,
291
+ };
292
+
293
+ return this.searchAppointments(searchParams);
294
+ }
295
+
296
+ /**
297
+ * Gets appointments for a specific clinic.
298
+ *
299
+ * @param clinicBranchId ID of the clinic branch
300
+ * @param options Optional parameters for filtering and pagination
301
+ * @returns Found appointments and the last document for pagination
302
+ */
303
+ async getClinicAppointments(
304
+ clinicBranchId: string,
305
+ options?: {
306
+ practitionerId?: string;
307
+ startDate?: Date;
308
+ endDate?: Date;
309
+ status?: AppointmentStatus | AppointmentStatus[];
310
+ limit?: number;
311
+ startAfter?: DocumentSnapshot;
312
+ }
313
+ ): Promise<{
314
+ appointments: Appointment[];
315
+ lastDoc: DocumentSnapshot | null;
316
+ }> {
317
+ console.log(
318
+ `[APPOINTMENT_SERVICE] Getting appointments for clinic: ${clinicBranchId}`
319
+ );
320
+
321
+ const searchParams: SearchAppointmentsParams = {
322
+ clinicBranchId,
323
+ practitionerId: options?.practitionerId,
324
+ startDate: options?.startDate,
325
+ endDate: options?.endDate,
326
+ status: options?.status,
327
+ limit: options?.limit,
328
+ startAfter: options?.startAfter,
329
+ };
330
+
331
+ return this.searchAppointments(searchParams);
332
+ }
333
+
334
+ /**
335
+ * Updates the status of an appointment.
336
+ *
337
+ * @param appointmentId ID of the appointment
338
+ * @param newStatus New status to set
339
+ * @param cancellationReason Required if canceling the appointment
340
+ * @param canceledBy Required if canceling the appointment
341
+ * @returns The updated appointment
342
+ */
343
+ async updateAppointmentStatus(
344
+ appointmentId: string,
345
+ newStatus: AppointmentStatus,
346
+ cancellationReason?: string,
347
+ canceledBy?: "patient" | "clinic" | "practitioner" | "system"
348
+ ): Promise<Appointment> {
349
+ console.log(
350
+ `[APPOINTMENT_SERVICE] Updating status of appointment ${appointmentId} to ${newStatus}`
351
+ );
352
+
353
+ // Create update data based on the new status
354
+ const updateData: UpdateAppointmentData = { status: newStatus };
355
+
356
+ // Add cancellation details if applicable
357
+ if (
358
+ newStatus === AppointmentStatus.CANCELED_CLINIC ||
359
+ newStatus === AppointmentStatus.CANCELED_PATIENT
360
+ ) {
361
+ if (!cancellationReason) {
362
+ throw new Error(
363
+ "Cancellation reason is required when canceling an appointment"
364
+ );
365
+ }
366
+ if (!canceledBy) {
367
+ throw new Error(
368
+ "Canceled by is required when canceling an appointment"
369
+ );
370
+ }
371
+
372
+ updateData.cancellationReason = cancellationReason;
373
+ updateData.canceledBy = canceledBy;
374
+ }
375
+
376
+ // Add confirmation time if confirming
377
+ if (newStatus === AppointmentStatus.CONFIRMED) {
378
+ updateData.confirmationTime = Timestamp.now();
379
+ }
380
+
381
+ return this.updateAppointment(appointmentId, updateData);
382
+ }
383
+
384
+ /**
385
+ * Confirms an appointment.
386
+ *
387
+ * @param appointmentId ID of the appointment to confirm
388
+ * @returns The confirmed appointment
389
+ */
390
+ async confirmAppointment(appointmentId: string): Promise<Appointment> {
391
+ console.log(
392
+ `[APPOINTMENT_SERVICE] Confirming appointment: ${appointmentId}`
393
+ );
394
+ return this.updateAppointmentStatus(
395
+ appointmentId,
396
+ AppointmentStatus.CONFIRMED
397
+ );
398
+ }
399
+
400
+ /**
401
+ * Cancels an appointment from the clinic side.
402
+ *
403
+ * @param appointmentId ID of the appointment to cancel
404
+ * @param reason Reason for cancellation
405
+ * @returns The canceled appointment
406
+ */
407
+ async cancelAppointmentByClinic(
408
+ appointmentId: string,
409
+ reason: string
410
+ ): Promise<Appointment> {
411
+ console.log(
412
+ `[APPOINTMENT_SERVICE] Canceling appointment by clinic: ${appointmentId}`
413
+ );
414
+ return this.updateAppointmentStatus(
415
+ appointmentId,
416
+ AppointmentStatus.CANCELED_CLINIC,
417
+ reason,
418
+ "clinic"
419
+ );
420
+ }
421
+
422
+ /**
423
+ * Cancels an appointment from the patient side.
424
+ *
425
+ * @param appointmentId ID of the appointment to cancel
426
+ * @param reason Reason for cancellation
427
+ * @returns The canceled appointment
428
+ */
429
+ async cancelAppointmentByPatient(
430
+ appointmentId: string,
431
+ reason: string
432
+ ): Promise<Appointment> {
433
+ console.log(
434
+ `[APPOINTMENT_SERVICE] Canceling appointment by patient: ${appointmentId}`
435
+ );
436
+ return this.updateAppointmentStatus(
437
+ appointmentId,
438
+ AppointmentStatus.CANCELED_PATIENT,
439
+ reason,
440
+ "patient"
441
+ );
442
+ }
443
+
444
+ /**
445
+ * Marks an appointment as checked in.
446
+ *
447
+ * @param appointmentId ID of the appointment
448
+ * @returns The updated appointment
449
+ */
450
+ async checkInAppointment(appointmentId: string): Promise<Appointment> {
451
+ console.log(
452
+ `[APPOINTMENT_SERVICE] Checking in appointment: ${appointmentId}`
453
+ );
454
+ return this.updateAppointmentStatus(
455
+ appointmentId,
456
+ AppointmentStatus.CHECKED_IN
457
+ );
458
+ }
459
+
460
+ /**
461
+ * Marks an appointment as in progress.
462
+ *
463
+ * @param appointmentId ID of the appointment
464
+ * @returns The updated appointment
465
+ */
466
+ async startAppointment(appointmentId: string): Promise<Appointment> {
467
+ console.log(`[APPOINTMENT_SERVICE] Starting appointment: ${appointmentId}`);
468
+ return this.updateAppointmentStatus(
469
+ appointmentId,
470
+ AppointmentStatus.IN_PROGRESS
471
+ );
472
+ }
473
+
474
+ /**
475
+ * Marks an appointment as completed.
476
+ *
477
+ * @param appointmentId ID of the appointment
478
+ * @param actualDurationMinutes Actual duration of the appointment in minutes
479
+ * @returns The updated appointment
480
+ */
481
+ async completeAppointment(
482
+ appointmentId: string,
483
+ actualDurationMinutes?: number
484
+ ): Promise<Appointment> {
485
+ console.log(
486
+ `[APPOINTMENT_SERVICE] Completing appointment: ${appointmentId}`
487
+ );
488
+
489
+ const updateData: UpdateAppointmentData = {
490
+ status: AppointmentStatus.COMPLETED,
491
+ actualDurationMinutes,
492
+ };
493
+
494
+ return this.updateAppointment(appointmentId, updateData);
495
+ }
496
+
497
+ /**
498
+ * Marks an appointment as no-show.
499
+ *
500
+ * @param appointmentId ID of the appointment
501
+ * @returns The updated appointment
502
+ */
503
+ async markNoShow(appointmentId: string): Promise<Appointment> {
504
+ console.log(
505
+ `[APPOINTMENT_SERVICE] Marking appointment as no-show: ${appointmentId}`
506
+ );
507
+ return this.updateAppointmentStatus(
508
+ appointmentId,
509
+ AppointmentStatus.NO_SHOW
510
+ );
511
+ }
512
+
513
+ /**
514
+ * Updates the payment status of an appointment.
515
+ *
516
+ * @param appointmentId ID of the appointment
517
+ * @param paymentStatus New payment status
518
+ * @param paymentTransactionId Optional transaction ID for the payment
519
+ * @returns The updated appointment
520
+ */
521
+ async updatePaymentStatus(
522
+ appointmentId: string,
523
+ paymentStatus: PaymentStatus,
524
+ paymentTransactionId?: string
525
+ ): Promise<Appointment> {
526
+ console.log(
527
+ `[APPOINTMENT_SERVICE] Updating payment status of appointment ${appointmentId} to ${paymentStatus}`
528
+ );
529
+
530
+ const updateData: UpdateAppointmentData = {
531
+ paymentStatus,
532
+ paymentTransactionId: paymentTransactionId || null,
533
+ };
534
+
535
+ return this.updateAppointment(appointmentId, updateData);
536
+ }
537
+
538
+ /**
539
+ * Marks pre-procedure requirements as completed.
540
+ *
541
+ * @param appointmentId ID of the appointment
542
+ * @param requirementIds IDs of the requirements to mark as completed
543
+ * @returns The updated appointment
544
+ */
545
+ async completePreRequirements(
546
+ appointmentId: string,
547
+ requirementIds: string[]
548
+ ): Promise<Appointment> {
549
+ console.log(
550
+ `[APPOINTMENT_SERVICE] Marking pre-requirements as completed for appointment: ${appointmentId}`
551
+ );
552
+
553
+ const updateData: UpdateAppointmentData = {
554
+ completedPreRequirements: requirementIds,
555
+ };
556
+
557
+ return this.updateAppointment(appointmentId, updateData);
558
+ }
559
+
560
+ /**
561
+ * Marks post-procedure requirements as completed.
562
+ *
563
+ * @param appointmentId ID of the appointment
564
+ * @param requirementIds IDs of the requirements to mark as completed
565
+ * @returns The updated appointment
566
+ */
567
+ async completePostRequirements(
568
+ appointmentId: string,
569
+ requirementIds: string[]
570
+ ): Promise<Appointment> {
571
+ console.log(
572
+ `[APPOINTMENT_SERVICE] Marking post-requirements as completed for appointment: ${appointmentId}`
573
+ );
574
+
575
+ const updateData: UpdateAppointmentData = {
576
+ completedPostRequirements: requirementIds,
577
+ };
578
+
579
+ return this.updateAppointment(appointmentId, updateData);
580
+ }
581
+
582
+ /**
583
+ * Updates the internal notes of an appointment.
584
+ *
585
+ * @param appointmentId ID of the appointment
586
+ * @param notes Updated internal notes
587
+ * @returns The updated appointment
588
+ */
589
+ async updateInternalNotes(
590
+ appointmentId: string,
591
+ notes: string | null
592
+ ): Promise<Appointment> {
593
+ console.log(
594
+ `[APPOINTMENT_SERVICE] Updating internal notes for appointment: ${appointmentId}`
595
+ );
596
+
597
+ const updateData: UpdateAppointmentData = {
598
+ internalNotes: notes,
599
+ };
600
+
601
+ return this.updateAppointment(appointmentId, updateData);
602
+ }
603
+ }
@@ -0,0 +1,2 @@
1
+ export { AppointmentService } from "./appointment.service";
2
+ export * from "../../types/appointment";