@blackcode_sa/metaestetics-api 1.6.4 → 1.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/admin/index.d.mts +236 -2
  2. package/dist/admin/index.d.ts +236 -2
  3. package/dist/admin/index.js +11251 -10447
  4. package/dist/admin/index.mjs +11251 -10447
  5. package/dist/backoffice/index.d.mts +2 -0
  6. package/dist/backoffice/index.d.ts +2 -0
  7. package/dist/index.d.mts +50 -77
  8. package/dist/index.d.ts +50 -77
  9. package/dist/index.js +77 -305
  10. package/dist/index.mjs +78 -306
  11. package/package.json +1 -1
  12. package/src/admin/aggregation/appointment/README.md +128 -0
  13. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1053 -0
  14. package/src/admin/booking/README.md +125 -0
  15. package/src/admin/booking/booking.admin.ts +638 -3
  16. package/src/admin/calendar/calendar.admin.service.ts +183 -0
  17. package/src/admin/documentation-templates/document-manager.admin.ts +131 -0
  18. package/src/admin/mailing/appointment/appointment.mailing.service.ts +264 -0
  19. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -0
  20. package/src/admin/mailing/base.mailing.service.ts +1 -1
  21. package/src/admin/mailing/index.ts +2 -0
  22. package/src/admin/notifications/notifications.admin.ts +397 -1
  23. package/src/backoffice/types/product.types.ts +2 -0
  24. package/src/services/appointment/appointment.service.ts +89 -182
  25. package/src/services/procedure/procedure.service.ts +1 -0
  26. package/src/types/appointment/index.ts +3 -1
  27. package/src/types/notifications/index.ts +4 -2
  28. package/src/types/procedure/index.ts +7 -0
  29. package/src/validations/appointment.schema.ts +2 -3
  30. package/src/validations/procedure.schema.ts +3 -0
@@ -93,100 +93,6 @@ export class AppointmentService extends BaseService {
93
93
  this.functions = getFunctions(app, "europe-west6"); // Initialize Firebase Functions with the correct region
94
94
  }
95
95
 
96
- /**
97
- * Test method using the callable function version of getAvailableBookingSlots
98
- * For development and testing purposes only - not for production use
99
- *
100
- * @param clinicId ID of the clinic
101
- * @param practitionerId ID of the practitioner
102
- * @param procedureId ID of the procedure
103
- * @param startDate Start date of the time range to check
104
- * @param endDate End date of the time range to check
105
- * @returns Test result from the callable function
106
- */
107
- async testGetAvailableBookingSlots(
108
- clinicId: string,
109
- practitionerId: string,
110
- procedureId: string,
111
- startDate: Date,
112
- endDate: Date
113
- ): Promise<any> {
114
- try {
115
- console.log(
116
- `[APPOINTMENT_SERVICE] Testing callable function for clinic: ${clinicId}, practitioner: ${practitionerId}, procedure: ${procedureId}`
117
- );
118
-
119
- // Get the callable function
120
- const getAvailableBookingSlotsCallable = httpsCallable(
121
- this.functions,
122
- "getAvailableBookingSlots"
123
- );
124
-
125
- // Call the function with the required parameters
126
- const result = await getAvailableBookingSlotsCallable({
127
- clinicId,
128
- practitionerId,
129
- procedureId,
130
- timeframe: {
131
- start: startDate.getTime(),
132
- end: endDate.getTime(),
133
- },
134
- });
135
-
136
- console.log(
137
- "[APPOINTMENT_SERVICE] Callable function test result:",
138
- result.data
139
- );
140
-
141
- return result.data;
142
- } catch (error) {
143
- console.error(
144
- "[APPOINTMENT_SERVICE] Error testing callable function:",
145
- error
146
- );
147
- throw error;
148
- }
149
- }
150
-
151
- /**
152
- * Gets available booking slots for a specific clinic, practitioner, and procedure.
153
- *
154
- * @param clinicId ID of the clinic
155
- * @param practitionerId ID of the practitioner
156
- * @param procedureId ID of the procedure
157
- * @param startDate Start date of the time range to check
158
- * @param endDate End date of the time range to check
159
- * @returns Array of available booking slots
160
- */
161
- async getAvailableBookingSlots(
162
- clinicId: string,
163
- practitionerId: string,
164
- procedureId: string,
165
- startDate: Date,
166
- endDate: Date
167
- ): Promise<AvailableSlot[]> {
168
- try {
169
- console.log(
170
- `[APPOINTMENT_SERVICE] Getting available booking slots for clinic: ${clinicId}, practitioner: ${practitionerId}, procedure: ${procedureId}`
171
- );
172
-
173
- // Just call our HTTP implementation since the callable function isn't working in the browser
174
- return this.getAvailableBookingSlotsHttp(
175
- clinicId,
176
- practitionerId,
177
- procedureId,
178
- startDate,
179
- endDate
180
- );
181
- } catch (error) {
182
- console.error(
183
- "[APPOINTMENT_SERVICE] Error getting available booking slots:",
184
- error
185
- );
186
- throw error;
187
- }
188
- }
189
-
190
96
  /**
191
97
  * Gets available booking slots for a specific clinic, practitioner, and procedure using HTTP request.
192
98
  * This is an alternative implementation using direct HTTP request instead of callable function.
@@ -328,42 +234,108 @@ export class AppointmentService extends BaseService {
328
234
  }
329
235
 
330
236
  /**
331
- * Creates a new appointment.
237
+ * Creates an appointment via the Cloud Function orchestrateAppointmentCreation
332
238
  *
333
- * @param data Data needed to create the appointment
239
+ * @param data - CreateAppointmentData object
334
240
  * @returns The created appointment
335
241
  */
336
- async createAppointment(data: CreateAppointmentData): Promise<Appointment> {
242
+ async createAppointmentHttp(
243
+ data: CreateAppointmentData
244
+ ): Promise<Appointment> {
337
245
  try {
338
- console.log("[APPOINTMENT_SERVICE] Creating appointment");
246
+ console.log(
247
+ "[APPOINTMENT_SERVICE] Creating appointment via cloud function"
248
+ );
339
249
 
340
- // Validate input data
341
- const validatedData = await createAppointmentSchema.parseAsync(data);
250
+ // Get the authenticated user's ID token
251
+ const currentUser = this.auth.currentUser;
252
+ if (!currentUser) {
253
+ throw new Error("User must be authenticated to create an appointment");
254
+ }
255
+ const idToken = await currentUser.getIdToken();
342
256
 
343
- // Fetch all required aggregated information
344
- const aggregatedInfo = await fetchAggregatedInfoUtil(
345
- this.db,
346
- validatedData.clinicBranchId,
347
- validatedData.practitionerId,
348
- validatedData.patientId,
349
- validatedData.procedureId
350
- );
257
+ // Construct the function URL for the Express app endpoint
258
+ const functionUrl = `https://europe-west6-metaestetics.cloudfunctions.net/bookingApi/orchestrateAppointmentCreation`;
351
259
 
352
- // Create the appointment using the utility function
353
- const appointment = await createAppointmentUtil(
354
- this.db,
355
- validatedData as CreateAppointmentData,
356
- aggregatedInfo,
357
- this.generateId.bind(this)
260
+ // Prepare request data for the Cloud Function
261
+ // Map CreateAppointmentData to OrchestrateAppointmentCreationData format
262
+ const requestData = {
263
+ patientId: data.patientId,
264
+ procedureId: data.procedureId,
265
+ appointmentStartTime: data.appointmentStartTime.toMillis
266
+ ? data.appointmentStartTime.toMillis()
267
+ : new Date(data.appointmentStartTime as any).getTime(),
268
+ appointmentEndTime: data.appointmentEndTime.toMillis
269
+ ? data.appointmentEndTime.toMillis()
270
+ : new Date(data.appointmentEndTime as any).getTime(),
271
+ patientNotes: data.patientNotes || null,
272
+ };
273
+
274
+ console.log(
275
+ `[APPOINTMENT_SERVICE] Making fetch request to ${functionUrl}`
358
276
  );
359
277
 
278
+ // Make the HTTP request with expanded CORS options for browser
279
+ const response = await fetch(functionUrl, {
280
+ method: "POST",
281
+ mode: "cors",
282
+ cache: "no-cache",
283
+ credentials: "omit",
284
+ headers: {
285
+ "Content-Type": "application/json",
286
+ Authorization: `Bearer ${idToken}`,
287
+ },
288
+ redirect: "follow",
289
+ referrerPolicy: "no-referrer",
290
+ body: JSON.stringify(requestData),
291
+ });
292
+
360
293
  console.log(
361
- `[APPOINTMENT_SERVICE] Appointment created with ID: ${appointment.id}`
294
+ `[APPOINTMENT_SERVICE] Received response ${response.status}: ${response.statusText}`
362
295
  );
363
296
 
364
- return appointment;
297
+ // Check if the request was successful
298
+ if (!response.ok) {
299
+ const errorText = await response.text();
300
+ console.error(
301
+ `[APPOINTMENT_SERVICE] Error response details: ${errorText}`
302
+ );
303
+ throw new Error(
304
+ `Failed to create appointment: ${response.status} ${response.statusText} - ${errorText}`
305
+ );
306
+ }
307
+
308
+ // Parse the response
309
+ const result = await response.json();
310
+
311
+ if (!result.success) {
312
+ throw new Error(result.error || "Failed to create appointment");
313
+ }
314
+
315
+ // If the backend returns the full appointment data
316
+ if (result.appointmentData) {
317
+ console.log(
318
+ `[APPOINTMENT_SERVICE] Appointment created with ID: ${result.appointmentId}`
319
+ );
320
+ return result.appointmentData;
321
+ }
322
+
323
+ // If only the ID is returned, fetch the complete appointment
324
+ const createdAppointment = await this.getAppointmentById(
325
+ result.appointmentId
326
+ );
327
+ if (!createdAppointment) {
328
+ throw new Error(
329
+ `Failed to retrieve created appointment with ID: ${result.appointmentId}`
330
+ );
331
+ }
332
+
333
+ return createdAppointment;
365
334
  } catch (error) {
366
- console.error("[APPOINTMENT_SERVICE] Error creating appointment:", error);
335
+ console.error(
336
+ "[APPOINTMENT_SERVICE] Error creating appointment via cloud function:",
337
+ error
338
+ );
367
339
  throw error;
368
340
  }
369
341
  }
@@ -1008,50 +980,6 @@ export class AppointmentService extends BaseService {
1008
980
  return this.updateAppointment(appointmentId, updateData);
1009
981
  }
1010
982
 
1011
- /**
1012
- * Marks pre-procedure requirements as completed.
1013
- *
1014
- * @param appointmentId ID of the appointment
1015
- * @param requirementIds IDs of the requirements to mark as completed
1016
- * @returns The updated appointment
1017
- */
1018
- async completePreRequirements(
1019
- appointmentId: string,
1020
- requirementIds: string[]
1021
- ): Promise<Appointment> {
1022
- console.log(
1023
- `[APPOINTMENT_SERVICE] Marking pre-requirements as completed for appointment: ${appointmentId}`
1024
- );
1025
-
1026
- const updateData: UpdateAppointmentData = {
1027
- completedPreRequirements: requirementIds,
1028
- };
1029
-
1030
- return this.updateAppointment(appointmentId, updateData);
1031
- }
1032
-
1033
- /**
1034
- * Marks post-procedure requirements as completed.
1035
- *
1036
- * @param appointmentId ID of the appointment
1037
- * @param requirementIds IDs of the requirements to mark as completed
1038
- * @returns The updated appointment
1039
- */
1040
- async completePostRequirements(
1041
- appointmentId: string,
1042
- requirementIds: string[]
1043
- ): Promise<Appointment> {
1044
- console.log(
1045
- `[APPOINTMENT_SERVICE] Marking post-requirements as completed for appointment: ${appointmentId}`
1046
- );
1047
-
1048
- const updateData: UpdateAppointmentData = {
1049
- completedPostRequirements: requirementIds,
1050
- };
1051
-
1052
- return this.updateAppointment(appointmentId, updateData);
1053
- }
1054
-
1055
983
  /**
1056
984
  * Updates the internal notes of an appointment.
1057
985
  *
@@ -1073,25 +1001,4 @@ export class AppointmentService extends BaseService {
1073
1001
 
1074
1002
  return this.updateAppointment(appointmentId, updateData);
1075
1003
  }
1076
-
1077
- /**
1078
- * Debug helper: Get the current user's ID token for testing purposes
1079
- * Use this token in Postman with Authorization: Bearer TOKEN
1080
- */
1081
- async getDebugToken(): Promise<string | null> {
1082
- try {
1083
- const currentUser = this.auth.currentUser;
1084
- if (!currentUser) {
1085
- console.log("[APPOINTMENT_SERVICE] No user is signed in");
1086
- return null;
1087
- }
1088
-
1089
- const idToken = await currentUser.getIdToken();
1090
- console.log("[APPOINTMENT_SERVICE] Debug token:", idToken);
1091
- return idToken;
1092
- } catch (error) {
1093
- console.error("[APPOINTMENT_SERVICE] Error getting debug token:", error);
1094
- return null;
1095
- }
1096
- }
1097
1004
  }
@@ -179,6 +179,7 @@ export class ProcedureService extends BaseService {
179
179
  technology,
180
180
  product,
181
181
  blockingConditions: technology.blockingConditions,
182
+ contraindications: technology.contraindications || [],
182
183
  treatmentBenefits: technology.benefits,
183
184
  preRequirements: technology.requirements.pre,
184
185
  postRequirements: technology.requirements.post,
@@ -93,6 +93,7 @@ export interface LinkedFormInfo {
93
93
  templateVersion: number;
94
94
  title: string; // For display, usually from DocumentTemplate.title
95
95
  isUserForm: boolean;
96
+ isRequired?: boolean;
96
97
  status: FilledDocumentStatus; // Status of the filled form (e.g., draft, completed, signed)
97
98
  path: string; // Full Firestore path to the filled document (e.g., appointments/{aid}/user-forms/{fid})
98
99
  submittedAt?: Timestamp;
@@ -178,6 +179,7 @@ export interface Appointment {
178
179
  completedPostRequirements?: string[]; // IDs of completed post-requirements
179
180
 
180
181
  /** NEW: Linked forms (consent, procedure-specific forms, etc.) */
182
+ linkedFormIds?: string[];
181
183
  linkedForms?: LinkedFormInfo[];
182
184
  pendingUserFormsIds?: string[]; // Determines if there are any user forms that are pending for this appointment, blocks the appointment from being checked in (only for user forms with isRequired = true)
183
185
 
@@ -210,7 +212,6 @@ export interface Appointment {
210
212
  * Data needed to create a new Appointment
211
213
  */
212
214
  export interface CreateAppointmentData {
213
- calendarEventId: string;
214
215
  clinicBranchId: string;
215
216
  practitionerId: string;
216
217
  patientId: string;
@@ -250,6 +251,7 @@ export interface UpdateAppointmentData {
250
251
  practitionerId?: string; // If practitioner is changed
251
252
 
252
253
  /** NEW: For updating linked forms - typically managed by dedicated methods */
254
+ linkedFormIds?: string[] | FieldValue;
253
255
  linkedForms?: LinkedFormInfo[] | FieldValue;
254
256
 
255
257
  /** NEW: For updating media items - typically managed by dedicated methods */
@@ -11,6 +11,8 @@ export enum NotificationType {
11
11
  APPOINTMENT_CANCELLED = "appointmentCancelled", // When an appointment is cancelled
12
12
 
13
13
  // --- Requirement-Driven Instructions ---
14
+ PRE_REQUIREMENT_INSTRUCTION_DUE = "preRequirementInstructionDue",
15
+ POST_REQUIREMENT_INSTRUCTION_DUE = "postRequirementInstructionDue",
14
16
  REQUIREMENT_INSTRUCTION_DUE = "requirementInstructionDue", // For specific pre/post care instructions
15
17
 
16
18
  // --- Form Related ---
@@ -106,7 +108,7 @@ export enum NotificationStatus {
106
108
  * Notifikacija za pre-requirement
107
109
  */
108
110
  export interface PreRequirementNotification extends BaseNotification {
109
- notificationType: NotificationType.REQUIREMENT_INSTRUCTION_DUE;
111
+ notificationType: NotificationType.PRE_REQUIREMENT_INSTRUCTION_DUE;
110
112
  /** ID tretmana za koji je vezan pre-requirement */
111
113
  treatmentId: string;
112
114
  /** Lista pre-requirements koji treba da se ispune */
@@ -119,7 +121,7 @@ export interface PreRequirementNotification extends BaseNotification {
119
121
  * Notifikacija za post-requirement
120
122
  */
121
123
  export interface PostRequirementNotification extends BaseNotification {
122
- notificationType: NotificationType.REQUIREMENT_INSTRUCTION_DUE;
124
+ notificationType: NotificationType.POST_REQUIREMENT_INSTRUCTION_DUE;
123
125
  /** ID tretmana za koji je vezan post-requirement */
124
126
  treatmentId: string;
125
127
  /** Lista post-requirements koji treba da se ispune */
@@ -16,6 +16,7 @@ import { ClinicInfo } from "../profile";
16
16
  import { DoctorInfo } from "../clinic";
17
17
  import { PRACTITIONERS_COLLECTION } from "../practitioner";
18
18
  import { ProcedureReviewInfo } from "../reviews";
19
+ import type { Contraindication } from "../../backoffice/types/static/contraindication.types";
19
20
 
20
21
  /**
21
22
  * Procedure represents a specific medical procedure that can be performed by a practitioner in a clinic
@@ -26,6 +27,8 @@ export interface Procedure {
26
27
  id: string;
27
28
  /** Name of the procedure */
28
29
  name: string;
30
+ /** Photos of the procedure */
31
+ photos?: string[];
29
32
  /** Detailed description of the procedure */
30
33
  description: string;
31
34
  /** Family of procedures this belongs to (aesthetics/surgery) */
@@ -50,6 +53,8 @@ export interface Procedure {
50
53
  blockingConditions: BlockingCondition[];
51
54
  /** Treatment benefits of this procedure */
52
55
  treatmentBenefits: TreatmentBenefit[];
56
+ /** Contraindications of this procedure */
57
+ contraindications: Contraindication[];
53
58
  /** Pre-procedure requirements */
54
59
  preRequirements: Requirement[];
55
60
  /** Post-procedure requirements */
@@ -93,6 +98,7 @@ export interface CreateProcedureData {
93
98
  duration: number;
94
99
  practitionerId: string;
95
100
  clinicBranchId: string;
101
+ photos?: string[];
96
102
  }
97
103
 
98
104
  /**
@@ -112,6 +118,7 @@ export interface UpdateProcedureData {
112
118
  technologyId?: string;
113
119
  productId?: string;
114
120
  clinicBranchId?: string;
121
+ photos?: string[];
115
122
  }
116
123
 
117
124
  /**
@@ -70,6 +70,7 @@ export const linkedFormInfoSchema = z.object({
70
70
  .positive("Template version must be a positive integer"),
71
71
  title: z.string().min(MIN_STRING_LENGTH, "Form title is required"),
72
72
  isUserForm: z.boolean(),
73
+ isRequired: z.boolean().optional(),
73
74
  status: filledDocumentStatusSchema,
74
75
  path: z.string().min(MIN_STRING_LENGTH, "Form path is required"),
75
76
  submittedAt: z
@@ -138,9 +139,6 @@ export const finalizedDetailsSchema = z.object({
138
139
  */
139
140
  export const createAppointmentSchema = z
140
141
  .object({
141
- calendarEventId: z
142
- .string()
143
- .min(MIN_STRING_LENGTH, "Calendar event ID is required"),
144
142
  clinicBranchId: z
145
143
  .string()
146
144
  .min(MIN_STRING_LENGTH, "Clinic branch ID is required"),
@@ -221,6 +219,7 @@ export const updateAppointmentSchema = z
221
219
  completedPostRequirements: z
222
220
  .union([z.array(z.string()), z.any()])
223
221
  .optional(),
222
+ linkedFormIds: z.union([z.array(z.string()), z.any()]).optional(),
224
223
  pendingUserFormsIds: z.union([z.array(z.string()), z.any()]).optional(),
225
224
  appointmentStartTime: z
226
225
  .any()
@@ -24,6 +24,7 @@ export const createProcedureSchema = z.object({
24
24
  duration: z.number().min(1).max(480), // Max 8 hours
25
25
  practitionerId: z.string().min(1),
26
26
  clinicBranchId: z.string().min(1),
27
+ photos: z.array(z.string()).optional(),
27
28
  });
28
29
 
29
30
  /**
@@ -43,6 +44,7 @@ export const updateProcedureSchema = z.object({
43
44
  technologyId: z.string().optional(),
44
45
  productId: z.string().optional(),
45
46
  clinicBranchId: z.string().optional(),
47
+ photos: z.array(z.string()).optional(),
46
48
  });
47
49
 
48
50
  /**
@@ -55,6 +57,7 @@ export const procedureSchema = createProcedureSchema.extend({
55
57
  technology: z.any(), // We'll validate the full technology object separately
56
58
  product: z.any(), // We'll validate the full product object separately
57
59
  blockingConditions: z.array(z.any()), // We'll validate blocking conditions separately
60
+ contraindications: z.array(z.any()), // We'll validate contraindications separately
58
61
  treatmentBenefits: z.array(z.any()), // We'll validate treatment benefits separately
59
62
  preRequirements: z.array(z.any()), // We'll validate requirements separately
60
63
  postRequirements: z.array(z.any()), // We'll validate requirements separately