@blackcode_sa/metaestetics-api 1.11.3 → 1.12.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 (53) hide show
  1. package/dist/admin/index.d.mts +378 -334
  2. package/dist/admin/index.d.ts +378 -334
  3. package/dist/backoffice/index.d.mts +1198 -430
  4. package/dist/backoffice/index.d.ts +1198 -430
  5. package/dist/backoffice/index.js +1128 -245
  6. package/dist/backoffice/index.mjs +1119 -209
  7. package/dist/index.d.mts +4478 -4031
  8. package/dist/index.d.ts +4478 -4031
  9. package/dist/index.js +1974 -757
  10. package/dist/index.mjs +1735 -490
  11. package/package.json +1 -1
  12. package/src/backoffice/expo-safe/index.ts +4 -0
  13. package/src/backoffice/services/README.md +40 -0
  14. package/src/backoffice/services/brand.service.ts +85 -6
  15. package/src/backoffice/services/category.service.ts +92 -10
  16. package/src/backoffice/services/constants.service.ts +308 -0
  17. package/src/backoffice/services/documentation-template.service.ts +56 -2
  18. package/src/backoffice/services/index.ts +1 -0
  19. package/src/backoffice/services/product.service.ts +126 -5
  20. package/src/backoffice/services/requirement.service.ts +13 -0
  21. package/src/backoffice/services/subcategory.service.ts +184 -13
  22. package/src/backoffice/services/technology.service.ts +344 -129
  23. package/src/backoffice/types/admin-constants.types.ts +69 -0
  24. package/src/backoffice/types/brand.types.ts +1 -0
  25. package/src/backoffice/types/index.ts +2 -0
  26. package/src/backoffice/types/procedure-product.types.ts +38 -0
  27. package/src/backoffice/types/product.types.ts +31 -4
  28. package/src/backoffice/types/static/contraindication.types.ts +1 -0
  29. package/src/backoffice/types/static/treatment-benefit.types.ts +1 -0
  30. package/src/backoffice/types/technology.types.ts +113 -4
  31. package/src/backoffice/validations/schemas.ts +35 -9
  32. package/src/services/appointment/appointment.service.ts +0 -5
  33. package/src/services/appointment/utils/appointment.utils.ts +124 -113
  34. package/src/services/base.service.ts +10 -3
  35. package/src/services/documentation-templates/documentation-template.service.ts +116 -0
  36. package/src/services/media/media.service.ts +2 -2
  37. package/src/services/practitioner/practitioner.service.ts +201 -83
  38. package/src/services/procedure/README.md +76 -1
  39. package/src/services/procedure/procedure.service.ts +538 -235
  40. package/src/types/appointment/index.ts +2 -3
  41. package/src/types/clinic/index.ts +1 -6
  42. package/src/types/patient/medical-info.types.ts +3 -3
  43. package/src/types/procedure/index.ts +39 -20
  44. package/src/validations/clinic.schema.ts +1 -6
  45. package/src/validations/patient/medical-info.schema.ts +7 -2
  46. package/src/validations/procedure-product.schema.ts +41 -0
  47. package/src/validations/procedure.schema.ts +59 -8
  48. package/src/backoffice/services/__tests__/brand.service.test.ts +0 -196
  49. package/src/backoffice/services/__tests__/category.service.test.ts +0 -201
  50. package/src/backoffice/services/__tests__/product.service.test.ts +0 -358
  51. package/src/backoffice/services/__tests__/requirement.service.test.ts +0 -226
  52. package/src/backoffice/services/__tests__/subcategory.service.test.ts +0 -181
  53. package/src/backoffice/services/__tests__/technology.service.test.ts +0 -1097
@@ -20,39 +20,62 @@ import {
20
20
  startAfter,
21
21
  QueryConstraint,
22
22
  documentId,
23
- } from 'firebase/firestore';
24
- import { BaseService } from '../base.service';
23
+ } from "firebase/firestore";
24
+ import { BaseService } from "../base.service";
25
25
  import {
26
26
  Procedure,
27
27
  CreateProcedureData,
28
28
  UpdateProcedureData,
29
29
  PROCEDURES_COLLECTION,
30
30
  ProcedureSummaryInfo,
31
- } from '../../types/procedure';
32
- import { createProcedureSchema, updateProcedureSchema } from '../../validations/procedure.schema';
33
- import { z } from 'zod';
34
- import { Auth } from 'firebase/auth';
35
- import { Firestore } from 'firebase/firestore';
36
- import { FirebaseApp } from 'firebase/app';
37
- import { Category, CATEGORIES_COLLECTION } from '../../backoffice/types/category.types';
38
- import { Subcategory, SUBCATEGORIES_COLLECTION } from '../../backoffice/types/subcategory.types';
39
- import { Technology, TECHNOLOGIES_COLLECTION } from '../../backoffice/types/technology.types';
40
- import { Product, PRODUCTS_COLLECTION } from '../../backoffice/types/product.types';
41
- import { CategoryService } from '../../backoffice/services/category.service';
42
- import { SubcategoryService } from '../../backoffice/services/subcategory.service';
43
- import { TechnologyService } from '../../backoffice/services/technology.service';
44
- import { ProductService } from '../../backoffice/services/product.service';
45
- import { Practitioner, PRACTITIONERS_COLLECTION } from '../../types/practitioner';
31
+ } from "../../types/procedure";
32
+ import {
33
+ createProcedureSchema,
34
+ updateProcedureSchema,
35
+ } from "../../validations/procedure.schema";
36
+ import { z } from "zod";
37
+ import { Auth } from "firebase/auth";
38
+ import { Firestore } from "firebase/firestore";
39
+ import { FirebaseApp } from "firebase/app";
40
+ import {
41
+ Category,
42
+ CATEGORIES_COLLECTION,
43
+ } from "../../backoffice/types/category.types";
44
+ import {
45
+ Subcategory,
46
+ SUBCATEGORIES_COLLECTION,
47
+ } from "../../backoffice/types/subcategory.types";
48
+ import {
49
+ Technology,
50
+ TECHNOLOGIES_COLLECTION,
51
+ } from "../../backoffice/types/technology.types";
52
+ import {
53
+ Product,
54
+ PRODUCTS_COLLECTION,
55
+ } from "../../backoffice/types/product.types";
56
+ import { CategoryService } from "../../backoffice/services/category.service";
57
+ import { SubcategoryService } from "../../backoffice/services/subcategory.service";
58
+ import { TechnologyService } from "../../backoffice/services/technology.service";
59
+ import { ProductService } from "../../backoffice/services/product.service";
60
+ import {
61
+ Practitioner,
62
+ PRACTITIONERS_COLLECTION,
63
+ } from "../../types/practitioner";
46
64
  import {
47
65
  CertificationLevel,
48
66
  CertificationSpecialty,
49
67
  ProcedureFamily,
50
- } from '../../backoffice/types';
51
- import { Clinic, CLINICS_COLLECTION } from '../../types/clinic';
52
- import { ProcedureReviewInfo } from '../../types/reviews';
53
- import { distanceBetween, geohashQueryBounds } from 'geofire-common';
54
- import { TreatmentBenefit } from '../../backoffice/types/static/treatment-benefit.types';
55
- import { MediaService, MediaAccessLevel } from '../media/media.service';
68
+ type TreatmentBenefitDynamic,
69
+ } from "../../backoffice/types";
70
+ import {
71
+ Currency,
72
+ PricingMeasure,
73
+ } from "../../backoffice/types/static/pricing.types";
74
+ import { Clinic, CLINICS_COLLECTION } from "../../types/clinic";
75
+ import { ProcedureReviewInfo } from "../../types/reviews";
76
+ import { distanceBetween, geohashQueryBounds } from "geofire-common";
77
+ import { MediaService, MediaAccessLevel } from "../media/media.service";
78
+ import type { ProcedureProduct } from "../../backoffice/types/procedure-product.types";
56
79
 
57
80
  export class ProcedureService extends BaseService {
58
81
  private categoryService: CategoryService;
@@ -69,7 +92,7 @@ export class ProcedureService extends BaseService {
69
92
  subcategoryService: SubcategoryService,
70
93
  technologyService: TechnologyService,
71
94
  productService: ProductService,
72
- mediaService: MediaService,
95
+ mediaService: MediaService
73
96
  ) {
74
97
  super(db, auth, app);
75
98
  this.categoryService = categoryService;
@@ -89,23 +112,25 @@ export class ProcedureService extends BaseService {
89
112
  private async processMedia(
90
113
  media: string | File | Blob | null | undefined,
91
114
  ownerId: string,
92
- collectionName: string,
115
+ collectionName: string
93
116
  ): Promise<string | null> {
94
117
  if (!media) return null;
95
118
 
96
119
  // If already a string URL, return it directly
97
- if (typeof media === 'string') {
120
+ if (typeof media === "string") {
98
121
  return media;
99
122
  }
100
123
 
101
124
  // If it's a File, upload it using MediaService
102
125
  if (media instanceof File || media instanceof Blob) {
103
- console.log(`[ProcedureService] Uploading ${collectionName} media for ${ownerId}`);
126
+ console.log(
127
+ `[ProcedureService] Uploading ${collectionName} media for ${ownerId}`
128
+ );
104
129
  const metadata = await this.mediaService.uploadMedia(
105
130
  media,
106
131
  ownerId,
107
132
  MediaAccessLevel.PUBLIC,
108
- collectionName,
133
+ collectionName
109
134
  );
110
135
  return metadata.url;
111
136
  }
@@ -123,14 +148,18 @@ export class ProcedureService extends BaseService {
123
148
  private async processMediaArray(
124
149
  mediaArray: (string | File | Blob)[] | undefined,
125
150
  ownerId: string,
126
- collectionName: string,
151
+ collectionName: string
127
152
  ): Promise<string[]> {
128
153
  if (!mediaArray || mediaArray.length === 0) return [];
129
154
 
130
155
  const result: string[] = [];
131
156
 
132
157
  for (const media of mediaArray) {
133
- const processedUrl = await this.processMedia(media, ownerId, collectionName);
158
+ const processedUrl = await this.processMedia(
159
+ media,
160
+ ownerId,
161
+ collectionName
162
+ );
134
163
  if (processedUrl) {
135
164
  result.push(processedUrl);
136
165
  }
@@ -139,6 +168,49 @@ export class ProcedureService extends BaseService {
139
168
  return result;
140
169
  }
141
170
 
171
+ /**
172
+ * Transforms validated procedure product data (with productId) to ProcedureProduct objects (with full product)
173
+ * @param productsMetadata Array of validated procedure product data
174
+ * @param technologyId Technology ID to fetch products from
175
+ * @returns Array of ProcedureProduct objects with full product information
176
+ */
177
+ private async transformProductsMetadata(
178
+ productsMetadata: {
179
+ productId: string;
180
+ price: number;
181
+ currency: Currency;
182
+ pricingMeasure: PricingMeasure;
183
+ isDefault?: boolean;
184
+ }[],
185
+ technologyId: string
186
+ ): Promise<ProcedureProduct[]> {
187
+ const transformedProducts: ProcedureProduct[] = [];
188
+
189
+ for (const productData of productsMetadata) {
190
+ // Fetch the full product object
191
+ const product = await this.productService.getById(
192
+ technologyId,
193
+ productData.productId
194
+ );
195
+ if (!product) {
196
+ throw new Error(
197
+ `Product with ID ${productData.productId} not found for technology ${technologyId}`
198
+ );
199
+ }
200
+
201
+ // Transform to ProcedureProduct
202
+ transformedProducts.push({
203
+ product,
204
+ price: productData.price,
205
+ currency: productData.currency,
206
+ pricingMeasure: productData.pricingMeasure,
207
+ isDefault: productData.isDefault,
208
+ });
209
+ }
210
+
211
+ return transformedProducts;
212
+ }
213
+
142
214
  /**
143
215
  * Creates a new procedure
144
216
  * @param data - The data for creating a new procedure
@@ -153,27 +225,45 @@ export class ProcedureService extends BaseService {
153
225
  // Get references to related entities (Category, Subcategory, Technology, Product)
154
226
  const [category, subcategory, technology, product] = await Promise.all([
155
227
  this.categoryService.getById(validatedData.categoryId),
156
- this.subcategoryService.getById(validatedData.categoryId, validatedData.subcategoryId),
228
+ this.subcategoryService.getById(
229
+ validatedData.categoryId,
230
+ validatedData.subcategoryId
231
+ ),
157
232
  this.technologyService.getById(validatedData.technologyId),
158
- this.productService.getById(validatedData.technologyId, validatedData.productId),
233
+ this.productService.getById(
234
+ validatedData.technologyId,
235
+ validatedData.productId
236
+ ),
159
237
  ]);
160
238
 
161
239
  if (!category || !subcategory || !technology || !product) {
162
- throw new Error('One or more required base entities not found');
240
+ throw new Error("One or more required base entities not found");
163
241
  }
164
242
 
165
243
  // Get clinic and practitioner information for aggregation
166
- const clinicRef = doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId);
244
+ const clinicRef = doc(
245
+ this.db,
246
+ CLINICS_COLLECTION,
247
+ validatedData.clinicBranchId
248
+ );
167
249
  const clinicSnapshot = await getDoc(clinicRef);
168
250
  if (!clinicSnapshot.exists()) {
169
- throw new Error(`Clinic with ID ${validatedData.clinicBranchId} not found`);
251
+ throw new Error(
252
+ `Clinic with ID ${validatedData.clinicBranchId} not found`
253
+ );
170
254
  }
171
255
  const clinic = clinicSnapshot.data() as Clinic; // Assert type
172
256
 
173
- const practitionerRef = doc(this.db, PRACTITIONERS_COLLECTION, validatedData.practitionerId);
257
+ const practitionerRef = doc(
258
+ this.db,
259
+ PRACTITIONERS_COLLECTION,
260
+ validatedData.practitionerId
261
+ );
174
262
  const practitionerSnapshot = await getDoc(practitionerRef);
175
263
  if (!practitionerSnapshot.exists()) {
176
- throw new Error(`Practitioner with ID ${validatedData.practitionerId} not found`);
264
+ throw new Error(
265
+ `Practitioner with ID ${validatedData.practitionerId} not found`
266
+ );
177
267
  }
178
268
  const practitioner = practitionerSnapshot.data() as Practitioner; // Assert type
179
269
 
@@ -183,23 +273,29 @@ export class ProcedureService extends BaseService {
183
273
  processedPhotos = await this.processMediaArray(
184
274
  validatedData.photos,
185
275
  procedureId,
186
- 'procedure-photos',
276
+ "procedure-photos"
187
277
  );
188
278
  }
189
279
 
280
+ // Transform productsMetadata from validation format to ProcedureProduct format
281
+ const transformedProductsMetadata = await this.transformProductsMetadata(
282
+ validatedData.productsMetadata,
283
+ validatedData.technologyId
284
+ );
285
+
190
286
  // Create aggregated clinic info for the procedure document
191
287
  const clinicInfo = {
192
288
  id: clinicSnapshot.id,
193
289
  name: clinic.name,
194
- description: clinic.description || '',
290
+ description: clinic.description || "",
195
291
  featuredPhoto:
196
292
  clinic.featuredPhotos && clinic.featuredPhotos.length > 0
197
- ? typeof clinic.featuredPhotos[0] === 'string'
293
+ ? typeof clinic.featuredPhotos[0] === "string"
198
294
  ? clinic.featuredPhotos[0]
199
- : ''
200
- : typeof clinic.coverPhoto === 'string'
295
+ : ""
296
+ : typeof clinic.coverPhoto === "string"
201
297
  ? clinic.coverPhoto
202
- : '',
298
+ : "",
203
299
  location: clinic.location,
204
300
  contactInfo: clinic.contactInfo,
205
301
  };
@@ -208,29 +304,33 @@ export class ProcedureService extends BaseService {
208
304
  const doctorInfo = {
209
305
  id: practitionerSnapshot.id,
210
306
  name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
211
- description: practitioner.basicInfo.bio || '',
307
+ description: practitioner.basicInfo.bio || "",
212
308
  photo:
213
- typeof practitioner.basicInfo.profileImageUrl === 'string'
309
+ typeof practitioner.basicInfo.profileImageUrl === "string"
214
310
  ? practitioner.basicInfo.profileImageUrl
215
- : '', // Default to empty string if not a processed URL
311
+ : "", // Default to empty string if not a processed URL
216
312
  rating: practitioner.reviewInfo?.averageRating || 0,
217
313
  services: practitioner.procedures || [],
218
314
  };
219
315
 
220
316
  // Create the procedure object
221
- const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
317
+ const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
222
318
  id: procedureId,
223
319
  ...validatedData,
224
320
  // Ensure nameLower is always set even if omitted by client
225
- nameLower: (validatedData as any).nameLower || validatedData.name.toLowerCase(),
321
+ nameLower:
322
+ (validatedData as any).nameLower || validatedData.name.toLowerCase(),
226
323
  photos: processedPhotos,
227
324
  category, // Embed full objects
228
325
  subcategory,
229
326
  technology,
230
327
  product,
328
+ productsMetadata: transformedProductsMetadata,
231
329
  blockingConditions: technology.blockingConditions,
232
330
  contraindications: technology.contraindications || [],
331
+ contraindicationIds: technology.contraindications?.map((c) => c.id) || [],
233
332
  treatmentBenefits: technology.benefits,
333
+ treatmentBenefitIds: technology.benefits?.map((b) => b.id) || [],
234
334
  preRequirements: technology.requirements.pre,
235
335
  postRequirements: technology.requirements.post,
236
336
  certificationRequirement: technology.certificationRequirement,
@@ -273,12 +373,12 @@ export class ProcedureService extends BaseService {
273
373
  * @returns A promise that resolves to an array of the newly created procedures.
274
374
  */
275
375
  async bulkCreateProcedures(
276
- baseData: Omit<CreateProcedureData, 'practitionerId'>,
277
- practitionerIds: string[],
376
+ baseData: Omit<CreateProcedureData, "practitionerId">,
377
+ practitionerIds: string[]
278
378
  ): Promise<Procedure[]> {
279
379
  // 1. Validation
280
380
  if (!practitionerIds || practitionerIds.length === 0) {
281
- throw new Error('Practitioner IDs array cannot be empty.');
381
+ throw new Error("Practitioner IDs array cannot be empty.");
282
382
  }
283
383
 
284
384
  // Add a dummy practitionerId for the validation schema to pass
@@ -286,19 +386,28 @@ export class ProcedureService extends BaseService {
286
386
  const validatedData = createProcedureSchema.parse(validationData);
287
387
 
288
388
  // 2. Fetch common data once to avoid redundant reads
289
- const [category, subcategory, technology, product, clinicSnapshot] = await Promise.all([
290
- this.categoryService.getById(validatedData.categoryId),
291
- this.subcategoryService.getById(validatedData.categoryId, validatedData.subcategoryId),
292
- this.technologyService.getById(validatedData.technologyId),
293
- this.productService.getById(validatedData.technologyId, validatedData.productId),
294
- getDoc(doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId)),
295
- ]);
389
+ const [category, subcategory, technology, product, clinicSnapshot] =
390
+ await Promise.all([
391
+ this.categoryService.getById(validatedData.categoryId),
392
+ this.subcategoryService.getById(
393
+ validatedData.categoryId,
394
+ validatedData.subcategoryId
395
+ ),
396
+ this.technologyService.getById(validatedData.technologyId),
397
+ this.productService.getById(
398
+ validatedData.technologyId,
399
+ validatedData.productId
400
+ ),
401
+ getDoc(doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId)),
402
+ ]);
296
403
 
297
404
  if (!category || !subcategory || !technology || !product) {
298
- throw new Error('One or more required base entities not found');
405
+ throw new Error("One or more required base entities not found");
299
406
  }
300
407
  if (!clinicSnapshot.exists()) {
301
- throw new Error(`Clinic with ID ${validatedData.clinicBranchId} not found`);
408
+ throw new Error(
409
+ `Clinic with ID ${validatedData.clinicBranchId} not found`
410
+ );
302
411
  }
303
412
  const clinic = clinicSnapshot.data() as Clinic;
304
413
 
@@ -309,10 +418,16 @@ export class ProcedureService extends BaseService {
309
418
  processedPhotos = await this.processMediaArray(
310
419
  validatedData.photos,
311
420
  batchId,
312
- 'procedure-photos-batch',
421
+ "procedure-photos-batch"
313
422
  );
314
423
  }
315
424
 
425
+ // Transform productsMetadata from validation format to ProcedureProduct format
426
+ const transformedProductsMetadata = await this.transformProductsMetadata(
427
+ validatedData.productsMetadata,
428
+ validatedData.technologyId
429
+ );
430
+
316
431
  // 4. Fetch all practitioner data efficiently
317
432
  const practitionersMap = new Map<string, Practitioner>();
318
433
  // Use 'in' query in chunks of 30, as this is the Firestore limit
@@ -320,7 +435,7 @@ export class ProcedureService extends BaseService {
320
435
  const chunk = practitionerIds.slice(i, i + 30);
321
436
  const practitionersQuery = query(
322
437
  collection(this.db, PRACTITIONERS_COLLECTION),
323
- where(documentId(), 'in', chunk),
438
+ where(documentId(), "in", chunk)
324
439
  );
325
440
  const practitionersSnapshot = await getDocs(practitionersQuery);
326
441
  practitionersSnapshot.docs.forEach((doc) => {
@@ -331,8 +446,12 @@ export class ProcedureService extends BaseService {
331
446
  // Verify all practitioners were found
332
447
  if (practitionersMap.size !== practitionerIds.length) {
333
448
  const foundIds = Array.from(practitionersMap.keys());
334
- const notFoundIds = practitionerIds.filter((id) => !foundIds.includes(id));
335
- throw new Error(`The following practitioners were not found: ${notFoundIds.join(', ')}`);
449
+ const notFoundIds = practitionerIds.filter(
450
+ (id) => !foundIds.includes(id)
451
+ );
452
+ throw new Error(
453
+ `The following practitioners were not found: ${notFoundIds.join(", ")}`
454
+ );
336
455
  }
337
456
 
338
457
  // 5. Use a Firestore batch for atomic creation
@@ -341,15 +460,15 @@ export class ProcedureService extends BaseService {
341
460
  const clinicInfo = {
342
461
  id: clinicSnapshot.id,
343
462
  name: clinic.name,
344
- description: clinic.description || '',
463
+ description: clinic.description || "",
345
464
  featuredPhoto:
346
465
  clinic.featuredPhotos && clinic.featuredPhotos.length > 0
347
- ? typeof clinic.featuredPhotos[0] === 'string'
466
+ ? typeof clinic.featuredPhotos[0] === "string"
348
467
  ? clinic.featuredPhotos[0]
349
- : ''
350
- : typeof clinic.coverPhoto === 'string'
468
+ : ""
469
+ : typeof clinic.coverPhoto === "string"
351
470
  ? clinic.coverPhoto
352
- : '',
471
+ : "",
353
472
  location: clinic.location,
354
473
  contactInfo: clinic.contactInfo,
355
474
  };
@@ -360,11 +479,11 @@ export class ProcedureService extends BaseService {
360
479
  const doctorInfo = {
361
480
  id: practitioner.id,
362
481
  name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
363
- description: practitioner.basicInfo.bio || '',
482
+ description: practitioner.basicInfo.bio || "",
364
483
  photo:
365
- typeof practitioner.basicInfo.profileImageUrl === 'string'
484
+ typeof practitioner.basicInfo.profileImageUrl === "string"
366
485
  ? practitioner.basicInfo.profileImageUrl
367
- : '',
486
+ : "",
368
487
  rating: practitioner.reviewInfo?.averageRating || 0,
369
488
  services: practitioner.procedures || [],
370
489
  };
@@ -374,19 +493,24 @@ export class ProcedureService extends BaseService {
374
493
  const procedureRef = doc(this.db, PROCEDURES_COLLECTION, procedureId);
375
494
 
376
495
  // Construct the new procedure, reusing common data
377
- const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
496
+ const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
378
497
  id: procedureId,
379
498
  ...validatedData,
380
- nameLower: (validatedData as any).nameLower || validatedData.name.toLowerCase(),
499
+ nameLower:
500
+ (validatedData as any).nameLower || validatedData.name.toLowerCase(),
381
501
  practitionerId: practitionerId, // Override practitionerId with the correct one
382
502
  photos: processedPhotos,
383
503
  category,
384
504
  subcategory,
385
505
  technology,
386
506
  product,
507
+ productsMetadata: transformedProductsMetadata,
387
508
  blockingConditions: technology.blockingConditions,
388
509
  contraindications: technology.contraindications || [],
510
+ contraindicationIds:
511
+ technology.contraindications?.map((c) => c.id) || [],
389
512
  treatmentBenefits: technology.benefits,
513
+ treatmentBenefitIds: technology.benefits?.map((b) => b.id) || [],
390
514
  preRequirements: technology.requirements.pre,
391
515
  postRequirements: technology.requirements.post,
392
516
  certificationRequirement: technology.certificationRequirement,
@@ -420,7 +544,10 @@ export class ProcedureService extends BaseService {
420
544
  const fetchedProcedures: Procedure[] = [];
421
545
  for (let i = 0; i < createdProcedureIds.length; i += 30) {
422
546
  const chunk = createdProcedureIds.slice(i, i + 30);
423
- const q = query(collection(this.db, PROCEDURES_COLLECTION), where(documentId(), 'in', chunk));
547
+ const q = query(
548
+ collection(this.db, PROCEDURES_COLLECTION),
549
+ where(documentId(), "in", chunk)
550
+ );
424
551
  const snapshot = await getDocs(q);
425
552
  snapshot.forEach((doc) => {
426
553
  fetchedProcedures.push(doc.data() as Procedure);
@@ -451,11 +578,13 @@ export class ProcedureService extends BaseService {
451
578
  * @param clinicBranchId - The ID of the clinic branch
452
579
  * @returns List of procedures
453
580
  */
454
- async getProceduresByClinicBranch(clinicBranchId: string): Promise<Procedure[]> {
581
+ async getProceduresByClinicBranch(
582
+ clinicBranchId: string
583
+ ): Promise<Procedure[]> {
455
584
  const q = query(
456
585
  collection(this.db, PROCEDURES_COLLECTION),
457
- where('clinicBranchId', '==', clinicBranchId),
458
- where('isActive', '==', true),
586
+ where("clinicBranchId", "==", clinicBranchId),
587
+ where("isActive", "==", true)
459
588
  );
460
589
  const snapshot = await getDocs(q);
461
590
  return snapshot.docs.map((doc) => doc.data() as Procedure);
@@ -466,11 +595,13 @@ export class ProcedureService extends BaseService {
466
595
  * @param practitionerId - The ID of the practitioner
467
596
  * @returns List of procedures
468
597
  */
469
- async getProceduresByPractitioner(practitionerId: string): Promise<Procedure[]> {
598
+ async getProceduresByPractitioner(
599
+ practitionerId: string
600
+ ): Promise<Procedure[]> {
470
601
  const q = query(
471
602
  collection(this.db, PROCEDURES_COLLECTION),
472
- where('practitionerId', '==', practitionerId),
473
- where('isActive', '==', true),
603
+ where("practitionerId", "==", practitionerId),
604
+ where("isActive", "==", true)
474
605
  );
475
606
  const snapshot = await getDocs(q);
476
607
  return snapshot.docs.map((doc) => doc.data() as Procedure);
@@ -481,11 +612,13 @@ export class ProcedureService extends BaseService {
481
612
  * @param practitionerId - The ID of the practitioner
482
613
  * @returns List of inactive procedures
483
614
  */
484
- async getInactiveProceduresByPractitioner(practitionerId: string): Promise<Procedure[]> {
615
+ async getInactiveProceduresByPractitioner(
616
+ practitionerId: string
617
+ ): Promise<Procedure[]> {
485
618
  const q = query(
486
619
  collection(this.db, PROCEDURES_COLLECTION),
487
- where('practitionerId', '==', practitionerId),
488
- where('isActive', '==', false),
620
+ where("practitionerId", "==", practitionerId),
621
+ where("isActive", "==", false)
489
622
  );
490
623
  const snapshot = await getDocs(q);
491
624
  return snapshot.docs.map((doc) => doc.data() as Procedure);
@@ -497,7 +630,10 @@ export class ProcedureService extends BaseService {
497
630
  * @param data - The data to update the procedure with
498
631
  * @returns The updated procedure
499
632
  */
500
- async updateProcedure(id: string, data: UpdateProcedureData): Promise<Procedure> {
633
+ async updateProcedure(
634
+ id: string,
635
+ data: UpdateProcedureData
636
+ ): Promise<Procedure> {
501
637
  const validatedData = updateProcedureSchema.parse(data);
502
638
  const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
503
639
  const procedureSnapshot = await getDoc(procedureRef);
@@ -507,7 +643,23 @@ export class ProcedureService extends BaseService {
507
643
  }
508
644
 
509
645
  const existingProcedure = procedureSnapshot.data() as Procedure;
510
- let updatedProcedureData: Partial<Procedure> = { ...validatedData };
646
+ let updatedProcedureData: Partial<Procedure> = {};
647
+
648
+ // Copy validated simple fields
649
+ if (validatedData.name !== undefined)
650
+ updatedProcedureData.name = validatedData.name;
651
+ if (validatedData.description !== undefined)
652
+ updatedProcedureData.description = validatedData.description;
653
+ if (validatedData.price !== undefined)
654
+ updatedProcedureData.price = validatedData.price;
655
+ if (validatedData.currency !== undefined)
656
+ updatedProcedureData.currency = validatedData.currency;
657
+ if (validatedData.pricingMeasure !== undefined)
658
+ updatedProcedureData.pricingMeasure = validatedData.pricingMeasure;
659
+ if (validatedData.duration !== undefined)
660
+ updatedProcedureData.duration = validatedData.duration;
661
+ if (validatedData.isActive !== undefined)
662
+ updatedProcedureData.isActive = validatedData.isActive;
511
663
 
512
664
  let practitionerChanged = false;
513
665
  let clinicChanged = false;
@@ -521,42 +673,70 @@ export class ProcedureService extends BaseService {
521
673
  updatedProcedureData.photos = await this.processMediaArray(
522
674
  validatedData.photos,
523
675
  id,
524
- 'procedure-photos',
676
+ "procedure-photos"
525
677
  );
526
678
  }
527
679
 
680
+ // Transform productsMetadata if provided
681
+ if (validatedData.productsMetadata !== undefined) {
682
+ const technologyId =
683
+ validatedData.technologyId ?? existingProcedure.technology.id;
684
+ if (!technologyId) {
685
+ throw new Error(
686
+ "Technology ID is required for updating products metadata"
687
+ );
688
+ }
689
+ updatedProcedureData.productsMetadata =
690
+ await this.transformProductsMetadata(
691
+ validatedData.productsMetadata,
692
+ technologyId
693
+ );
694
+ }
695
+
528
696
  // --- Prepare updates and fetch new related data if IDs change ---
529
697
 
530
698
  // Handle Practitioner Change
531
- if (validatedData.practitionerId && validatedData.practitionerId !== oldPractitionerId) {
699
+ if (
700
+ validatedData.practitionerId &&
701
+ validatedData.practitionerId !== oldPractitionerId
702
+ ) {
532
703
  practitionerChanged = true;
533
704
  const newPractitionerRef = doc(
534
705
  this.db,
535
706
  PRACTITIONERS_COLLECTION,
536
- validatedData.practitionerId,
707
+ validatedData.practitionerId
537
708
  );
538
709
  const newPractitionerSnap = await getDoc(newPractitionerRef);
539
710
  if (!newPractitionerSnap.exists())
540
- throw new Error(`New Practitioner ${validatedData.practitionerId} not found`);
711
+ throw new Error(
712
+ `New Practitioner ${validatedData.practitionerId} not found`
713
+ );
541
714
  newPractitioner = newPractitionerSnap.data() as Practitioner;
542
715
  // Update doctorInfo within the procedure document
543
716
  updatedProcedureData.doctorInfo = {
544
717
  id: newPractitioner.id,
545
718
  name: `${newPractitioner.basicInfo.firstName} ${newPractitioner.basicInfo.lastName}`,
546
- description: newPractitioner.basicInfo.bio || '',
719
+ description: newPractitioner.basicInfo.bio || "",
547
720
  photo:
548
- typeof newPractitioner.basicInfo.profileImageUrl === 'string'
721
+ typeof newPractitioner.basicInfo.profileImageUrl === "string"
549
722
  ? newPractitioner.basicInfo.profileImageUrl
550
- : '', // Default to empty string if not a processed URL
723
+ : "", // Default to empty string if not a processed URL
551
724
  rating: newPractitioner.reviewInfo?.averageRating || 0,
552
725
  services: newPractitioner.procedures || [],
553
726
  };
554
727
  }
555
728
 
556
729
  // Handle Clinic Change
557
- if (validatedData.clinicBranchId && validatedData.clinicBranchId !== oldClinicId) {
730
+ if (
731
+ validatedData.clinicBranchId &&
732
+ validatedData.clinicBranchId !== oldClinicId
733
+ ) {
558
734
  clinicChanged = true;
559
- const newClinicRef = doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId);
735
+ const newClinicRef = doc(
736
+ this.db,
737
+ CLINICS_COLLECTION,
738
+ validatedData.clinicBranchId
739
+ );
560
740
  const newClinicSnap = await getDoc(newClinicRef);
561
741
  if (!newClinicSnap.exists())
562
742
  throw new Error(`New Clinic ${validatedData.clinicBranchId} not found`);
@@ -565,15 +745,15 @@ export class ProcedureService extends BaseService {
565
745
  updatedProcedureData.clinicInfo = {
566
746
  id: newClinic.id,
567
747
  name: newClinic.name,
568
- description: newClinic.description || '',
748
+ description: newClinic.description || "",
569
749
  featuredPhoto:
570
750
  newClinic.featuredPhotos && newClinic.featuredPhotos.length > 0
571
- ? typeof newClinic.featuredPhotos[0] === 'string'
751
+ ? typeof newClinic.featuredPhotos[0] === "string"
572
752
  ? newClinic.featuredPhotos[0]
573
- : ''
574
- : typeof newClinic.coverPhoto === 'string'
753
+ : ""
754
+ : typeof newClinic.coverPhoto === "string"
575
755
  ? newClinic.coverPhoto
576
- : '',
756
+ : "",
577
757
  location: newClinic.location,
578
758
  contactInfo: newClinic.contactInfo,
579
759
  };
@@ -585,8 +765,11 @@ export class ProcedureService extends BaseService {
585
765
  updatedProcedureData.nameLower = validatedData.name.toLowerCase();
586
766
  }
587
767
  if (validatedData.categoryId) {
588
- const category = await this.categoryService.getById(validatedData.categoryId);
589
- if (!category) throw new Error(`Category ${validatedData.categoryId} not found`);
768
+ const category = await this.categoryService.getById(
769
+ validatedData.categoryId
770
+ );
771
+ if (!category)
772
+ throw new Error(`Category ${validatedData.categoryId} not found`);
590
773
  updatedProcedureData.category = category;
591
774
  finalCategoryId = category.id; // Update finalCategoryId if category changed
592
775
  }
@@ -595,42 +778,58 @@ export class ProcedureService extends BaseService {
595
778
  if (validatedData.subcategoryId && finalCategoryId) {
596
779
  const subcategory = await this.subcategoryService.getById(
597
780
  finalCategoryId,
598
- validatedData.subcategoryId,
781
+ validatedData.subcategoryId
599
782
  );
600
783
  if (!subcategory)
601
784
  throw new Error(
602
- `Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}`,
785
+ `Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}`
603
786
  );
604
787
  updatedProcedureData.subcategory = subcategory;
605
788
  } else if (validatedData.subcategoryId) {
606
- console.warn('Attempted to update subcategory without a valid categoryId');
789
+ console.warn(
790
+ "Attempted to update subcategory without a valid categoryId"
791
+ );
607
792
  }
608
793
 
609
794
  let finalTechnologyId = existingProcedure.technology.id;
610
795
  if (validatedData.technologyId) {
611
- const technology = await this.technologyService.getById(validatedData.technologyId);
612
- if (!technology) throw new Error(`Technology ${validatedData.technologyId} not found`);
796
+ const technology = await this.technologyService.getById(
797
+ validatedData.technologyId
798
+ );
799
+ if (!technology)
800
+ throw new Error(`Technology ${validatedData.technologyId} not found`);
613
801
  updatedProcedureData.technology = technology;
614
802
  finalTechnologyId = technology.id; // Update finalTechnologyId if technology changed
615
803
  // Update related fields derived from technology
616
804
  updatedProcedureData.blockingConditions = technology.blockingConditions;
805
+ updatedProcedureData.contraindications =
806
+ technology.contraindications || [];
807
+ updatedProcedureData.contraindicationIds =
808
+ technology.contraindications?.map((c) => c.id) || [];
617
809
  updatedProcedureData.treatmentBenefits = technology.benefits;
810
+ updatedProcedureData.treatmentBenefitIds =
811
+ technology.benefits?.map((b) => b.id) || [];
618
812
  updatedProcedureData.preRequirements = technology.requirements.pre;
619
813
  updatedProcedureData.postRequirements = technology.requirements.post;
620
- updatedProcedureData.certificationRequirement = technology.certificationRequirement;
621
- updatedProcedureData.documentationTemplates = technology.documentationTemplates || [];
814
+ updatedProcedureData.certificationRequirement =
815
+ technology.certificationRequirement;
816
+ updatedProcedureData.documentationTemplates =
817
+ technology.documentationTemplates || [];
622
818
  }
623
819
 
624
820
  // Only fetch product if its ID is provided AND we have a valid finalTechnologyId
625
821
  if (validatedData.productId && finalTechnologyId) {
626
- const product = await this.productService.getById(finalTechnologyId, validatedData.productId);
822
+ const product = await this.productService.getById(
823
+ finalTechnologyId,
824
+ validatedData.productId
825
+ );
627
826
  if (!product)
628
827
  throw new Error(
629
- `Product ${validatedData.productId} not found for technology ${finalTechnologyId}`,
828
+ `Product ${validatedData.productId} not found for technology ${finalTechnologyId}`
630
829
  );
631
830
  updatedProcedureData.product = product;
632
831
  } else if (validatedData.productId) {
633
- console.warn('Attempted to update product without a valid technologyId');
832
+ console.warn("Attempted to update product without a valid technologyId");
634
833
  }
635
834
 
636
835
  // Update the procedure document
@@ -714,7 +913,7 @@ export class ProcedureService extends BaseService {
714
913
  */
715
914
  async getAllProcedures(
716
915
  pagination?: number,
717
- lastDoc?: any,
916
+ lastDoc?: any
718
917
  ): Promise<{ procedures: Procedure[]; lastDoc: any }> {
719
918
  try {
720
919
  const proceduresCollection = collection(this.db, PROCEDURES_COLLECTION);
@@ -722,24 +921,29 @@ export class ProcedureService extends BaseService {
722
921
 
723
922
  // Apply pagination if specified
724
923
  if (pagination && pagination > 0) {
725
- const { limit, startAfter } = await import('firebase/firestore'); // Use dynamic import if needed top-level
924
+ const { limit, startAfter } = await import("firebase/firestore"); // Use dynamic import if needed top-level
726
925
 
727
926
  if (lastDoc) {
728
927
  proceduresQuery = query(
729
928
  proceduresCollection,
730
- orderBy('name'), // Use imported orderBy
929
+ orderBy("name"), // Use imported orderBy
731
930
  startAfter(lastDoc),
732
- limit(pagination),
931
+ limit(pagination)
733
932
  );
734
933
  } else {
735
- proceduresQuery = query(proceduresCollection, orderBy('name'), limit(pagination)); // Use imported orderBy
934
+ proceduresQuery = query(
935
+ proceduresCollection,
936
+ orderBy("name"),
937
+ limit(pagination)
938
+ ); // Use imported orderBy
736
939
  }
737
940
  } else {
738
- proceduresQuery = query(proceduresCollection, orderBy('name')); // Use imported orderBy
941
+ proceduresQuery = query(proceduresCollection, orderBy("name")); // Use imported orderBy
739
942
  }
740
943
 
741
944
  const proceduresSnapshot = await getDocs(proceduresQuery);
742
- const lastVisible = proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
945
+ const lastVisible =
946
+ proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
743
947
 
744
948
  const procedures = proceduresSnapshot.docs.map((doc) => {
745
949
  const data = doc.data() as Procedure;
@@ -754,7 +958,7 @@ export class ProcedureService extends BaseService {
754
958
  lastDoc: lastVisible,
755
959
  };
756
960
  } catch (error) {
757
- console.error('[PROCEDURE_SERVICE] Error getting all procedures:', error);
961
+ console.error("[PROCEDURE_SERVICE] Error getting all procedures:", error);
758
962
  throw error;
759
963
  }
760
964
  }
@@ -766,7 +970,7 @@ export class ProcedureService extends BaseService {
766
970
  *
767
971
  * @param filters - Various filters to apply
768
972
  * @param filters.nameSearch - Optional search text for procedure name
769
- * @param filters.treatmentBenefits - Optional array of treatment benefits to filter by
973
+ * @param filters.treatmentBenefitIds - Optional array of treatment benefits to filter by
770
974
  * @param filters.procedureFamily - Optional procedure family to filter by
771
975
  * @param filters.procedureCategory - Optional procedure category to filter by
772
976
  * @param filters.procedureSubcategory - Optional procedure subcategory to filter by
@@ -784,7 +988,7 @@ export class ProcedureService extends BaseService {
784
988
  */
785
989
  async getProceduresByFilters(filters: {
786
990
  nameSearch?: string;
787
- treatmentBenefits?: TreatmentBenefit[];
991
+ treatmentBenefits?: string[];
788
992
  procedureFamily?: ProcedureFamily;
789
993
  procedureCategory?: string;
790
994
  procedureSubcategory?: string;
@@ -803,26 +1007,32 @@ export class ProcedureService extends BaseService {
803
1007
  lastDoc: any;
804
1008
  }> {
805
1009
  try {
806
- console.log('[PROCEDURE_SERVICE] Starting procedure filtering with multiple strategies');
1010
+ console.log(
1011
+ "[PROCEDURE_SERVICE] Starting procedure filtering with multiple strategies"
1012
+ );
807
1013
 
808
1014
  // Geo query debug i validacija
809
1015
  if (filters.location && filters.radiusInKm) {
810
- console.log('[PROCEDURE_SERVICE] Executing geo query:', {
1016
+ console.log("[PROCEDURE_SERVICE] Executing geo query:", {
811
1017
  location: filters.location,
812
1018
  radius: filters.radiusInKm,
813
- serviceName: 'ProcedureService',
1019
+ serviceName: "ProcedureService",
814
1020
  });
815
1021
 
816
1022
  // Validacija location podataka
817
1023
  if (!filters.location.latitude || !filters.location.longitude) {
818
- console.warn('[PROCEDURE_SERVICE] Invalid location data:', filters.location);
1024
+ console.warn(
1025
+ "[PROCEDURE_SERVICE] Invalid location data:",
1026
+ filters.location
1027
+ );
819
1028
  filters.location = undefined;
820
1029
  filters.radiusInKm = undefined;
821
1030
  }
822
1031
  }
823
1032
 
824
1033
  // Handle geo queries separately (they work differently)
825
- const isGeoQuery = filters.location && filters.radiusInKm && filters.radiusInKm > 0;
1034
+ const isGeoQuery =
1035
+ filters.location && filters.radiusInKm && filters.radiusInKm > 0;
826
1036
  if (isGeoQuery) {
827
1037
  return this.handleGeoQuery(filters);
828
1038
  }
@@ -833,39 +1043,55 @@ export class ProcedureService extends BaseService {
833
1043
 
834
1044
  // Active status filter
835
1045
  if (filters.isActive !== undefined) {
836
- constraints.push(where('isActive', '==', filters.isActive));
1046
+ constraints.push(where("isActive", "==", filters.isActive));
837
1047
  } else {
838
- constraints.push(where('isActive', '==', true));
1048
+ constraints.push(where("isActive", "==", true));
839
1049
  }
840
1050
 
841
1051
  // Filter constraints
842
1052
  if (filters.procedureFamily) {
843
- constraints.push(where('family', '==', filters.procedureFamily));
1053
+ constraints.push(where("family", "==", filters.procedureFamily));
844
1054
  }
845
1055
  if (filters.procedureCategory) {
846
- constraints.push(where('category.id', '==', filters.procedureCategory));
1056
+ constraints.push(
1057
+ where("category.id", "==", filters.procedureCategory)
1058
+ );
847
1059
  }
848
1060
  if (filters.procedureSubcategory) {
849
- constraints.push(where('subcategory.id', '==', filters.procedureSubcategory));
1061
+ constraints.push(
1062
+ where("subcategory.id", "==", filters.procedureSubcategory)
1063
+ );
850
1064
  }
851
1065
  if (filters.procedureTechnology) {
852
- constraints.push(where('technology.id', '==', filters.procedureTechnology));
1066
+ constraints.push(
1067
+ where("technology.id", "==", filters.procedureTechnology)
1068
+ );
853
1069
  }
854
1070
  if (filters.minPrice !== undefined) {
855
- constraints.push(where('price', '>=', filters.minPrice));
1071
+ constraints.push(where("price", ">=", filters.minPrice));
856
1072
  }
857
1073
  if (filters.maxPrice !== undefined) {
858
- constraints.push(where('price', '<=', filters.maxPrice));
1074
+ constraints.push(where("price", "<=", filters.maxPrice));
859
1075
  }
860
1076
  if (filters.minRating !== undefined) {
861
- constraints.push(where('reviewInfo.averageRating', '>=', filters.minRating));
1077
+ constraints.push(
1078
+ where("reviewInfo.averageRating", ">=", filters.minRating)
1079
+ );
862
1080
  }
863
1081
  if (filters.maxRating !== undefined) {
864
- constraints.push(where('reviewInfo.averageRating', '<=', filters.maxRating));
1082
+ constraints.push(
1083
+ where("reviewInfo.averageRating", "<=", filters.maxRating)
1084
+ );
865
1085
  }
866
1086
  if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
867
- const benefitsToMatch = filters.treatmentBenefits;
868
- constraints.push(where('treatmentBenefits', 'array-contains-any', benefitsToMatch));
1087
+ const benefitIdsToMatch = filters.treatmentBenefits;
1088
+ constraints.push(
1089
+ where(
1090
+ "treatmentBenefitIds",
1091
+ "array-contains-any",
1092
+ benefitIdsToMatch
1093
+ )
1094
+ );
869
1095
  }
870
1096
 
871
1097
  return constraints;
@@ -874,15 +1100,17 @@ export class ProcedureService extends BaseService {
874
1100
  // Strategy 1: Try nameLower search if nameSearch exists
875
1101
  if (filters.nameSearch && filters.nameSearch.trim()) {
876
1102
  try {
877
- console.log('[PROCEDURE_SERVICE] Strategy 1: Trying nameLower search');
1103
+ console.log(
1104
+ "[PROCEDURE_SERVICE] Strategy 1: Trying nameLower search"
1105
+ );
878
1106
  const searchTerm = filters.nameSearch.trim().toLowerCase();
879
1107
  const constraints = getBaseConstraints();
880
- constraints.push(where('nameLower', '>=', searchTerm));
881
- constraints.push(where('nameLower', '<=', searchTerm + '\uf8ff'));
882
- constraints.push(orderBy('nameLower'));
1108
+ constraints.push(where("nameLower", ">=", searchTerm));
1109
+ constraints.push(where("nameLower", "<=", searchTerm + "\uf8ff"));
1110
+ constraints.push(orderBy("nameLower"));
883
1111
 
884
1112
  if (filters.lastDoc) {
885
- if (typeof filters.lastDoc.data === 'function') {
1113
+ if (typeof filters.lastDoc.data === "function") {
886
1114
  constraints.push(startAfter(filters.lastDoc));
887
1115
  } else if (Array.isArray(filters.lastDoc)) {
888
1116
  constraints.push(startAfter(...filters.lastDoc));
@@ -892,17 +1120,22 @@ export class ProcedureService extends BaseService {
892
1120
  }
893
1121
  constraints.push(limit(filters.pagination || 10));
894
1122
 
895
- const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1123
+ const q = query(
1124
+ collection(this.db, PROCEDURES_COLLECTION),
1125
+ ...constraints
1126
+ );
896
1127
  const querySnapshot = await getDocs(q);
897
1128
  const procedures = querySnapshot.docs.map(
898
- (doc) => ({ ...doc.data(), id: doc.id } as Procedure),
1129
+ (doc) => ({ ...doc.data(), id: doc.id } as Procedure)
899
1130
  );
900
1131
  const lastDoc =
901
1132
  querySnapshot.docs.length > 0
902
1133
  ? querySnapshot.docs[querySnapshot.docs.length - 1]
903
1134
  : null;
904
1135
 
905
- console.log(`[PROCEDURE_SERVICE] Strategy 1 success: ${procedures.length} procedures`);
1136
+ console.log(
1137
+ `[PROCEDURE_SERVICE] Strategy 1 success: ${procedures.length} procedures`
1138
+ );
906
1139
 
907
1140
  // Fix Load More - ako je broj rezultata manji od pagination, nema više
908
1141
  if (procedures.length < (filters.pagination || 10)) {
@@ -910,22 +1143,24 @@ export class ProcedureService extends BaseService {
910
1143
  }
911
1144
  return { procedures, lastDoc };
912
1145
  } catch (error) {
913
- console.log('[PROCEDURE_SERVICE] Strategy 1 failed:', error);
1146
+ console.log("[PROCEDURE_SERVICE] Strategy 1 failed:", error);
914
1147
  }
915
1148
  }
916
1149
 
917
1150
  // Strategy 2: Try name field search as fallback
918
1151
  if (filters.nameSearch && filters.nameSearch.trim()) {
919
1152
  try {
920
- console.log('[PROCEDURE_SERVICE] Strategy 2: Trying name field search');
1153
+ console.log(
1154
+ "[PROCEDURE_SERVICE] Strategy 2: Trying name field search"
1155
+ );
921
1156
  const searchTerm = filters.nameSearch.trim().toLowerCase();
922
1157
  const constraints = getBaseConstraints();
923
- constraints.push(where('name', '>=', searchTerm));
924
- constraints.push(where('name', '<=', searchTerm + '\uf8ff'));
925
- constraints.push(orderBy('name'));
1158
+ constraints.push(where("name", ">=", searchTerm));
1159
+ constraints.push(where("name", "<=", searchTerm + "\uf8ff"));
1160
+ constraints.push(orderBy("name"));
926
1161
 
927
1162
  if (filters.lastDoc) {
928
- if (typeof filters.lastDoc.data === 'function') {
1163
+ if (typeof filters.lastDoc.data === "function") {
929
1164
  constraints.push(startAfter(filters.lastDoc));
930
1165
  } else if (Array.isArray(filters.lastDoc)) {
931
1166
  constraints.push(startAfter(...filters.lastDoc));
@@ -935,17 +1170,22 @@ export class ProcedureService extends BaseService {
935
1170
  }
936
1171
  constraints.push(limit(filters.pagination || 10));
937
1172
 
938
- const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1173
+ const q = query(
1174
+ collection(this.db, PROCEDURES_COLLECTION),
1175
+ ...constraints
1176
+ );
939
1177
  const querySnapshot = await getDocs(q);
940
1178
  const procedures = querySnapshot.docs.map(
941
- (doc) => ({ ...doc.data(), id: doc.id } as Procedure),
1179
+ (doc) => ({ ...doc.data(), id: doc.id } as Procedure)
942
1180
  );
943
1181
  const lastDoc =
944
1182
  querySnapshot.docs.length > 0
945
1183
  ? querySnapshot.docs[querySnapshot.docs.length - 1]
946
1184
  : null;
947
1185
 
948
- console.log(`[PROCEDURE_SERVICE] Strategy 2 success: ${procedures.length} procedures`);
1186
+ console.log(
1187
+ `[PROCEDURE_SERVICE] Strategy 2 success: ${procedures.length} procedures`
1188
+ );
949
1189
 
950
1190
  // Fix Load More - ako je broj rezultata manji od pagination, nema više
951
1191
  if (procedures.length < (filters.pagination || 10)) {
@@ -953,20 +1193,20 @@ export class ProcedureService extends BaseService {
953
1193
  }
954
1194
  return { procedures, lastDoc };
955
1195
  } catch (error) {
956
- console.log('[PROCEDURE_SERVICE] Strategy 2 failed:', error);
1196
+ console.log("[PROCEDURE_SERVICE] Strategy 2 failed:", error);
957
1197
  }
958
1198
  }
959
1199
 
960
1200
  // Strategy 3: orderBy createdAt with client-side filtering
961
1201
  try {
962
1202
  console.log(
963
- '[PROCEDURE_SERVICE] Strategy 3: Using createdAt orderBy with client-side filtering',
1203
+ "[PROCEDURE_SERVICE] Strategy 3: Using createdAt orderBy with client-side filtering"
964
1204
  );
965
1205
  const constraints = getBaseConstraints();
966
- constraints.push(orderBy('createdAt', 'desc'));
1206
+ constraints.push(orderBy("createdAt", "desc"));
967
1207
 
968
1208
  if (filters.lastDoc) {
969
- if (typeof filters.lastDoc.data === 'function') {
1209
+ if (typeof filters.lastDoc.data === "function") {
970
1210
  constraints.push(startAfter(filters.lastDoc));
971
1211
  } else if (Array.isArray(filters.lastDoc)) {
972
1212
  constraints.push(startAfter(...filters.lastDoc));
@@ -976,18 +1216,25 @@ export class ProcedureService extends BaseService {
976
1216
  }
977
1217
  constraints.push(limit(filters.pagination || 10));
978
1218
 
979
- const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1219
+ const q = query(
1220
+ collection(this.db, PROCEDURES_COLLECTION),
1221
+ ...constraints
1222
+ );
980
1223
  const querySnapshot = await getDocs(q);
981
1224
  let procedures = querySnapshot.docs.map(
982
- (doc) => ({ ...doc.data(), id: doc.id } as Procedure),
1225
+ (doc) => ({ ...doc.data(), id: doc.id } as Procedure)
983
1226
  );
984
1227
 
985
1228
  // Apply all client-side filters using centralized function
986
1229
  procedures = this.applyInMemoryFilters(procedures, filters);
987
1230
 
988
1231
  const lastDoc =
989
- querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
990
- console.log(`[PROCEDURE_SERVICE] Strategy 3 success: ${procedures.length} procedures`);
1232
+ querySnapshot.docs.length > 0
1233
+ ? querySnapshot.docs[querySnapshot.docs.length - 1]
1234
+ : null;
1235
+ console.log(
1236
+ `[PROCEDURE_SERVICE] Strategy 3 success: ${procedures.length} procedures`
1237
+ );
991
1238
 
992
1239
  // Fix Load More - ako je broj rezultata manji od pagination, nema više
993
1240
  if (procedures.length < (filters.pagination || 10)) {
@@ -995,30 +1242,37 @@ export class ProcedureService extends BaseService {
995
1242
  }
996
1243
  return { procedures, lastDoc };
997
1244
  } catch (error) {
998
- console.log('[PROCEDURE_SERVICE] Strategy 3 failed:', error);
1245
+ console.log("[PROCEDURE_SERVICE] Strategy 3 failed:", error);
999
1246
  }
1000
1247
 
1001
1248
  // Strategy 4: Minimal query fallback
1002
1249
  try {
1003
- console.log('[PROCEDURE_SERVICE] Strategy 4: Minimal query fallback');
1250
+ console.log("[PROCEDURE_SERVICE] Strategy 4: Minimal query fallback");
1004
1251
  const constraints: QueryConstraint[] = [
1005
- where('isActive', '==', true),
1006
- orderBy('createdAt', 'desc'),
1252
+ where("isActive", "==", true),
1253
+ orderBy("createdAt", "desc"),
1007
1254
  limit(filters.pagination || 10),
1008
1255
  ];
1009
1256
 
1010
- const q = query(collection(this.db, PROCEDURES_COLLECTION), ...constraints);
1257
+ const q = query(
1258
+ collection(this.db, PROCEDURES_COLLECTION),
1259
+ ...constraints
1260
+ );
1011
1261
  const querySnapshot = await getDocs(q);
1012
1262
  let procedures = querySnapshot.docs.map(
1013
- (doc) => ({ ...doc.data(), id: doc.id } as Procedure),
1263
+ (doc) => ({ ...doc.data(), id: doc.id } as Procedure)
1014
1264
  );
1015
1265
 
1016
1266
  // Apply all client-side filters using centralized function
1017
1267
  procedures = this.applyInMemoryFilters(procedures, filters);
1018
1268
 
1019
1269
  const lastDoc =
1020
- querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
1021
- console.log(`[PROCEDURE_SERVICE] Strategy 4 success: ${procedures.length} procedures`);
1270
+ querySnapshot.docs.length > 0
1271
+ ? querySnapshot.docs[querySnapshot.docs.length - 1]
1272
+ : null;
1273
+ console.log(
1274
+ `[PROCEDURE_SERVICE] Strategy 4 success: ${procedures.length} procedures`
1275
+ );
1022
1276
 
1023
1277
  // Fix Load More - ako je broj rezultata manji od pagination, nema više
1024
1278
  if (procedures.length < (filters.pagination || 10)) {
@@ -1026,14 +1280,16 @@ export class ProcedureService extends BaseService {
1026
1280
  }
1027
1281
  return { procedures, lastDoc };
1028
1282
  } catch (error) {
1029
- console.log('[PROCEDURE_SERVICE] Strategy 4 failed:', error);
1283
+ console.log("[PROCEDURE_SERVICE] Strategy 4 failed:", error);
1030
1284
  }
1031
1285
 
1032
1286
  // All strategies failed
1033
- console.log('[PROCEDURE_SERVICE] All strategies failed, returning empty result');
1287
+ console.log(
1288
+ "[PROCEDURE_SERVICE] All strategies failed, returning empty result"
1289
+ );
1034
1290
  return { procedures: [], lastDoc: null };
1035
1291
  } catch (error) {
1036
- console.error('[PROCEDURE_SERVICE] Error filtering procedures:', error);
1292
+ console.error("[PROCEDURE_SERVICE] Error filtering procedures:", error);
1037
1293
  return { procedures: [], lastDoc: null };
1038
1294
  }
1039
1295
  }
@@ -1044,7 +1300,7 @@ export class ProcedureService extends BaseService {
1044
1300
  */
1045
1301
  private applyInMemoryFilters(
1046
1302
  procedures: Procedure[],
1047
- filters: any,
1303
+ filters: any
1048
1304
  ): (Procedure & { distance?: number })[] {
1049
1305
  let filteredProcedures = [...procedures]; // Create copy to avoid mutating original
1050
1306
 
@@ -1052,23 +1308,27 @@ export class ProcedureService extends BaseService {
1052
1308
  if (filters.nameSearch && filters.nameSearch.trim()) {
1053
1309
  const searchTerm = filters.nameSearch.trim().toLowerCase();
1054
1310
  filteredProcedures = filteredProcedures.filter((procedure) => {
1055
- const name = (procedure.name || '').toLowerCase();
1056
- const nameLower = procedure.nameLower || '';
1311
+ const name = (procedure.name || "").toLowerCase();
1312
+ const nameLower = procedure.nameLower || "";
1057
1313
  return name.includes(searchTerm) || nameLower.includes(searchTerm);
1058
1314
  });
1059
- console.log(`[PROCEDURE_SERVICE] Applied name filter, results: ${filteredProcedures.length}`);
1315
+ console.log(
1316
+ `[PROCEDURE_SERVICE] Applied name filter, results: ${filteredProcedures.length}`
1317
+ );
1060
1318
  }
1061
1319
 
1062
1320
  // Price filtering
1063
1321
  if (filters.minPrice !== undefined || filters.maxPrice !== undefined) {
1064
1322
  filteredProcedures = filteredProcedures.filter((procedure) => {
1065
1323
  const price = procedure.price || 0;
1066
- if (filters.minPrice !== undefined && price < filters.minPrice) return false;
1067
- if (filters.maxPrice !== undefined && price > filters.maxPrice) return false;
1324
+ if (filters.minPrice !== undefined && price < filters.minPrice)
1325
+ return false;
1326
+ if (filters.maxPrice !== undefined && price > filters.maxPrice)
1327
+ return false;
1068
1328
  return true;
1069
1329
  });
1070
1330
  console.log(
1071
- `[PROCEDURE_SERVICE] Applied price filter (${filters.minPrice}-${filters.maxPrice}), results: ${filteredProcedures.length}`,
1331
+ `[PROCEDURE_SERVICE] Applied price filter (${filters.minPrice}-${filters.maxPrice}), results: ${filteredProcedures.length}`
1072
1332
  );
1073
1333
  }
1074
1334
 
@@ -1076,64 +1336,69 @@ export class ProcedureService extends BaseService {
1076
1336
  if (filters.minRating !== undefined || filters.maxRating !== undefined) {
1077
1337
  filteredProcedures = filteredProcedures.filter((procedure) => {
1078
1338
  const rating = procedure.reviewInfo?.averageRating || 0;
1079
- if (filters.minRating !== undefined && rating < filters.minRating) return false;
1080
- if (filters.maxRating !== undefined && rating > filters.maxRating) return false;
1339
+ if (filters.minRating !== undefined && rating < filters.minRating)
1340
+ return false;
1341
+ if (filters.maxRating !== undefined && rating > filters.maxRating)
1342
+ return false;
1081
1343
  return true;
1082
1344
  });
1083
1345
  console.log(
1084
- `[PROCEDURE_SERVICE] Applied rating filter, results: ${filteredProcedures.length}`,
1346
+ `[PROCEDURE_SERVICE] Applied rating filter, results: ${filteredProcedures.length}`
1085
1347
  );
1086
1348
  }
1087
1349
 
1088
1350
  // Treatment benefits filtering
1089
1351
  if (filters.treatmentBenefits && filters.treatmentBenefits.length > 0) {
1090
- const benefitsToMatch = filters.treatmentBenefits;
1352
+ const benefitIdsToMatch = filters.treatmentBenefits;
1091
1353
  filteredProcedures = filteredProcedures.filter((procedure) => {
1092
- const procedureBenefits = procedure.treatmentBenefits || [];
1093
- return benefitsToMatch.some((benefit: any) => procedureBenefits.includes(benefit));
1354
+ const procedureBenefitIds = procedure.treatmentBenefitIds || [];
1355
+ return benefitIdsToMatch.some((benefitId: string) =>
1356
+ procedureBenefitIds.includes(benefitId)
1357
+ );
1094
1358
  });
1095
1359
  console.log(
1096
- `[PROCEDURE_SERVICE] Applied benefits filter, results: ${filteredProcedures.length}`,
1360
+ `[PROCEDURE_SERVICE] Applied benefits filter, results: ${filteredProcedures.length}`
1097
1361
  );
1098
1362
  }
1099
1363
 
1100
1364
  // Procedure family filtering
1101
1365
  if (filters.procedureFamily) {
1102
1366
  filteredProcedures = filteredProcedures.filter(
1103
- (procedure) => procedure.family === filters.procedureFamily,
1367
+ (procedure) => procedure.family === filters.procedureFamily
1104
1368
  );
1105
1369
  console.log(
1106
- `[PROCEDURE_SERVICE] Applied family filter, results: ${filteredProcedures.length}`,
1370
+ `[PROCEDURE_SERVICE] Applied family filter, results: ${filteredProcedures.length}`
1107
1371
  );
1108
1372
  }
1109
1373
 
1110
1374
  // Category filtering
1111
1375
  if (filters.procedureCategory) {
1112
1376
  filteredProcedures = filteredProcedures.filter(
1113
- (procedure) => procedure.category?.id === filters.procedureCategory,
1377
+ (procedure) => procedure.category?.id === filters.procedureCategory
1114
1378
  );
1115
1379
  console.log(
1116
- `[PROCEDURE_SERVICE] Applied category filter, results: ${filteredProcedures.length}`,
1380
+ `[PROCEDURE_SERVICE] Applied category filter, results: ${filteredProcedures.length}`
1117
1381
  );
1118
1382
  }
1119
1383
 
1120
1384
  // Subcategory filtering
1121
1385
  if (filters.procedureSubcategory) {
1122
1386
  filteredProcedures = filteredProcedures.filter(
1123
- (procedure) => procedure.subcategory?.id === filters.procedureSubcategory,
1387
+ (procedure) =>
1388
+ procedure.subcategory?.id === filters.procedureSubcategory
1124
1389
  );
1125
1390
  console.log(
1126
- `[PROCEDURE_SERVICE] Applied subcategory filter, results: ${filteredProcedures.length}`,
1391
+ `[PROCEDURE_SERVICE] Applied subcategory filter, results: ${filteredProcedures.length}`
1127
1392
  );
1128
1393
  }
1129
1394
 
1130
1395
  // Technology filtering
1131
1396
  if (filters.procedureTechnology) {
1132
1397
  filteredProcedures = filteredProcedures.filter(
1133
- (procedure) => procedure.technology?.id === filters.procedureTechnology,
1398
+ (procedure) => procedure.technology?.id === filters.procedureTechnology
1134
1399
  );
1135
1400
  console.log(
1136
- `[PROCEDURE_SERVICE] Applied technology filter, results: ${filteredProcedures.length}`,
1401
+ `[PROCEDURE_SERVICE] Applied technology filter, results: ${filteredProcedures.length}`
1137
1402
  );
1138
1403
  }
1139
1404
 
@@ -1150,7 +1415,7 @@ export class ProcedureService extends BaseService {
1150
1415
  const distance =
1151
1416
  distanceBetween(
1152
1417
  [location.latitude, location.longitude],
1153
- [clinicLocation.latitude, clinicLocation.longitude],
1418
+ [clinicLocation.latitude, clinicLocation.longitude]
1154
1419
  ) / 1000; // Convert to km
1155
1420
 
1156
1421
  // Attach distance for frontend sorting/display
@@ -1158,19 +1423,24 @@ export class ProcedureService extends BaseService {
1158
1423
 
1159
1424
  return distance <= radiusInKm;
1160
1425
  });
1161
- console.log(`[PROCEDURE_SERVICE] Applied geo filter, results: ${filteredProcedures.length}`);
1426
+ console.log(
1427
+ `[PROCEDURE_SERVICE] Applied geo filter, results: ${filteredProcedures.length}`
1428
+ );
1162
1429
 
1163
1430
  // Sort by distance when geo filtering is applied
1164
- filteredProcedures.sort((a, b) => ((a as any).distance || 0) - ((b as any).distance || 0));
1431
+ filteredProcedures.sort(
1432
+ (a, b) => ((a as any).distance || 0) - ((b as any).distance || 0)
1433
+ );
1165
1434
  }
1166
1435
 
1167
1436
  return filteredProcedures as (Procedure & { distance?: number })[];
1168
1437
  }
1169
1438
 
1170
- private handleGeoQuery(
1171
- filters: any,
1172
- ): Promise<{ procedures: (Procedure & { distance?: number })[]; lastDoc: any }> {
1173
- console.log('[PROCEDURE_SERVICE] Executing geo query with geohash bounds');
1439
+ private handleGeoQuery(filters: any): Promise<{
1440
+ procedures: (Procedure & { distance?: number })[];
1441
+ lastDoc: any;
1442
+ }> {
1443
+ console.log("[PROCEDURE_SERVICE] Executing geo query with geohash bounds");
1174
1444
  try {
1175
1445
  const location = filters.location;
1176
1446
  const radiusInKm = filters.radiusInKm;
@@ -1179,22 +1449,33 @@ export class ProcedureService extends BaseService {
1179
1449
  return Promise.resolve({ procedures: [], lastDoc: null });
1180
1450
  }
1181
1451
 
1182
- const bounds = geohashQueryBounds([location.latitude, location.longitude], radiusInKm * 1000);
1452
+ const bounds = geohashQueryBounds(
1453
+ [location.latitude, location.longitude],
1454
+ radiusInKm * 1000
1455
+ );
1183
1456
 
1184
1457
  const fetches = bounds.map((b) => {
1185
1458
  const constraints: QueryConstraint[] = [
1186
- where('clinicInfo.location.geohash', '>=', b[0]),
1187
- where('clinicInfo.location.geohash', '<=', b[1]),
1188
- where('isActive', '==', filters.isActive !== undefined ? filters.isActive : true),
1459
+ where("clinicInfo.location.geohash", ">=", b[0]),
1460
+ where("clinicInfo.location.geohash", "<=", b[1]),
1461
+ where(
1462
+ "isActive",
1463
+ "==",
1464
+ filters.isActive !== undefined ? filters.isActive : true
1465
+ ),
1189
1466
  ];
1190
- return getDocs(query(collection(this.db, PROCEDURES_COLLECTION), ...constraints));
1467
+ return getDocs(
1468
+ query(collection(this.db, PROCEDURES_COLLECTION), ...constraints)
1469
+ );
1191
1470
  });
1192
1471
 
1193
1472
  return Promise.all(fetches)
1194
1473
  .then((snaps) => {
1195
1474
  const collected: Procedure[] = [];
1196
1475
  snaps.forEach((snap) => {
1197
- snap.docs.forEach((d) => collected.push({ ...(d.data() as Procedure), id: d.id }));
1476
+ snap.docs.forEach((d) =>
1477
+ collected.push({ ...(d.data() as Procedure), id: d.id })
1478
+ );
1198
1479
  });
1199
1480
 
1200
1481
  // Deduplicate by id
@@ -1212,26 +1493,29 @@ export class ProcedureService extends BaseService {
1212
1493
  let startIndex = 0;
1213
1494
  if (
1214
1495
  filters.lastDoc &&
1215
- typeof filters.lastDoc === 'object' &&
1496
+ typeof filters.lastDoc === "object" &&
1216
1497
  (filters.lastDoc as any).id
1217
1498
  ) {
1218
- const idx = procedures.findIndex((p) => p.id === (filters.lastDoc as any).id);
1499
+ const idx = procedures.findIndex(
1500
+ (p) => p.id === (filters.lastDoc as any).id
1501
+ );
1219
1502
  if (idx >= 0) startIndex = idx + 1;
1220
1503
  }
1221
1504
  const page = procedures.slice(startIndex, startIndex + pageSize);
1222
- const newLastDoc = page.length === pageSize ? page[page.length - 1] : null;
1505
+ const newLastDoc =
1506
+ page.length === pageSize ? page[page.length - 1] : null;
1223
1507
 
1224
1508
  console.log(
1225
- `[PROCEDURE_SERVICE] Geo query success: ${page.length} (of ${procedures.length}) within ${radiusInKm}km`,
1509
+ `[PROCEDURE_SERVICE] Geo query success: ${page.length} (of ${procedures.length}) within ${radiusInKm}km`
1226
1510
  );
1227
1511
  return { procedures: page, lastDoc: newLastDoc };
1228
1512
  })
1229
1513
  .catch((err) => {
1230
- console.error('[PROCEDURE_SERVICE] Geo bounds fetch failed:', err);
1514
+ console.error("[PROCEDURE_SERVICE] Geo bounds fetch failed:", err);
1231
1515
  return { procedures: [], lastDoc: null };
1232
1516
  });
1233
1517
  } catch (error) {
1234
- console.error('[PROCEDURE_SERVICE] Geo query failed:', error);
1518
+ console.error("[PROCEDURE_SERVICE] Geo query failed:", error);
1235
1519
  return Promise.resolve({ procedures: [], lastDoc: null });
1236
1520
  }
1237
1521
  }
@@ -1243,7 +1527,7 @@ export class ProcedureService extends BaseService {
1243
1527
  * @returns The created procedure
1244
1528
  */
1245
1529
  async createConsultationProcedure(
1246
- data: Omit<CreateProcedureData, 'productId'>,
1530
+ data: Omit<CreateProcedureData, "productId">
1247
1531
  ): Promise<Procedure> {
1248
1532
  // Generate procedure ID first so we can use it for media uploads
1249
1533
  const procedureId = this.generateId();
@@ -1257,7 +1541,7 @@ export class ProcedureService extends BaseService {
1257
1541
  ]);
1258
1542
 
1259
1543
  if (!category || !subcategory || !technology) {
1260
- throw new Error('One or more required base entities not found');
1544
+ throw new Error("One or more required base entities not found");
1261
1545
  }
1262
1546
 
1263
1547
  // Get clinic and practitioner information for aggregation
@@ -1268,7 +1552,11 @@ export class ProcedureService extends BaseService {
1268
1552
  }
1269
1553
  const clinic = clinicSnapshot.data() as Clinic;
1270
1554
 
1271
- const practitionerRef = doc(this.db, PRACTITIONERS_COLLECTION, data.practitionerId);
1555
+ const practitionerRef = doc(
1556
+ this.db,
1557
+ PRACTITIONERS_COLLECTION,
1558
+ data.practitionerId
1559
+ );
1272
1560
  const practitionerSnapshot = await getDoc(practitionerRef);
1273
1561
  if (!practitionerSnapshot.exists()) {
1274
1562
  throw new Error(`Practitioner with ID ${data.practitionerId} not found`);
@@ -1278,22 +1566,32 @@ export class ProcedureService extends BaseService {
1278
1566
  // Process photos if provided
1279
1567
  let processedPhotos: string[] = [];
1280
1568
  if (data.photos && data.photos.length > 0) {
1281
- processedPhotos = await this.processMediaArray(data.photos, procedureId, 'procedure-photos');
1569
+ processedPhotos = await this.processMediaArray(
1570
+ data.photos,
1571
+ procedureId,
1572
+ "procedure-photos"
1573
+ );
1282
1574
  }
1283
1575
 
1576
+ // Transform productsMetadata from validation format to ProcedureProduct format
1577
+ const transformedProductsMetadata = await this.transformProductsMetadata(
1578
+ data.productsMetadata,
1579
+ data.technologyId
1580
+ );
1581
+
1284
1582
  // Create aggregated clinic info for the procedure document
1285
1583
  const clinicInfo = {
1286
1584
  id: clinicSnapshot.id,
1287
1585
  name: clinic.name,
1288
- description: clinic.description || '',
1586
+ description: clinic.description || "",
1289
1587
  featuredPhoto:
1290
1588
  clinic.featuredPhotos && clinic.featuredPhotos.length > 0
1291
- ? typeof clinic.featuredPhotos[0] === 'string'
1589
+ ? typeof clinic.featuredPhotos[0] === "string"
1292
1590
  ? clinic.featuredPhotos[0]
1293
- : ''
1294
- : typeof clinic.coverPhoto === 'string'
1591
+ : ""
1592
+ : typeof clinic.coverPhoto === "string"
1295
1593
  ? clinic.coverPhoto
1296
- : '',
1594
+ : "",
1297
1595
  location: clinic.location,
1298
1596
  contactInfo: clinic.contactInfo,
1299
1597
  };
@@ -1302,31 +1600,33 @@ export class ProcedureService extends BaseService {
1302
1600
  const doctorInfo = {
1303
1601
  id: practitionerSnapshot.id,
1304
1602
  name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
1305
- description: practitioner.basicInfo.bio || '',
1603
+ description: practitioner.basicInfo.bio || "",
1306
1604
  photo:
1307
- typeof practitioner.basicInfo.profileImageUrl === 'string'
1605
+ typeof practitioner.basicInfo.profileImageUrl === "string"
1308
1606
  ? practitioner.basicInfo.profileImageUrl
1309
- : '',
1607
+ : "",
1310
1608
  rating: practitioner.reviewInfo?.averageRating || 0,
1311
1609
  services: practitioner.procedures || [],
1312
1610
  };
1313
1611
 
1314
1612
  // Create a placeholder product for consultation procedures
1315
1613
  const consultationProduct: Product = {
1316
- id: 'consultation-no-product',
1317
- name: 'No Product Required',
1318
- description: 'Consultation procedures do not require specific products',
1319
- brandId: 'consultation-brand',
1320
- brandName: 'Consultation',
1614
+ id: "consultation-no-product",
1615
+ name: "No Product Required",
1616
+ description: "Consultation procedures do not require specific products",
1617
+ brandId: "consultation-brand",
1618
+ brandName: "Consultation",
1321
1619
  technologyId: data.technologyId,
1322
1620
  technologyName: technology.name,
1621
+ categoryId: technology.categoryId,
1622
+ subcategoryId: technology.subcategoryId,
1323
1623
  isActive: true,
1324
1624
  createdAt: new Date(),
1325
1625
  updatedAt: new Date(),
1326
1626
  };
1327
1627
 
1328
1628
  // Create the procedure object
1329
- const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
1629
+ const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
1330
1630
  id: procedureId,
1331
1631
  ...data,
1332
1632
  nameLower: (data as any).nameLower || data.name.toLowerCase(),
@@ -1335,9 +1635,12 @@ export class ProcedureService extends BaseService {
1335
1635
  subcategory,
1336
1636
  technology,
1337
1637
  product: consultationProduct, // Use placeholder product
1638
+ productsMetadata: transformedProductsMetadata,
1338
1639
  blockingConditions: technology.blockingConditions,
1339
1640
  contraindications: technology.contraindications || [],
1641
+ contraindicationIds: technology.contraindications?.map((c) => c.id) || [],
1340
1642
  treatmentBenefits: technology.benefits,
1643
+ treatmentBenefitIds: technology.benefits?.map((b) => b.id) || [],
1341
1644
  preRequirements: technology.requirements.pre,
1342
1645
  postRequirements: technology.requirements.post,
1343
1646
  certificationRequirement: technology.certificationRequirement,
@@ -1395,7 +1698,7 @@ export class ProcedureService extends BaseService {
1395
1698
  name: data.name,
1396
1699
  clinicId: data.clinicInfo?.id,
1397
1700
  clinicName: data.clinicInfo?.name,
1398
- address: data.clinicInfo?.location?.address || '',
1701
+ address: data.clinicInfo?.location?.address || "",
1399
1702
  latitude: data.clinicInfo?.location?.latitude,
1400
1703
  longitude: data.clinicInfo?.location?.longitude,
1401
1704
  };