@blackcode_sa/metaestetics-api 1.7.13 → 1.7.15

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.
@@ -64,6 +64,7 @@ import {
64
64
  import { DocumentManagerAdminService } from "../documentation-templates/document-manager.admin";
65
65
  import { LinkedFormInfo } from "../../types/appointment";
66
66
  import { TimestampUtils } from "../../utils/TimestampUtils";
67
+ import { Logger } from "../logger";
67
68
 
68
69
  /**
69
70
  * Interface for the data required by orchestrateAppointmentCreation
@@ -112,9 +113,19 @@ export class BookingAdmin {
112
113
  }
113
114
  ): Promise<{ availableSlots: { start: admin.firestore.Timestamp }[] }> {
114
115
  try {
115
- console.log(
116
- `[BookingAdmin] Getting available slots for clinic ${clinicId}, practitioner ${practitionerId}, procedure ${procedureId}`
117
- );
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
+ });
118
129
 
119
130
  // Convert timeframe dates to Firestore Timestamps if needed
120
131
  const start =
@@ -128,42 +139,80 @@ export class BookingAdmin {
128
139
  : timeframe.end;
129
140
 
130
141
  // 1. Fetch clinic data
142
+ Logger.debug("[BookingAdmin] Fetching clinic data", { clinicId });
131
143
  const clinicDoc = await this.db.collection("clinics").doc(clinicId).get();
132
144
  if (!clinicDoc.exists) {
145
+ Logger.error("[BookingAdmin] Clinic not found", { clinicId });
133
146
  throw new Error(`Clinic ${clinicId} not found`);
134
147
  }
135
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
+ });
136
153
 
137
154
  // 2. Fetch practitioner data
155
+ Logger.debug("[BookingAdmin] Fetching practitioner data", {
156
+ practitionerId,
157
+ });
138
158
  const practitionerDoc = await this.db
139
159
  .collection("practitioners")
140
160
  .doc(practitionerId)
141
161
  .get();
142
162
  if (!practitionerDoc.exists) {
163
+ Logger.error("[BookingAdmin] Practitioner not found", {
164
+ practitionerId,
165
+ });
143
166
  throw new Error(`Practitioner ${practitionerId} not found`);
144
167
  }
145
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
+ });
146
173
 
147
174
  // 3. Fetch procedure data
175
+ Logger.debug("[BookingAdmin] Fetching procedure data", { procedureId });
148
176
  const procedureDoc = await this.db
149
177
  .collection("procedures")
150
178
  .doc(procedureId)
151
179
  .get();
152
180
  if (!procedureDoc.exists) {
181
+ Logger.error("[BookingAdmin] Procedure not found", { procedureId });
153
182
  throw new Error(`Procedure ${procedureId} not found`);
154
183
  }
155
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
+ });
156
189
 
157
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
+ });
158
196
  const clinicCalendarEvents = await this.getClinicCalendarEvents(
159
197
  clinicId,
160
198
  start,
161
199
  end
162
200
  );
201
+ Logger.debug("[BookingAdmin] Retrieved clinic calendar events", {
202
+ count: clinicCalendarEvents.length,
203
+ });
163
204
 
164
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
+ });
165
211
  const practitionerCalendarEvents =
166
212
  await this.getPractitionerCalendarEvents(practitionerId, start, end);
213
+ Logger.debug("[BookingAdmin] Retrieved practitioner calendar events", {
214
+ count: practitionerCalendarEvents.length,
215
+ });
167
216
 
168
217
  // Since we're working with two different Timestamp implementations (admin vs client),
169
218
  // we need to convert our timestamps to the client-side format expected by the calculator
@@ -186,17 +235,57 @@ export class BookingAdmin {
186
235
  ),
187
236
  };
188
237
 
238
+ Logger.info("[BookingAdmin] Calling availability calculator", {
239
+ calculatorInputReady: true,
240
+ timeframeDurationHours: Math.round(
241
+ (end.toMillis() - start.toMillis()) / (1000 * 60 * 60)
242
+ ),
243
+ clinicEventsCount: clinicCalendarEvents.length,
244
+ practitionerEventsCount: practitionerCalendarEvents.length,
245
+ });
246
+
189
247
  // Use the calculator to compute available slots
190
248
  const result = BookingAvailabilityCalculator.calculateSlots(request);
191
249
 
192
250
  // Convert the client Timestamps to admin Timestamps before returning
193
- return {
251
+ const availableSlotsResult = {
194
252
  availableSlots: result.availableSlots.map((slot) => ({
195
253
  start: admin.firestore.Timestamp.fromMillis(slot.start.toMillis()),
196
254
  })),
197
255
  };
256
+
257
+ Logger.info(
258
+ "[BookingAdmin] Availability calculation completed successfully",
259
+ {
260
+ availableSlotsCount: availableSlotsResult.availableSlots.length,
261
+ firstSlotTime:
262
+ availableSlotsResult.availableSlots.length > 0
263
+ ? availableSlotsResult.availableSlots[0].start
264
+ .toDate()
265
+ .toISOString()
266
+ : "none",
267
+ lastSlotTime:
268
+ availableSlotsResult.availableSlots.length > 0
269
+ ? availableSlotsResult.availableSlots[
270
+ availableSlotsResult.availableSlots.length - 1
271
+ ].start
272
+ .toDate()
273
+ .toISOString()
274
+ : "none",
275
+ }
276
+ );
277
+
278
+ return availableSlotsResult;
198
279
  } catch (error) {
199
- console.error("[BookingAdmin] Error getting available slots:", error);
280
+ const errorMessage =
281
+ error instanceof Error ? error.message : String(error);
282
+ Logger.error("[BookingAdmin] Error getting available slots", {
283
+ errorMessage,
284
+ clinicId,
285
+ practitionerId,
286
+ procedureId,
287
+ stack: error instanceof Error ? error.stack : undefined,
288
+ });
200
289
  throw error;
201
290
  }
202
291
  }
@@ -243,6 +332,12 @@ export class BookingAdmin {
243
332
  end: admin.firestore.Timestamp
244
333
  ): Promise<any[]> {
245
334
  try {
335
+ Logger.debug("[BookingAdmin] Querying clinic calendar events", {
336
+ clinicId,
337
+ startTime: start.toDate().toISOString(),
338
+ endTime: end.toDate().toISOString(),
339
+ });
340
+
246
341
  const eventsRef = this.db
247
342
  .collection(`clinics/${clinicId}/calendar`)
248
343
  .where("eventTime.start", ">=", start)
@@ -250,15 +345,28 @@ export class BookingAdmin {
250
345
 
251
346
  const snapshot = await eventsRef.get();
252
347
 
253
- return snapshot.docs.map((doc) => ({
348
+ const events = snapshot.docs.map((doc) => ({
254
349
  ...doc.data(),
255
350
  id: doc.id,
256
351
  }));
352
+
353
+ Logger.debug("[BookingAdmin] Retrieved clinic calendar events", {
354
+ clinicId,
355
+ eventsCount: events.length,
356
+ eventsTypes: this.summarizeEventTypes(events),
357
+ });
358
+
359
+ return events;
257
360
  } catch (error) {
258
- console.error(
259
- `[BookingAdmin] Error fetching clinic calendar events:`,
260
- error
261
- );
361
+ const errorMessage =
362
+ error instanceof Error ? error.message : String(error);
363
+ Logger.error("[BookingAdmin] Error fetching clinic calendar events", {
364
+ errorMessage,
365
+ clinicId,
366
+ startTime: start.toDate().toISOString(),
367
+ endTime: end.toDate().toISOString(),
368
+ stack: error instanceof Error ? error.stack : undefined,
369
+ });
262
370
  return [];
263
371
  }
264
372
  }
@@ -277,6 +385,12 @@ export class BookingAdmin {
277
385
  end: admin.firestore.Timestamp
278
386
  ): Promise<any[]> {
279
387
  try {
388
+ Logger.debug("[BookingAdmin] Querying practitioner calendar events", {
389
+ practitionerId,
390
+ startTime: start.toDate().toISOString(),
391
+ endTime: end.toDate().toISOString(),
392
+ });
393
+
280
394
  const eventsRef = this.db
281
395
  .collection(`practitioners/${practitionerId}/calendar`)
282
396
  .where("eventTime.start", ">=", start)
@@ -284,19 +398,51 @@ export class BookingAdmin {
284
398
 
285
399
  const snapshot = await eventsRef.get();
286
400
 
287
- return snapshot.docs.map((doc) => ({
401
+ const events = snapshot.docs.map((doc) => ({
288
402
  ...doc.data(),
289
403
  id: doc.id,
290
404
  }));
405
+
406
+ Logger.debug("[BookingAdmin] Retrieved practitioner calendar events", {
407
+ practitionerId,
408
+ eventsCount: events.length,
409
+ eventsTypes: this.summarizeEventTypes(events),
410
+ });
411
+
412
+ return events;
291
413
  } catch (error) {
292
- console.error(
293
- `[BookingAdmin] Error fetching practitioner calendar events:`,
294
- error
414
+ const errorMessage =
415
+ error instanceof Error ? error.message : String(error);
416
+ Logger.error(
417
+ "[BookingAdmin] Error fetching practitioner calendar events",
418
+ {
419
+ errorMessage,
420
+ practitionerId,
421
+ startTime: start.toDate().toISOString(),
422
+ endTime: end.toDate().toISOString(),
423
+ stack: error instanceof Error ? error.stack : undefined,
424
+ }
295
425
  );
296
426
  return [];
297
427
  }
298
428
  }
299
429
 
430
+ /**
431
+ * Summarizes event types for logging purposes
432
+ * @param events Array of calendar events
433
+ * @returns Object with counts of each event type
434
+ */
435
+ private summarizeEventTypes(events: any[]): Record<string, number> {
436
+ const typeCounts: Record<string, number> = {};
437
+
438
+ events.forEach((event) => {
439
+ const eventType = event.eventType || "unknown";
440
+ typeCounts[eventType] = (typeCounts[eventType] || 0) + 1;
441
+ });
442
+
443
+ return typeCounts;
444
+ }
445
+
300
446
  private _generateCalendarProcedureInfo(
301
447
  procedure: Procedure
302
448
  ): CalendarProcedureInfo {
@@ -70,12 +70,14 @@ import { Clinic, CLINICS_COLLECTION } from "../../types/clinic";
70
70
  import { ProcedureReviewInfo } from "../../types/reviews";
71
71
  import { distanceBetween, geohashQueryBounds } from "geofire-common";
72
72
  import { TreatmentBenefit } from "../../backoffice/types/static/treatment-benefit.types";
73
+ import { MediaService, MediaAccessLevel } from "../media/media.service";
73
74
 
74
75
  export class ProcedureService extends BaseService {
75
76
  private categoryService: CategoryService;
76
77
  private subcategoryService: SubcategoryService;
77
78
  private technologyService: TechnologyService;
78
79
  private productService: ProductService;
80
+ private mediaService: MediaService;
79
81
 
80
82
  constructor(
81
83
  db: Firestore,
@@ -84,13 +86,81 @@ export class ProcedureService extends BaseService {
84
86
  categoryService: CategoryService,
85
87
  subcategoryService: SubcategoryService,
86
88
  technologyService: TechnologyService,
87
- productService: ProductService
89
+ productService: ProductService,
90
+ mediaService: MediaService
88
91
  ) {
89
92
  super(db, auth, app);
90
93
  this.categoryService = categoryService;
91
94
  this.subcategoryService = subcategoryService;
92
95
  this.technologyService = technologyService;
93
96
  this.productService = productService;
97
+ this.mediaService = mediaService;
98
+ }
99
+
100
+ /**
101
+ * Process media resource (string URL or File object)
102
+ * @param media String URL or File object
103
+ * @param ownerId Owner ID for the media (usually procedureId)
104
+ * @param collectionName Collection name for organizing files
105
+ * @returns URL string after processing
106
+ */
107
+ private async processMedia(
108
+ media: string | File | Blob | null | undefined,
109
+ ownerId: string,
110
+ collectionName: string
111
+ ): Promise<string | null> {
112
+ if (!media) return null;
113
+
114
+ // If already a string URL, return it directly
115
+ if (typeof media === "string") {
116
+ return media;
117
+ }
118
+
119
+ // If it's a File, upload it using MediaService
120
+ if (media instanceof File || media instanceof Blob) {
121
+ console.log(
122
+ `[ProcedureService] Uploading ${collectionName} media for ${ownerId}`
123
+ );
124
+ const metadata = await this.mediaService.uploadMedia(
125
+ media,
126
+ ownerId,
127
+ MediaAccessLevel.PUBLIC,
128
+ collectionName
129
+ );
130
+ return metadata.url;
131
+ }
132
+
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Process array of media resources (strings or Files)
138
+ * @param mediaArray Array of string URLs or File objects
139
+ * @param ownerId Owner ID for the media
140
+ * @param collectionName Collection name for organizing files
141
+ * @returns Array of URL strings after processing
142
+ */
143
+ private async processMediaArray(
144
+ mediaArray: (string | File | Blob)[] | undefined,
145
+ ownerId: string,
146
+ collectionName: string
147
+ ): Promise<string[]> {
148
+ if (!mediaArray || mediaArray.length === 0) return [];
149
+
150
+ const result: string[] = [];
151
+
152
+ for (const media of mediaArray) {
153
+ const processedUrl = await this.processMedia(
154
+ media,
155
+ ownerId,
156
+ collectionName
157
+ );
158
+ if (processedUrl) {
159
+ result.push(processedUrl);
160
+ }
161
+ }
162
+
163
+ return result;
94
164
  }
95
165
 
96
166
  /**
@@ -101,6 +171,9 @@ export class ProcedureService extends BaseService {
101
171
  async createProcedure(data: CreateProcedureData): Promise<Procedure> {
102
172
  const validatedData = createProcedureSchema.parse(data);
103
173
 
174
+ // Generate procedure ID first so we can use it for media uploads
175
+ const procedureId = this.generateId();
176
+
104
177
  // Get references to related entities (Category, Subcategory, Technology, Product)
105
178
  const [category, subcategory, technology, product] = await Promise.all([
106
179
  this.categoryService.getById(validatedData.categoryId),
@@ -146,6 +219,16 @@ export class ProcedureService extends BaseService {
146
219
  }
147
220
  const practitioner = practitionerSnapshot.data() as Practitioner; // Assert type
148
221
 
222
+ // Process photos if provided
223
+ let processedPhotos: string[] = [];
224
+ if (validatedData.photos && validatedData.photos.length > 0) {
225
+ processedPhotos = await this.processMediaArray(
226
+ validatedData.photos,
227
+ procedureId,
228
+ "procedure-photos"
229
+ );
230
+ }
231
+
149
232
  // Create aggregated clinic info for the procedure document
150
233
  const clinicInfo = {
151
234
  id: clinicSnapshot.id,
@@ -174,10 +257,10 @@ export class ProcedureService extends BaseService {
174
257
  };
175
258
 
176
259
  // Create the procedure object
177
- const procedureId = this.generateId();
178
260
  const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
179
261
  id: procedureId,
180
262
  ...validatedData,
263
+ photos: processedPhotos,
181
264
  category, // Embed full objects
182
265
  subcategory,
183
266
  technology,
@@ -296,6 +379,15 @@ export class ProcedureService extends BaseService {
296
379
  let newPractitioner: Practitioner | null = null;
297
380
  let newClinic: Clinic | null = null;
298
381
 
382
+ // Process photos if provided
383
+ if (validatedData.photos !== undefined) {
384
+ updatedProcedureData.photos = await this.processMediaArray(
385
+ validatedData.photos,
386
+ id,
387
+ "procedure-photos"
388
+ );
389
+ }
390
+
299
391
  // --- Prepare updates and fetch new related data if IDs change ---
300
392
 
301
393
  // Handle Practitioner Change
@@ -20,6 +20,7 @@ import { DoctorInfo } from "../clinic";
20
20
  import { PRACTITIONERS_COLLECTION } from "../practitioner";
21
21
  import { ProcedureReviewInfo } from "../reviews";
22
22
  import type { Contraindication } from "../../backoffice/types/static/contraindication.types";
23
+ import { MediaResource } from "../../services/media/media.service";
23
24
 
24
25
  /**
25
26
  * Procedure represents a specific medical procedure that can be performed by a practitioner in a clinic
@@ -31,7 +32,7 @@ export interface Procedure {
31
32
  /** Name of the procedure */
32
33
  name: string;
33
34
  /** Photos of the procedure */
34
- photos?: string[];
35
+ photos?: MediaResource[];
35
36
  /** Detailed description of the procedure */
36
37
  description: string;
37
38
  /** Family of procedures this belongs to (aesthetics/surgery) */
@@ -101,7 +102,7 @@ export interface CreateProcedureData {
101
102
  duration: number;
102
103
  practitionerId: string;
103
104
  clinicBranchId: string;
104
- photos?: string[];
105
+ photos?: MediaResource[];
105
106
  }
106
107
 
107
108
  /**
@@ -121,7 +122,7 @@ export interface UpdateProcedureData {
121
122
  technologyId?: string;
122
123
  productId?: string;
123
124
  clinicBranchId?: string;
124
- photos?: string[];
125
+ photos?: MediaResource[];
125
126
  }
126
127
 
127
128
  /**
@@ -6,7 +6,7 @@ import {
6
6
  } from "../backoffice/types/static/pricing.types";
7
7
  import { clinicInfoSchema, doctorInfoSchema } from "./shared.schema";
8
8
  import { procedureReviewInfoSchema } from "./reviews.schema";
9
-
9
+ import { mediaResourceSchema } from "./media.schema";
10
10
  /**
11
11
  * Schema for creating a new procedure
12
12
  */
@@ -24,7 +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
+ photos: z.array(mediaResourceSchema).optional(),
28
28
  });
29
29
 
30
30
  /**
@@ -44,7 +44,7 @@ export const updateProcedureSchema = z.object({
44
44
  technologyId: z.string().optional(),
45
45
  productId: z.string().optional(),
46
46
  clinicBranchId: z.string().optional(),
47
- photos: z.array(z.string()).optional(),
47
+ photos: z.array(mediaResourceSchema).optional(),
48
48
  });
49
49
 
50
50
  /**