@blackcode_sa/metaestetics-api 1.10.0 → 1.11.1

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 (43) hide show
  1. package/dist/admin/index.d.mts +337 -319
  2. package/dist/admin/index.d.ts +337 -319
  3. package/dist/admin/index.js +98 -79
  4. package/dist/admin/index.mjs +98 -79
  5. package/dist/backoffice/index.d.mts +284 -67
  6. package/dist/backoffice/index.d.ts +284 -67
  7. package/dist/backoffice/index.js +114 -6
  8. package/dist/backoffice/index.mjs +112 -6
  9. package/dist/index.d.mts +3145 -3065
  10. package/dist/index.d.ts +3145 -3065
  11. package/dist/index.js +460 -141
  12. package/dist/index.mjs +463 -143
  13. package/package.json +3 -1
  14. package/src/admin/booking/booking.admin.ts +2 -0
  15. package/src/admin/booking/booking.calculator.ts +121 -117
  16. package/src/admin/booking/booking.types.ts +3 -0
  17. package/src/backoffice/expo-safe/index.ts +2 -0
  18. package/src/backoffice/services/README.md +40 -0
  19. package/src/backoffice/services/constants.service.ts +268 -0
  20. package/src/backoffice/services/technology.service.ts +122 -10
  21. package/src/backoffice/types/admin-constants.types.ts +69 -0
  22. package/src/backoffice/types/index.ts +1 -0
  23. package/src/backoffice/types/product.types.ts +3 -1
  24. package/src/backoffice/types/technology.types.ts +4 -4
  25. package/src/backoffice/validations/schemas.ts +35 -9
  26. package/src/services/appointment/appointment.service.ts +0 -5
  27. package/src/services/appointment/utils/appointment.utils.ts +124 -113
  28. package/src/services/clinic/clinic.service.ts +163 -82
  29. package/src/services/procedure/procedure.service.ts +435 -234
  30. package/src/types/appointment/index.ts +9 -3
  31. package/src/types/clinic/index.ts +3 -6
  32. package/src/types/patient/medical-info.types.ts +3 -3
  33. package/src/types/procedure/index.ts +20 -17
  34. package/src/validations/appointment.schema.ts +2 -0
  35. package/src/validations/clinic.schema.ts +3 -6
  36. package/src/validations/patient/medical-info.schema.ts +7 -2
  37. package/src/validations/procedure.schema.ts +8 -10
  38. package/src/backoffice/services/__tests__/brand.service.test.ts +0 -196
  39. package/src/backoffice/services/__tests__/category.service.test.ts +0 -201
  40. package/src/backoffice/services/__tests__/product.service.test.ts +0 -358
  41. package/src/backoffice/services/__tests__/requirement.service.test.ts +0 -226
  42. package/src/backoffice/services/__tests__/subcategory.service.test.ts +0 -181
  43. package/src/backoffice/services/__tests__/technology.service.test.ts +0 -1097
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.10.0",
4
+ "version": "1.11.1",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -83,6 +83,7 @@
83
83
  "devDependencies": {
84
84
  "@testing-library/jest-dom": "^6.6.3",
85
85
  "@types/jest": "^29.5.14",
86
+ "@types/luxon": "^3.7.1",
86
87
  "@types/mailgun-js": "^0.22.18",
87
88
  "@types/node": "^20.17.13",
88
89
  "@types/react": "^19.1.0",
@@ -98,6 +99,7 @@
98
99
  "expo-server-sdk": "^3.13.0",
99
100
  "firebase-admin": "^13.0.2",
100
101
  "geofire-common": "^6.0.0",
102
+ "luxon": "^3.7.1",
101
103
  "zod": "^3.24.1"
102
104
  },
103
105
  "optionalDependencies": {
@@ -233,6 +233,7 @@ export class BookingAdmin {
233
233
  practitionerCalendarEvents: this.convertEventsTimestamps(
234
234
  practitionerCalendarEvents
235
235
  ),
236
+ tz: clinic.location.tz || "UTC",
236
237
  };
237
238
 
238
239
  Logger.info("[BookingAdmin] Calling availability calculator", {
@@ -823,6 +824,7 @@ export class BookingAdmin {
823
824
  calendarEventId: practitionerCalendarEventId,
824
825
  clinicBranchId: procedure.clinicBranchId,
825
826
  clinicInfo,
827
+ clinic_tz: clinicData.location.tz || "UTC",
826
828
  practitionerId: procedure.practitionerId,
827
829
  practitionerInfo,
828
830
  patientId: data.patientId,
@@ -1,4 +1,5 @@
1
1
  import { Timestamp } from "firebase/firestore";
2
+ import { DateTime } from "luxon";
2
3
  import {
3
4
  BookingAvailabilityRequest,
4
5
  BookingAvailabilityResponse,
@@ -39,6 +40,7 @@ export class BookingAvailabilityCalculator {
39
40
  timeframe,
40
41
  clinicCalendarEvents,
41
42
  practitionerCalendarEvents,
43
+ tz,
42
44
  } = request;
43
45
 
44
46
  // Get scheduling interval (default to 15 minutes if not specified)
@@ -61,7 +63,8 @@ export class BookingAvailabilityCalculator {
61
63
  availableIntervals = this.applyClinicWorkingHours(
62
64
  availableIntervals,
63
65
  clinic.workingHours,
64
- timeframe
66
+ timeframe,
67
+ tz
65
68
  );
66
69
 
67
70
  // Step 2: Subtract clinic blocking events
@@ -75,7 +78,8 @@ export class BookingAvailabilityCalculator {
75
78
  availableIntervals,
76
79
  practitioner,
77
80
  clinic.id,
78
- timeframe
81
+ timeframe,
82
+ tz
79
83
  );
80
84
 
81
85
  // Step 4: Subtract practitioner's busy times
@@ -104,12 +108,14 @@ export class BookingAvailabilityCalculator {
104
108
  * @param intervals - Current available intervals
105
109
  * @param workingHours - Clinic working hours
106
110
  * @param timeframe - Overall timeframe being considered
111
+ * @param tz - IANA timezone of the clinic
107
112
  * @returns Intervals filtered by clinic working hours
108
113
  */
109
114
  private static applyClinicWorkingHours(
110
115
  intervals: TimeInterval[],
111
116
  workingHours: any, // Using 'any' for now since we're working with the existing type structure
112
- timeframe: { start: Timestamp; end: Timestamp }
117
+ timeframe: { start: Timestamp; end: Timestamp },
118
+ tz: string
113
119
  ): TimeInterval[] {
114
120
  if (!intervals.length) return [];
115
121
  console.log(
@@ -120,7 +126,8 @@ export class BookingAvailabilityCalculator {
120
126
  const workingIntervals = this.createWorkingHoursIntervals(
121
127
  workingHours,
122
128
  timeframe.start.toDate(),
123
- timeframe.end.toDate()
129
+ timeframe.end.toDate(),
130
+ tz
124
131
  );
125
132
 
126
133
  // Intersect the available intervals with working hours intervals
@@ -133,44 +140,34 @@ export class BookingAvailabilityCalculator {
133
140
  * @param workingHours - Working hours definition
134
141
  * @param startDate - Start date of the overall timeframe
135
142
  * @param endDate - End date of the overall timeframe
143
+ * @param tz - IANA timezone of the clinic
136
144
  * @returns Array of time intervals representing working hours
137
145
  */
138
146
  private static createWorkingHoursIntervals(
139
147
  workingHours: any,
140
148
  startDate: Date,
141
- endDate: Date
149
+ endDate: Date,
150
+ tz: string
142
151
  ): TimeInterval[] {
143
152
  const workingIntervals: TimeInterval[] = [];
144
-
145
- // Clone the start date to avoid modifying the original
146
- const currentDate = new Date(startDate);
147
-
148
- // Reset time to start of day
149
- currentDate.setHours(0, 0, 0, 0);
150
-
151
- // Create a map of day names to day numbers (0 = Sunday, 1 = Monday, etc.)
152
- const dayNameToNumber: { [key: string]: number } = {
153
- sunday: 0,
154
- monday: 1,
155
- tuesday: 2,
156
- wednesday: 3,
157
- thursday: 4,
158
- friday: 5,
159
- saturday: 6,
160
- };
161
-
162
- // Iterate through each day in the timeframe
163
- while (currentDate <= endDate) {
164
- const dayOfWeek = currentDate.getDay();
165
- const dayName = Object.keys(dayNameToNumber).find(
166
- (key) => dayNameToNumber[key] === dayOfWeek
167
- );
153
+ let start = DateTime.fromJSDate(startDate, { zone: tz });
154
+ const end = DateTime.fromJSDate(endDate, { zone: tz });
155
+
156
+ while (start <= end) {
157
+ const dayOfWeek = start.weekday; // 1 for Monday, 7 for Sunday
158
+ const dayName = [
159
+ "monday",
160
+ "tuesday",
161
+ "wednesday",
162
+ "thursday",
163
+ "friday",
164
+ "saturday",
165
+ "sunday",
166
+ ][dayOfWeek - 1];
168
167
 
169
168
  if (dayName && workingHours[dayName]) {
170
169
  const daySchedule = workingHours[dayName];
171
-
172
170
  if (daySchedule) {
173
- // Parse open and close times
174
171
  const [openHours, openMinutes] = daySchedule.open
175
172
  .split(":")
176
173
  .map(Number);
@@ -178,25 +175,37 @@ export class BookingAvailabilityCalculator {
178
175
  .split(":")
179
176
  .map(Number);
180
177
 
181
- // Create start and end times for this working day
182
- const workStart = new Date(currentDate);
183
- workStart.setHours(openHours, openMinutes, 0, 0);
184
-
185
- const workEnd = new Date(currentDate);
186
- workEnd.setHours(closeHours, closeMinutes, 0, 0);
178
+ let workStart = start.set({
179
+ hour: openHours,
180
+ minute: openMinutes,
181
+ second: 0,
182
+ millisecond: 0,
183
+ });
184
+ let workEnd = start.set({
185
+ hour: closeHours,
186
+ minute: closeMinutes,
187
+ second: 0,
188
+ millisecond: 0,
189
+ });
187
190
 
188
- // Only add if the working hours are within our timeframe
189
- if (workEnd > startDate && workStart < endDate) {
190
- // Adjust interval to be within our overall timeframe
191
- const intervalStart = workStart < startDate ? startDate : workStart;
192
- const intervalEnd = workEnd > endDate ? endDate : workEnd;
191
+ if (
192
+ workEnd.toMillis() > startDate.getTime() &&
193
+ workStart.toMillis() < endDate.getTime()
194
+ ) {
195
+ const intervalStart =
196
+ workStart < DateTime.fromJSDate(startDate, { zone: tz })
197
+ ? DateTime.fromJSDate(startDate, { zone: tz })
198
+ : workStart;
199
+ const intervalEnd =
200
+ workEnd > DateTime.fromJSDate(endDate, { zone: tz })
201
+ ? DateTime.fromJSDate(endDate, { zone: tz })
202
+ : workEnd;
193
203
 
194
204
  workingIntervals.push({
195
- start: Timestamp.fromDate(intervalStart),
196
- end: Timestamp.fromDate(intervalEnd),
205
+ start: Timestamp.fromMillis(intervalStart.toMillis()),
206
+ end: Timestamp.fromMillis(intervalEnd.toMillis()),
197
207
  });
198
208
 
199
- // Handle breaks if they exist
200
209
  if (daySchedule.breaks && daySchedule.breaks.length > 0) {
201
210
  for (const breakTime of daySchedule.breaks) {
202
211
  const [breakStartHours, breakStartMinutes] = breakTime.start
@@ -206,21 +215,23 @@ export class BookingAvailabilityCalculator {
206
215
  .split(":")
207
216
  .map(Number);
208
217
 
209
- const breakStart = new Date(currentDate);
210
- breakStart.setHours(breakStartHours, breakStartMinutes, 0, 0);
211
-
212
- const breakEnd = new Date(currentDate);
213
- breakEnd.setHours(breakEndHours, breakEndMinutes, 0, 0);
218
+ const breakStart = start.set({
219
+ hour: breakStartHours,
220
+ minute: breakStartMinutes,
221
+ });
222
+ const breakEnd = start.set({
223
+ hour: breakEndHours,
224
+ minute: breakEndMinutes,
225
+ });
214
226
 
215
- // Subtract this break from our working intervals
216
227
  workingIntervals.splice(
217
228
  -1,
218
229
  1,
219
230
  ...this.subtractInterval(
220
231
  workingIntervals[workingIntervals.length - 1],
221
232
  {
222
- start: Timestamp.fromDate(breakStart),
223
- end: Timestamp.fromDate(breakEnd),
233
+ start: Timestamp.fromMillis(breakStart.toMillis()),
234
+ end: Timestamp.fromMillis(breakEnd.toMillis()),
224
235
  }
225
236
  )
226
237
  );
@@ -229,11 +240,8 @@ export class BookingAvailabilityCalculator {
229
240
  }
230
241
  }
231
242
  }
232
-
233
- // Move to the next day
234
- currentDate.setDate(currentDate.getDate() + 1);
243
+ start = start.plus({ days: 1 });
235
244
  }
236
-
237
245
  return workingIntervals;
238
246
  }
239
247
 
@@ -290,13 +298,15 @@ export class BookingAvailabilityCalculator {
290
298
  * @param practitioner - Practitioner object
291
299
  * @param clinicId - ID of the clinic
292
300
  * @param timeframe - Overall timeframe being considered
301
+ * @param tz - IANA timezone of the clinic
293
302
  * @returns Intervals filtered by practitioner's working hours
294
303
  */
295
304
  private static applyPractitionerWorkingHours(
296
305
  intervals: TimeInterval[],
297
306
  practitioner: any,
298
307
  clinicId: string,
299
- timeframe: { start: Timestamp; end: Timestamp }
308
+ timeframe: { start: Timestamp; end: Timestamp },
309
+ tz: string
300
310
  ): TimeInterval[] {
301
311
  if (!intervals.length) return [];
302
312
  console.log(`Applying practitioner working hours for clinic ${clinicId}`);
@@ -319,7 +329,8 @@ export class BookingAvailabilityCalculator {
319
329
  const workingIntervals = this.createPractitionerWorkingHoursIntervals(
320
330
  clinicWorkingHours.workingHours,
321
331
  timeframe.start.toDate(),
322
- timeframe.end.toDate()
332
+ timeframe.end.toDate(),
333
+ tz
323
334
  );
324
335
 
325
336
  // Intersect the available intervals with practitioner's working hours intervals
@@ -332,74 +343,67 @@ export class BookingAvailabilityCalculator {
332
343
  * @param workingHours - Practitioner's working hours definition
333
344
  * @param startDate - Start date of the overall timeframe
334
345
  * @param endDate - End date of the overall timeframe
346
+ * @param tz - IANA timezone of the clinic
335
347
  * @returns Array of time intervals representing practitioner's working hours
336
348
  */
337
349
  private static createPractitionerWorkingHoursIntervals(
338
350
  workingHours: any,
339
351
  startDate: Date,
340
- endDate: Date
352
+ endDate: Date,
353
+ tz: string
341
354
  ): TimeInterval[] {
342
355
  const workingIntervals: TimeInterval[] = [];
343
-
344
- // Clone the start date to avoid modifying the original
345
- const currentDate = new Date(startDate);
346
-
347
- // Reset time to start of day
348
- currentDate.setHours(0, 0, 0, 0);
349
-
350
- // Create a map of day names to day numbers (0 = Sunday, 1 = Monday, etc.)
351
- const dayNameToNumber: { [key: string]: number } = {
352
- sunday: 0,
353
- monday: 1,
354
- tuesday: 2,
355
- wednesday: 3,
356
- thursday: 4,
357
- friday: 5,
358
- saturday: 6,
359
- };
360
-
361
- // Iterate through each day in the timeframe
362
- while (currentDate <= endDate) {
363
- const dayOfWeek = currentDate.getDay();
364
- const dayName = Object.keys(dayNameToNumber).find(
365
- (key) => dayNameToNumber[key] === dayOfWeek
366
- );
356
+ let start = DateTime.fromJSDate(startDate, { zone: tz });
357
+ const end = DateTime.fromJSDate(endDate, { zone: tz });
358
+
359
+ while (start <= end) {
360
+ const dayOfWeek = start.weekday;
361
+ const dayName = [
362
+ "monday",
363
+ "tuesday",
364
+ "wednesday",
365
+ "thursday",
366
+ "friday",
367
+ "saturday",
368
+ "sunday",
369
+ ][dayOfWeek - 1];
367
370
 
368
371
  if (dayName && workingHours[dayName]) {
369
372
  const daySchedule = workingHours[dayName];
370
-
371
373
  if (daySchedule) {
372
- // Parse start and end times
373
374
  const [startHours, startMinutes] = daySchedule.start
374
375
  .split(":")
375
376
  .map(Number);
376
377
  const [endHours, endMinutes] = daySchedule.end.split(":").map(Number);
377
378
 
378
- // Create start and end times for this working day
379
- const workStart = new Date(currentDate);
380
- workStart.setHours(startHours, startMinutes, 0, 0);
381
-
382
- const workEnd = new Date(currentDate);
383
- workEnd.setHours(endHours, endMinutes, 0, 0);
384
-
385
- // Only add if the working hours are within our timeframe
386
- if (workEnd > startDate && workStart < endDate) {
387
- // Adjust interval to be within our overall timeframe
388
- const intervalStart = workStart < startDate ? startDate : workStart;
389
- const intervalEnd = workEnd > endDate ? endDate : workEnd;
379
+ const workStart = start.set({
380
+ hour: startHours,
381
+ minute: startMinutes,
382
+ });
383
+ const workEnd = start.set({ hour: endHours, minute: endMinutes });
384
+
385
+ if (
386
+ workEnd.toMillis() > startDate.getTime() &&
387
+ workStart.toMillis() < endDate.getTime()
388
+ ) {
389
+ const intervalStart =
390
+ workStart < DateTime.fromJSDate(startDate, { zone: tz })
391
+ ? DateTime.fromJSDate(startDate, { zone: tz })
392
+ : workStart;
393
+ const intervalEnd =
394
+ workEnd > DateTime.fromJSDate(endDate, { zone: tz })
395
+ ? DateTime.fromJSDate(endDate, { zone: tz })
396
+ : workEnd;
390
397
 
391
398
  workingIntervals.push({
392
- start: Timestamp.fromDate(intervalStart),
393
- end: Timestamp.fromDate(intervalEnd),
399
+ start: Timestamp.fromMillis(intervalStart.toMillis()),
400
+ end: Timestamp.fromMillis(intervalEnd.toMillis()),
394
401
  });
395
402
  }
396
403
  }
397
404
  }
398
-
399
- // Move to the next day
400
- currentDate.setDate(currentDate.getDate() + 1);
405
+ start = start.plus({ days: 1 });
401
406
  }
402
-
403
407
  return workingIntervals;
404
408
  }
405
409
 
@@ -485,32 +489,32 @@ export class BookingAvailabilityCalculator {
485
489
  const intervalEnd = interval.end.toDate();
486
490
 
487
491
  // Start at the beginning of the interval
488
- let slotStart = new Date(intervalStart);
492
+ let slotStart = DateTime.fromJSDate(intervalStart);
489
493
 
490
494
  // Adjust slotStart to the nearest interval boundary if needed
491
- const minutesIntoDay = slotStart.getHours() * 60 + slotStart.getMinutes();
495
+ const minutesIntoDay = slotStart.hour * 60 + slotStart.minute;
492
496
  const minutesRemainder = minutesIntoDay % intervalMinutes;
493
497
 
494
498
  if (minutesRemainder > 0) {
495
- slotStart.setMinutes(
496
- slotStart.getMinutes() + (intervalMinutes - minutesRemainder)
497
- );
499
+ slotStart = slotStart.plus({
500
+ minutes: intervalMinutes - minutesRemainder,
501
+ });
498
502
  }
499
503
 
500
504
  // Iterate through potential start times
501
- while (slotStart.getTime() + durationMs <= intervalEnd.getTime()) {
505
+ while (slotStart.toMillis() + durationMs <= intervalEnd.getTime()) {
502
506
  // Calculate potential end time
503
- const slotEnd = new Date(slotStart.getTime() + durationMs);
507
+ const slotEnd = slotStart.plus({ minutes: durationMinutes });
504
508
 
505
509
  // Check if this slot fits entirely within one of our available intervals
506
510
  if (this.isSlotFullyAvailable(slotStart, slotEnd, intervals)) {
507
511
  slots.push({
508
- start: Timestamp.fromDate(slotStart),
512
+ start: Timestamp.fromMillis(slotStart.toMillis()),
509
513
  });
510
514
  }
511
515
 
512
516
  // Move to the next potential start time
513
- slotStart = new Date(slotStart.getTime() + intervalMs);
517
+ slotStart = slotStart.plus({ minutes: intervalMinutes });
514
518
  }
515
519
  }
516
520
 
@@ -527,14 +531,14 @@ export class BookingAvailabilityCalculator {
527
531
  * @returns True if the slot is fully contained within an available interval
528
532
  */
529
533
  private static isSlotFullyAvailable(
530
- slotStart: Date,
531
- slotEnd: Date,
534
+ slotStart: DateTime,
535
+ slotEnd: DateTime,
532
536
  intervals: TimeInterval[]
533
537
  ): boolean {
534
538
  // Check if the slot is fully contained in any of the available intervals
535
539
  return intervals.some((interval) => {
536
- const intervalStart = interval.start.toDate();
537
- const intervalEnd = interval.end.toDate();
540
+ const intervalStart = DateTime.fromMillis(interval.start.toMillis());
541
+ const intervalEnd = DateTime.fromMillis(interval.end.toMillis());
538
542
 
539
543
  return slotStart >= intervalStart && slotEnd <= intervalEnd;
540
544
  });
@@ -28,6 +28,9 @@ export interface BookingAvailabilityRequest {
28
28
 
29
29
  /** Calendar events for the practitioner during the specified timeframe */
30
30
  practitionerCalendarEvents: CalendarEvent[];
31
+
32
+ /** IANA timezone of the clinic */
33
+ tz: string;
31
34
  }
32
35
 
33
36
  /**
@@ -34,4 +34,6 @@ export {
34
34
  export { Contraindication } from "../types/static/contraindication.types";
35
35
  export { ProcedureFamily } from "../types/static/procedure-family.types";
36
36
  export { TreatmentBenefit } from "../types/static/treatment-benefit.types";
37
+ export { TreatmentBenefitDynamic } from "../types/";
38
+ export { ContraindicationDynamic } from "../types/";
37
39
  export { RequirementType, TimeUnit } from "../types/requirement.types";
@@ -0,0 +1,40 @@
1
+ # Backoffice Services
2
+
3
+ This directory contains services used by the backoffice application.
4
+
5
+ ## Services
6
+
7
+ ### `CategoryService`
8
+
9
+ Manages procedure categories. Categories are the first level of organization after procedure family (aesthetics/surgery).
10
+
11
+ - **`create(category)`**: Creates a new category.
12
+ - **`getAll()`**: Retrieves all active categories.
13
+ - **`getAllByFamily(family)`**: Retrieves all active categories for a specific procedure family.
14
+ - **`update(id, category)`**: Updates an existing category.
15
+ - **`delete(id)`**: Soft deletes a category.
16
+ - **`getById(id)`**: Retrieves a category by its ID.
17
+
18
+ ### `ProductService`
19
+
20
+ Manages products, which are sub-items of a `Technology`.
21
+
22
+ - **`create(technologyId, brandId, product)`**: Creates a new product under a technology.
23
+ - **`getAllByTechnology(technologyId)`**: Retrieves all products for a technology.
24
+ - **`getAllByBrand(brandId)`**: Retrieves all products for a brand.
25
+ - **`update(technologyId, productId, product)`**: Updates a product.
26
+ - **`delete(technologyId, productId)`**: Soft deletes a product.
27
+ - **`getById(technologyId, productId)`**: Retrieves a product by its ID.
28
+
29
+ ### `ConstantsService`
30
+
31
+ Manages administrative constants like treatment benefits and contraindications.
32
+
33
+ - **`getTreatmentBenefits()`**: Retrieves all treatment benefits.
34
+ - **`addTreatmentBenefit(benefit)`**: Adds a new treatment benefit.
35
+ - **`updateTreatmentBenefit(benefit)`**: Updates an existing treatment benefit.
36
+ - **`deleteTreatmentBenefit(benefitId)`**: Deletes a treatment benefit.
37
+ - **`getContraindications()`**: Retrieves all contraindications.
38
+ - **`addContraindication(contraindication)`**: Adds a new contraindication.
39
+ - **`updateContraindication(contraindication)`**: Updates an existing contraindication.
40
+ - **`deleteContraindication(contraindicationId)`**: Deletes a contraindication.