@blackcode_sa/metaestetics-api 1.5.28 → 1.5.29

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 (34) hide show
  1. package/dist/admin/index.d.mts +1199 -1
  2. package/dist/admin/index.d.ts +1199 -1
  3. package/dist/admin/index.js +1337 -2
  4. package/dist/admin/index.mjs +1333 -2
  5. package/dist/backoffice/index.d.mts +99 -7
  6. package/dist/backoffice/index.d.ts +99 -7
  7. package/dist/index.d.mts +4035 -2364
  8. package/dist/index.d.ts +4035 -2364
  9. package/dist/index.js +2616 -1929
  10. package/dist/index.mjs +2646 -1952
  11. package/package.json +1 -1
  12. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +642 -0
  13. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -0
  14. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -0
  15. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +508 -0
  16. package/src/admin/index.ts +53 -4
  17. package/src/index.ts +28 -4
  18. package/src/services/clinic/clinic.service.ts +320 -107
  19. package/src/services/clinic/utils/clinic.utils.ts +66 -117
  20. package/src/services/clinic/utils/filter.utils.d.ts +23 -0
  21. package/src/services/clinic/utils/filter.utils.ts +264 -0
  22. package/src/services/practitioner/practitioner.service.ts +616 -5
  23. package/src/services/procedure/procedure.service.ts +599 -352
  24. package/src/services/reviews/reviews.service.ts +842 -0
  25. package/src/types/clinic/index.ts +24 -56
  26. package/src/types/practitioner/index.ts +34 -33
  27. package/src/types/procedure/index.ts +32 -0
  28. package/src/types/profile/index.ts +1 -1
  29. package/src/types/reviews/index.ts +126 -0
  30. package/src/validations/clinic.schema.ts +37 -64
  31. package/src/validations/practitioner.schema.ts +42 -32
  32. package/src/validations/procedure.schema.ts +11 -3
  33. package/src/validations/reviews.schema.ts +189 -0
  34. package/src/services/clinic/utils/review.utils.ts +0 -93
@@ -12,6 +12,10 @@ import {
12
12
  serverTimestamp,
13
13
  DocumentData,
14
14
  writeBatch,
15
+ arrayUnion,
16
+ arrayRemove,
17
+ FieldValue,
18
+ orderBy,
15
19
  } from "firebase/firestore";
16
20
  import { BaseService } from "../base.service";
17
21
  import {
@@ -19,6 +23,7 @@ import {
19
23
  CreateProcedureData,
20
24
  UpdateProcedureData,
21
25
  PROCEDURES_COLLECTION,
26
+ ProcedureSummaryInfo,
22
27
  } from "../../types/procedure";
23
28
  import {
24
29
  createProcedureSchema,
@@ -57,7 +62,8 @@ import {
57
62
  CertificationSpecialty,
58
63
  ProcedureFamily,
59
64
  } from "../../backoffice/types";
60
- import { CLINICS_COLLECTION } from "../../types/clinic";
65
+ import { Clinic, CLINICS_COLLECTION } from "../../types/clinic";
66
+ import { ProcedureReviewInfo } from "../../types/reviews";
61
67
 
62
68
  export class ProcedureService extends BaseService {
63
69
  private categoryService: CategoryService;
@@ -81,16 +87,150 @@ export class ProcedureService extends BaseService {
81
87
  this.productService = productService;
82
88
  }
83
89
 
90
+ // Helper function to create ProcedureSummaryInfo
91
+ private _createProcedureSummaryInfo(
92
+ procedure: Procedure
93
+ ): ProcedureSummaryInfo {
94
+ // Ensure nested objects and names exist before accessing them
95
+ const categoryName = procedure.category?.name || "N/A";
96
+ const subcategoryName = procedure.subcategory?.name || "N/A";
97
+ const technologyName = procedure.technology?.name || "N/A";
98
+ const clinicName = procedure.clinicInfo?.name || "N/A";
99
+ const practitionerName = procedure.doctorInfo?.name || "N/A";
100
+
101
+ return {
102
+ id: procedure.id,
103
+ name: procedure.name,
104
+ description: procedure.description || "",
105
+ photo: "", // No photo source identified yet
106
+ family: procedure.family,
107
+ categoryName: categoryName,
108
+ subcategoryName: subcategoryName,
109
+ technologyName: technologyName,
110
+ price: procedure.price,
111
+ pricingMeasure: procedure.pricingMeasure,
112
+ currency: procedure.currency,
113
+ duration: procedure.duration,
114
+ clinicId: procedure.clinicBranchId,
115
+ clinicName: clinicName,
116
+ practitionerId: procedure.practitionerId,
117
+ practitionerName: practitionerName,
118
+ };
119
+ }
120
+
121
+ // Helper to update practitioner's procedures
122
+ private async _updatePractitionerProcedures(
123
+ practitionerId: string,
124
+ procedureSummary: ProcedureSummaryInfo | null, // Null means remove
125
+ procedureIdToRemove?: string
126
+ ) {
127
+ const practitionerRef = doc(
128
+ this.db,
129
+ PRACTITIONERS_COLLECTION,
130
+ practitionerId
131
+ );
132
+ const updateData: { [key: string]: FieldValue } = {};
133
+
134
+ if (procedureSummary) {
135
+ // Add/Update
136
+ updateData["procedures"] = arrayUnion(procedureSummary.id);
137
+ // Remove old summary first (by ID) then add new one to ensure update
138
+ updateData["proceduresInfo"] = arrayRemove(
139
+ ...[
140
+ // Need to spread the arrayRemove arguments
141
+ // Create a 'matcher' object with only the ID for removal
142
+ { id: procedureSummary.id },
143
+ ]
144
+ );
145
+ // We'll add the new summary in a separate update to avoid conflicts if needed, or rely on a subsequent transaction/batch commit
146
+ } else if (procedureIdToRemove) {
147
+ // Remove
148
+ updateData["procedures"] = arrayRemove(procedureIdToRemove);
149
+ updateData["proceduresInfo"] = arrayRemove(
150
+ ...[{ id: procedureIdToRemove }] // Use matcher for removal
151
+ );
152
+ } else {
153
+ return; // No operation needed
154
+ }
155
+
156
+ updateData["updatedAt"] = serverTimestamp();
157
+
158
+ // Perform the update
159
+ const batch = writeBatch(this.db);
160
+ batch.update(practitionerRef, updateData);
161
+
162
+ // If adding/updating, add the new summary info after removing the old one
163
+ if (procedureSummary) {
164
+ batch.update(practitionerRef, {
165
+ proceduresInfo: arrayUnion(procedureSummary),
166
+ });
167
+ }
168
+
169
+ await batch.commit();
170
+ }
171
+
172
+ // Helper to update clinic's procedures
173
+ private async _updateClinicProcedures(
174
+ clinicId: string,
175
+ procedureSummary: ProcedureSummaryInfo | null, // Null means remove
176
+ procedureIdToRemove?: string
177
+ ) {
178
+ const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
179
+
180
+ // Check if clinic exists before attempting update
181
+ const clinicSnap = await getDoc(clinicRef);
182
+ if (!clinicSnap.exists()) {
183
+ console.warn(
184
+ `Clinic ${clinicId} not found, skipping procedure aggregation update.`
185
+ );
186
+ return;
187
+ }
188
+
189
+ const updateData: { [key: string]: FieldValue } = {};
190
+
191
+ if (procedureSummary) {
192
+ // Add/Update - Using arrayUnion for simplicity, assuming procedure IDs are unique in services array too
193
+ // Note: 'services' array might be deprecated or used differently now
194
+ updateData["services"] = arrayUnion(procedureSummary.id);
195
+ // Remove old summary first (by ID) then add new one to ensure update
196
+ updateData["proceduresInfo"] = arrayRemove(
197
+ ...[{ id: procedureSummary.id }] // Use matcher for removal
198
+ );
199
+ } else if (procedureIdToRemove) {
200
+ // Remove
201
+ updateData["services"] = arrayRemove(procedureIdToRemove);
202
+ updateData["proceduresInfo"] = arrayRemove(
203
+ ...[{ id: procedureIdToRemove }] // Use matcher for removal
204
+ );
205
+ } else {
206
+ return; // No operation needed
207
+ }
208
+
209
+ updateData["updatedAt"] = serverTimestamp();
210
+
211
+ // Perform the update
212
+ const batch = writeBatch(this.db);
213
+ batch.update(clinicRef, updateData);
214
+
215
+ // If adding/updating, add the new summary info after removing the old one
216
+ if (procedureSummary) {
217
+ batch.update(clinicRef, {
218
+ proceduresInfo: arrayUnion(procedureSummary),
219
+ });
220
+ }
221
+
222
+ await batch.commit();
223
+ }
224
+
84
225
  /**
85
- * Creates a new procedure
226
+ * Creates a new procedure and updates related practitioner/clinic aggregates
86
227
  * @param data - The data for creating a new procedure
87
228
  * @returns The created procedure
88
229
  */
89
230
  async createProcedure(data: CreateProcedureData): Promise<Procedure> {
90
- // Validate input data
91
231
  const validatedData = createProcedureSchema.parse(data);
92
232
 
93
- // Get references to related entities
233
+ // Get references to related entities (Category, Subcategory, Technology, Product)
94
234
  const [category, subcategory, technology, product] = await Promise.all([
95
235
  this.categoryService.getById(validatedData.categoryId),
96
236
  this.subcategoryService.getById(
@@ -105,75 +245,66 @@ export class ProcedureService extends BaseService {
105
245
  ]);
106
246
 
107
247
  if (!category || !subcategory || !technology || !product) {
108
- throw new Error("One or more required entities not found");
248
+ throw new Error("One or more required base entities not found");
109
249
  }
110
250
 
111
- // Get clinic and practitioner information
251
+ // Get clinic and practitioner information for aggregation
112
252
  const clinicRef = doc(
113
253
  this.db,
114
254
  CLINICS_COLLECTION,
115
255
  validatedData.clinicBranchId
116
256
  );
117
257
  const clinicSnapshot = await getDoc(clinicRef);
118
-
119
258
  if (!clinicSnapshot.exists()) {
120
259
  throw new Error(
121
260
  `Clinic with ID ${validatedData.clinicBranchId} not found`
122
261
  );
123
262
  }
263
+ const clinic = clinicSnapshot.data() as Clinic; // Assert type
124
264
 
125
- const clinic = clinicSnapshot.data();
126
-
127
- // Create clinic info
128
- const clinicInfo = {
129
- id: clinicSnapshot.id,
130
- name: clinic.name,
131
- description: clinic.description || "",
132
- featuredPhoto:
133
- clinic.featuredPhotos && clinic.featuredPhotos.length > 0
134
- ? clinic.featuredPhotos[0]
135
- : clinic.coverPhoto || "",
136
- location: clinic.location,
137
- contactInfo: clinic.contactInfo,
138
- };
139
-
140
- // Get practitioner information
141
265
  const practitionerRef = doc(
142
266
  this.db,
143
267
  PRACTITIONERS_COLLECTION,
144
268
  validatedData.practitionerId
145
269
  );
146
270
  const practitionerSnapshot = await getDoc(practitionerRef);
147
-
148
271
  if (!practitionerSnapshot.exists()) {
149
272
  throw new Error(
150
273
  `Practitioner with ID ${validatedData.practitionerId} not found`
151
274
  );
152
275
  }
276
+ const practitioner = practitionerSnapshot.data() as Practitioner; // Assert type
153
277
 
154
- const practitioner = practitionerSnapshot.data();
155
-
156
- // Find doctor info in clinic's doctorsInfo array
157
- let doctorInfo = clinic.doctorsInfo?.find(
158
- (doctor: any) => doctor.id === validatedData.practitionerId
159
- );
278
+ // Create aggregated clinic info for the procedure document
279
+ const clinicInfo = {
280
+ id: clinicSnapshot.id,
281
+ name: clinic.name,
282
+ description: clinic.description || "",
283
+ featuredPhoto:
284
+ clinic.featuredPhotos && clinic.featuredPhotos.length > 0
285
+ ? clinic.featuredPhotos[0]
286
+ : clinic.coverPhoto || "",
287
+ location: clinic.location,
288
+ contactInfo: clinic.contactInfo,
289
+ };
160
290
 
161
- // If not found, create basic doctor info
162
- if (!doctorInfo) {
163
- doctorInfo = {
164
- id: practitionerSnapshot.id,
165
- name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
166
- description: practitioner.basicInfo.bio || "",
167
- photo: practitioner.basicInfo.profileImageUrl || "",
168
- rating: 0,
169
- services: [],
170
- };
171
- }
291
+ // Create aggregated doctor info for the procedure document
292
+ // Re-use logic from previous implementation or simplify
293
+ const doctorInfo = {
294
+ id: practitionerSnapshot.id,
295
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
296
+ description: practitioner.basicInfo.bio || "",
297
+ photo: practitioner.basicInfo.profileImageUrl || "",
298
+ rating: practitioner.reviewInfo?.averageRating || 0, // Example rating source
299
+ services: practitioner.procedures || [], // Link services to practitioner's procedures
300
+ };
172
301
 
173
302
  // Create the procedure object
174
- const procedure: Omit<Procedure, "id"> = {
303
+ const procedureId = this.generateId();
304
+ const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
305
+ id: procedureId,
175
306
  ...validatedData,
176
- category,
307
+ category, // Embed full objects
177
308
  subcategory,
178
309
  technology,
179
310
  product,
@@ -183,24 +314,67 @@ export class ProcedureService extends BaseService {
183
314
  postRequirements: technology.requirements.post,
184
315
  certificationRequirement: technology.certificationRequirement,
185
316
  documentationTemplates: technology.documentationTemplates || [],
186
- clinicInfo,
187
- doctorInfo,
188
- isActive: true,
189
- createdAt: new Date(),
190
- updatedAt: new Date(),
317
+ clinicInfo, // Embed aggregated info
318
+ doctorInfo, // Embed aggregated info
319
+ reviewInfo: {
320
+ // Default empty reviews
321
+ totalReviews: 0,
322
+ averageRating: 0,
323
+ effectivenessOfTreatment: 0,
324
+ outcomeExplanation: 0,
325
+ painManagement: 0,
326
+ followUpCare: 0,
327
+ valueForMoney: 0,
328
+ recommendationPercentage: 0,
329
+ },
330
+ isActive: true, // Default to active
191
331
  };
192
332
 
193
- // Generate ID and create document
194
- const id = this.generateId();
195
- const docRef = doc(this.db, PROCEDURES_COLLECTION, id);
196
- await setDoc(docRef, {
197
- ...procedure,
198
- id,
333
+ // --- Transaction/Batch Start ---
334
+ const batch = writeBatch(this.db);
335
+
336
+ // 1. Create the procedure document
337
+ const procedureRef = doc(this.db, PROCEDURES_COLLECTION, procedureId);
338
+ batch.set(procedureRef, {
339
+ ...newProcedure,
199
340
  createdAt: serverTimestamp(),
200
341
  updatedAt: serverTimestamp(),
201
342
  });
202
343
 
203
- return { ...procedure, id } as Procedure;
344
+ // 2. Create the summary object AFTER the main object is defined
345
+ const procedureSummary = this._createProcedureSummaryInfo({
346
+ ...newProcedure,
347
+ createdAt: new Date(), // Use placeholder date for summary creation
348
+ updatedAt: new Date(),
349
+ });
350
+
351
+ // 3. Update Practitioner
352
+ const practitionerUpdateData: { [key: string]: FieldValue } = {
353
+ procedures: arrayUnion(procedureId),
354
+ proceduresInfo: arrayUnion(procedureSummary),
355
+ updatedAt: serverTimestamp(),
356
+ };
357
+ batch.update(practitionerRef, practitionerUpdateData);
358
+
359
+ // 4. Update Clinic
360
+ const clinicUpdateData: { [key: string]: FieldValue } = {
361
+ // services: arrayUnion(procedureId), // Decide if 'services' array is still needed
362
+ proceduresInfo: arrayUnion(procedureSummary),
363
+ // Potentially update clinic.doctors array if not already present
364
+ doctors: arrayUnion(validatedData.practitionerId),
365
+ // Potentially update clinic.doctorsInfo array
366
+ // This requires fetching existing doctorsInfo and adding/updating
367
+ // For simplicity now, we'll just add the procedure summary
368
+ updatedAt: serverTimestamp(),
369
+ };
370
+ batch.update(clinicRef, clinicUpdateData);
371
+
372
+ // --- Transaction/Batch Commit ---
373
+ await batch.commit();
374
+
375
+ // Return the created procedure (fetch again to get server timestamps)
376
+ const savedDoc = await getDoc(procedureRef);
377
+ return savedDoc.data() as Procedure;
204
378
  }
205
379
 
206
380
  /**
@@ -254,73 +428,394 @@ export class ProcedureService extends BaseService {
254
428
  }
255
429
 
256
430
  /**
257
- * Updates a procedure
431
+ * Updates a procedure and its related aggregates in Practitioner and Clinic docs
258
432
  * @param id - The ID of the procedure to update
259
- * @param data - The data to update
433
+ * @param data - The data to update the procedure with
260
434
  * @returns The updated procedure
261
435
  */
262
436
  async updateProcedure(
263
437
  id: string,
264
438
  data: UpdateProcedureData
265
439
  ): Promise<Procedure> {
266
- // Validate input data
267
440
  const validatedData = updateProcedureSchema.parse(data);
441
+ const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
442
+ const procedureSnapshot = await getDoc(procedureRef);
268
443
 
269
- // Get the existing procedure
270
- const existingProcedure = await this.getProcedure(id);
271
- if (!existingProcedure) {
444
+ if (!procedureSnapshot.exists()) {
272
445
  throw new Error(`Procedure with ID ${id} not found`);
273
446
  }
274
447
 
275
- // Update the procedure
276
- const docRef = doc(this.db, PROCEDURES_COLLECTION, id);
277
- await updateDoc(docRef, {
278
- ...validatedData,
448
+ const existingProcedure = procedureSnapshot.data() as Procedure;
449
+ let updatedProcedureData: Partial<Procedure> = { ...validatedData };
450
+
451
+ let practitionerChanged = false;
452
+ let clinicChanged = false;
453
+ const oldPractitionerId = existingProcedure.practitionerId;
454
+ const oldClinicId = existingProcedure.clinicBranchId;
455
+ let newPractitioner: Practitioner | null = null;
456
+ let newClinic: Clinic | null = null;
457
+
458
+ // --- Prepare updates and fetch new related data if IDs change ---
459
+
460
+ // Handle Practitioner Change
461
+ if (
462
+ validatedData.practitionerId &&
463
+ validatedData.practitionerId !== oldPractitionerId
464
+ ) {
465
+ practitionerChanged = true;
466
+ const newPractitionerRef = doc(
467
+ this.db,
468
+ PRACTITIONERS_COLLECTION,
469
+ validatedData.practitionerId
470
+ );
471
+ const newPractitionerSnap = await getDoc(newPractitionerRef);
472
+ if (!newPractitionerSnap.exists())
473
+ throw new Error(
474
+ `New Practitioner ${validatedData.practitionerId} not found`
475
+ );
476
+ newPractitioner = newPractitionerSnap.data() as Practitioner;
477
+ // Update doctorInfo within the procedure document
478
+ updatedProcedureData.doctorInfo = {
479
+ id: newPractitioner.id,
480
+ name: `${newPractitioner.basicInfo.firstName} ${newPractitioner.basicInfo.lastName}`,
481
+ description: newPractitioner.basicInfo.bio || "",
482
+ photo: newPractitioner.basicInfo.profileImageUrl || "",
483
+ rating: newPractitioner.reviewInfo?.averageRating || 0,
484
+ services: newPractitioner.procedures || [],
485
+ };
486
+ }
487
+
488
+ // Handle Clinic Change
489
+ if (
490
+ validatedData.clinicBranchId &&
491
+ validatedData.clinicBranchId !== oldClinicId
492
+ ) {
493
+ clinicChanged = true;
494
+ const newClinicRef = doc(
495
+ this.db,
496
+ CLINICS_COLLECTION,
497
+ validatedData.clinicBranchId
498
+ );
499
+ const newClinicSnap = await getDoc(newClinicRef);
500
+ if (!newClinicSnap.exists())
501
+ throw new Error(`New Clinic ${validatedData.clinicBranchId} not found`);
502
+ newClinic = newClinicSnap.data() as Clinic;
503
+ // Update clinicInfo within the procedure document
504
+ updatedProcedureData.clinicInfo = {
505
+ id: newClinic.id,
506
+ name: newClinic.name,
507
+ description: newClinic.description || "",
508
+ featuredPhoto:
509
+ newClinic.featuredPhotos && newClinic.featuredPhotos.length > 0
510
+ ? newClinic.featuredPhotos[0]
511
+ : newClinic.coverPhoto || "",
512
+ location: newClinic.location,
513
+ contactInfo: newClinic.contactInfo,
514
+ };
515
+ }
516
+
517
+ // Handle Category/Subcategory/Technology/Product Changes
518
+ let finalCategoryId = existingProcedure.category.id;
519
+ if (validatedData.categoryId) {
520
+ const category = await this.categoryService.getById(
521
+ validatedData.categoryId
522
+ );
523
+ if (!category)
524
+ throw new Error(`Category ${validatedData.categoryId} not found`);
525
+ updatedProcedureData.category = category;
526
+ finalCategoryId = category.id; // Update finalCategoryId if category changed
527
+ }
528
+
529
+ // Only fetch subcategory if its ID is provided AND we have a valid finalCategoryId
530
+ if (validatedData.subcategoryId && finalCategoryId) {
531
+ const subcategory = await this.subcategoryService.getById(
532
+ finalCategoryId,
533
+ validatedData.subcategoryId
534
+ );
535
+ if (!subcategory)
536
+ throw new Error(
537
+ `Subcategory ${validatedData.subcategoryId} not found for category ${finalCategoryId}`
538
+ );
539
+ updatedProcedureData.subcategory = subcategory;
540
+ } else if (validatedData.subcategoryId) {
541
+ console.warn(
542
+ "Attempted to update subcategory without a valid categoryId"
543
+ );
544
+ }
545
+
546
+ let finalTechnologyId = existingProcedure.technology.id;
547
+ if (validatedData.technologyId) {
548
+ const technology = await this.technologyService.getById(
549
+ validatedData.technologyId
550
+ );
551
+ if (!technology)
552
+ throw new Error(`Technology ${validatedData.technologyId} not found`);
553
+ updatedProcedureData.technology = technology;
554
+ finalTechnologyId = technology.id; // Update finalTechnologyId if technology changed
555
+ // Update related fields derived from technology
556
+ updatedProcedureData.blockingConditions = technology.blockingConditions;
557
+ updatedProcedureData.treatmentBenefits = technology.benefits;
558
+ updatedProcedureData.preRequirements = technology.requirements.pre;
559
+ updatedProcedureData.postRequirements = technology.requirements.post;
560
+ updatedProcedureData.certificationRequirement =
561
+ technology.certificationRequirement;
562
+ updatedProcedureData.documentationTemplates =
563
+ technology.documentationTemplates || [];
564
+ }
565
+
566
+ // Only fetch product if its ID is provided AND we have a valid finalTechnologyId
567
+ if (validatedData.productId && finalTechnologyId) {
568
+ const product = await this.productService.getById(
569
+ finalTechnologyId,
570
+ validatedData.productId
571
+ );
572
+ if (!product)
573
+ throw new Error(
574
+ `Product ${validatedData.productId} not found for technology ${finalTechnologyId}`
575
+ );
576
+ updatedProcedureData.product = product;
577
+ } else if (validatedData.productId) {
578
+ console.warn("Attempted to update product without a valid technologyId");
579
+ }
580
+
581
+ // --- Perform updates using a batch ---
582
+ const batch = writeBatch(this.db);
583
+
584
+ // 1. Update the main procedure document
585
+ batch.update(procedureRef, {
586
+ ...updatedProcedureData,
279
587
  updatedAt: serverTimestamp(),
280
588
  });
281
589
 
282
- // Return the updated procedure with combined data
283
- // Note: Since we're not changing the clinicInfo or doctorInfo in this update,
284
- // we just keep the existing values from the procedure
285
- return {
590
+ // 2. Create the updated summary info object
591
+ // We need the final state of the procedure for the summary
592
+ const finalProcedureStateForSummary = {
286
593
  ...existingProcedure,
287
- ...validatedData,
288
- updatedAt: new Date(),
289
- };
594
+ ...updatedProcedureData, // Apply updates
595
+ // Ensure nested objects needed for summary are present
596
+ category: updatedProcedureData.category || existingProcedure.category,
597
+ subcategory:
598
+ updatedProcedureData.subcategory || existingProcedure.subcategory,
599
+ technology:
600
+ updatedProcedureData.technology || existingProcedure.technology,
601
+ product: updatedProcedureData.product || existingProcedure.product,
602
+ clinicInfo:
603
+ updatedProcedureData.clinicInfo || existingProcedure.clinicInfo,
604
+ doctorInfo:
605
+ updatedProcedureData.doctorInfo || existingProcedure.doctorInfo,
606
+ practitionerId:
607
+ validatedData.practitionerId || existingProcedure.practitionerId, // Use potentially updated IDs
608
+ clinicBranchId:
609
+ validatedData.clinicBranchId || existingProcedure.clinicBranchId,
610
+ } as Procedure; // Cast because we're merging potentially partial data
611
+
612
+ const updatedProcedureSummary = this._createProcedureSummaryInfo(
613
+ finalProcedureStateForSummary
614
+ );
615
+
616
+ // 3. Update Practitioner(s)
617
+ if (practitionerChanged) {
618
+ // Remove from old practitioner
619
+ const oldPractitionerRef = doc(
620
+ this.db,
621
+ PRACTITIONERS_COLLECTION,
622
+ oldPractitionerId
623
+ );
624
+ batch.update(oldPractitionerRef, {
625
+ procedures: arrayRemove(id),
626
+ proceduresInfo: arrayRemove(...[{ id: id }]), // Matcher for removal
627
+ updatedAt: serverTimestamp(),
628
+ });
629
+ // Add to new practitioner
630
+ const newPractitionerRef = doc(
631
+ this.db,
632
+ PRACTITIONERS_COLLECTION,
633
+ updatedProcedureSummary.practitionerId
634
+ );
635
+ batch.update(newPractitionerRef, {
636
+ procedures: arrayUnion(id),
637
+ proceduresInfo: arrayUnion(updatedProcedureSummary),
638
+ updatedAt: serverTimestamp(),
639
+ });
640
+ } else {
641
+ // Update in place for the current practitioner
642
+ const currentPractitionerRef = doc(
643
+ this.db,
644
+ PRACTITIONERS_COLLECTION,
645
+ oldPractitionerId
646
+ );
647
+ // Remove old first
648
+ batch.update(currentPractitionerRef, {
649
+ proceduresInfo: arrayRemove(...[{ id: id }]),
650
+ updatedAt: serverTimestamp(),
651
+ });
652
+ // Add updated
653
+ batch.update(currentPractitionerRef, {
654
+ proceduresInfo: arrayUnion(updatedProcedureSummary),
655
+ updatedAt: serverTimestamp(),
656
+ });
657
+ }
658
+
659
+ // 4. Update Clinic(s)
660
+ if (clinicChanged) {
661
+ // Remove from old clinic
662
+ const oldClinicRef = doc(this.db, CLINICS_COLLECTION, oldClinicId);
663
+ batch.update(oldClinicRef, {
664
+ // services: arrayRemove(id),
665
+ proceduresInfo: arrayRemove(...[{ id: id }]), // Matcher for removal
666
+ updatedAt: serverTimestamp(),
667
+ // Potentially remove from clinic.doctors and clinic.doctorsInfo if practitioner also changed or was last one for this clinic
668
+ });
669
+ // Add to new clinic
670
+ const newClinicRef = doc(
671
+ this.db,
672
+ CLINICS_COLLECTION,
673
+ updatedProcedureSummary.clinicId
674
+ );
675
+ batch.update(newClinicRef, {
676
+ // services: arrayUnion(id),
677
+ proceduresInfo: arrayUnion(updatedProcedureSummary),
678
+ doctors: arrayUnion(updatedProcedureSummary.practitionerId), // Ensure practitioner is listed
679
+ updatedAt: serverTimestamp(),
680
+ });
681
+ } else {
682
+ // Update in place for the current clinic
683
+ const currentClinicRef = doc(this.db, CLINICS_COLLECTION, oldClinicId);
684
+ // Remove old first
685
+ batch.update(currentClinicRef, {
686
+ proceduresInfo: arrayRemove(...[{ id: id }]),
687
+ updatedAt: serverTimestamp(),
688
+ });
689
+ // Add updated
690
+ batch.update(currentClinicRef, {
691
+ proceduresInfo: arrayUnion(updatedProcedureSummary),
692
+ updatedAt: serverTimestamp(),
693
+ });
694
+ }
695
+
696
+ // --- Commit Batch ---
697
+ await batch.commit();
698
+
699
+ // Return the updated procedure
700
+ const updatedSnapshot = await getDoc(procedureRef);
701
+ return updatedSnapshot.data() as Procedure;
290
702
  }
291
703
 
292
704
  /**
293
- * Deactivates a procedure
705
+ * Deactivates a procedure (soft delete) and updates aggregates
294
706
  * @param id - The ID of the procedure to deactivate
295
707
  */
296
708
  async deactivateProcedure(id: string): Promise<void> {
297
- const docRef = doc(this.db, PROCEDURES_COLLECTION, id);
298
- await updateDoc(docRef, {
709
+ const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
710
+ const procedureSnap = await getDoc(procedureRef);
711
+ if (!procedureSnap.exists()) {
712
+ console.warn(`Procedure ${id} not found for deactivation.`);
713
+ return;
714
+ }
715
+ const procedure = procedureSnap.data() as Procedure;
716
+
717
+ // We treat deactivation like a deletion for the aggregated lists
718
+ // Alternatively, keep inactive procedures but filter them client-side
719
+
720
+ // --- Perform updates using a batch ---
721
+ const batch = writeBatch(this.db);
722
+
723
+ // 1. Mark procedure as inactive
724
+ batch.update(procedureRef, {
299
725
  isActive: false,
300
726
  updatedAt: serverTimestamp(),
301
727
  });
728
+
729
+ // 2. Remove from Practitioner aggregates
730
+ const practitionerRef = doc(
731
+ this.db,
732
+ PRACTITIONERS_COLLECTION,
733
+ procedure.practitionerId
734
+ );
735
+ batch.update(practitionerRef, {
736
+ procedures: arrayRemove(id),
737
+ proceduresInfo: arrayRemove(...[{ id: id }]), // Matcher for removal
738
+ updatedAt: serverTimestamp(),
739
+ });
740
+
741
+ // 3. Remove from Clinic aggregates
742
+ const clinicRef = doc(
743
+ this.db,
744
+ CLINICS_COLLECTION,
745
+ procedure.clinicBranchId
746
+ );
747
+ batch.update(clinicRef, {
748
+ // services: arrayRemove(id),
749
+ proceduresInfo: arrayRemove(...[{ id: id }]), // Matcher for removal
750
+ updatedAt: serverTimestamp(),
751
+ // Potentially update clinic.doctors/doctorsInfo if this was the last active procedure for this doctor at this clinic
752
+ });
753
+
754
+ // --- Commit Batch ---
755
+ await batch.commit();
756
+ }
757
+
758
+ /**
759
+ * Deletes a procedure permanently and updates related aggregates
760
+ * @param id - The ID of the procedure to delete
761
+ * @returns A boolean indicating if the deletion was successful
762
+ */
763
+ async deleteProcedure(id: string): Promise<boolean> {
764
+ const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
765
+ const procedureSnapshot = await getDoc(procedureRef);
766
+
767
+ if (!procedureSnapshot.exists()) {
768
+ // Already deleted or never existed
769
+ return false;
770
+ }
771
+
772
+ const procedure = procedureSnapshot.data() as Procedure;
773
+
774
+ // --- Perform updates using a batch ---
775
+ const batch = writeBatch(this.db);
776
+
777
+ // 1. Remove from Practitioner aggregates
778
+ if (procedure.practitionerId) {
779
+ const practitionerRef = doc(
780
+ this.db,
781
+ PRACTITIONERS_COLLECTION,
782
+ procedure.practitionerId
783
+ );
784
+ batch.update(practitionerRef, {
785
+ procedures: arrayRemove(id),
786
+ proceduresInfo: arrayRemove(...[{ id: id }]), // Matcher for removal
787
+ updatedAt: serverTimestamp(),
788
+ });
789
+ }
790
+
791
+ // 2. Remove from Clinic aggregates
792
+ if (procedure.clinicBranchId) {
793
+ const clinicRef = doc(
794
+ this.db,
795
+ CLINICS_COLLECTION,
796
+ procedure.clinicBranchId
797
+ );
798
+ batch.update(clinicRef, {
799
+ // services: arrayRemove(id),
800
+ proceduresInfo: arrayRemove(...[{ id: id }]), // Matcher for removal
801
+ updatedAt: serverTimestamp(),
802
+ // Potentially update clinic.doctors/doctorsInfo
803
+ });
804
+ }
805
+
806
+ // 3. Delete the procedure document itself
807
+ batch.delete(procedureRef);
808
+
809
+ // --- Commit Batch ---
810
+ await batch.commit();
811
+
812
+ return true;
302
813
  }
303
814
 
304
815
  /**
305
816
  * Gets all procedures that a practitioner is certified to perform
306
817
  * @param practitioner - The practitioner's profile
307
- * @returns Object containing:
308
- * - procedures: List of procedures the practitioner can perform
309
- * - families: List of procedure families the practitioner can perform
310
- * - categories: List of category IDs the practitioner can perform
311
- * - subcategories: List of subcategory IDs the practitioner can perform
312
- *
313
- * @example
314
- * const practitioner = {
315
- * certification: {
316
- * level: CertificationLevel.DOCTOR,
317
- * specialties: [CertificationSpecialty.INJECTABLES]
318
- * }
319
- * };
320
- * const allowedProcedures = await procedureService.getAllowedProcedures(practitioner);
321
- * console.log(allowedProcedures.families); // [ProcedureFamily.AESTHETICS]
322
- * console.log(allowedProcedures.categories); // ["category1", "category2"]
323
- * console.log(allowedProcedures.subcategories); // ["subcategory1", "subcategory2"]
818
+ * @returns Object containing allowed technologies, families, categories, subcategories
324
819
  */
325
820
  async getAllowedTechnologies(practitioner: Practitioner): Promise<{
326
821
  technologies: Technology[];
@@ -328,7 +823,7 @@ export class ProcedureService extends BaseService {
328
823
  categories: string[];
329
824
  subcategories: string[];
330
825
  }> {
331
- // Get all allowed technologies for the practitioner
826
+ // This logic depends on TechnologyService and remains valid
332
827
  const { technologies, families, categories, subcategories } =
333
828
  await this.technologyService.getAllowedTechnologies(practitioner);
334
829
 
@@ -355,19 +850,26 @@ export class ProcedureService extends BaseService {
355
850
  const proceduresCollection = collection(this.db, PROCEDURES_COLLECTION);
356
851
  let proceduresQuery = query(proceduresCollection);
357
852
 
358
- // If pagination is specified and greater than 0, limit the query
853
+ // Apply pagination if specified
359
854
  if (pagination && pagination > 0) {
360
- const { limit, startAfter } = require("firebase/firestore");
855
+ const { limit, startAfter } = await import("firebase/firestore"); // Use dynamic import if needed top-level
361
856
 
362
857
  if (lastDoc) {
363
858
  proceduresQuery = query(
364
859
  proceduresCollection,
860
+ orderBy("name"), // Use imported orderBy
365
861
  startAfter(lastDoc),
366
862
  limit(pagination)
367
863
  );
368
864
  } else {
369
- proceduresQuery = query(proceduresCollection, limit(pagination));
865
+ proceduresQuery = query(
866
+ proceduresCollection,
867
+ orderBy("name"),
868
+ limit(pagination)
869
+ ); // Use imported orderBy
370
870
  }
871
+ } else {
872
+ proceduresQuery = query(proceduresCollection, orderBy("name")); // Use imported orderBy
371
873
  }
372
874
 
373
875
  const proceduresSnapshot = await getDocs(proceduresQuery);
@@ -376,17 +878,9 @@ export class ProcedureService extends BaseService {
376
878
 
377
879
  const procedures = proceduresSnapshot.docs.map((doc) => {
378
880
  const data = doc.data() as Procedure;
379
-
380
- // Ensure clinicInfo and doctorInfo are present
381
- if (!data.clinicInfo || !data.doctorInfo) {
382
- console.warn(
383
- `Procedure ${data.id} is missing clinicInfo or doctorInfo fields. These should be updated.`
384
- );
385
- }
386
-
387
881
  return {
388
882
  ...data,
389
- id: doc.id,
883
+ id: doc.id, // Ensure ID is present
390
884
  };
391
885
  });
392
886
 
@@ -399,251 +893,4 @@ export class ProcedureService extends BaseService {
399
893
  throw error;
400
894
  }
401
895
  }
402
-
403
- /**
404
- * Updates the clinicInfo and doctorInfo fields in procedures when the source data changes
405
- *
406
- * @param entityType - The type of entity that changed ("clinic" or "doctor")
407
- * @param entityId - The ID of the entity that changed
408
- * @param updatedData - The updated data for the entity
409
- * @returns Number of procedures updated
410
- */
411
- async updateProcedureAggregateData(
412
- entityType: "clinic" | "doctor",
413
- entityId: string,
414
- updatedData: any
415
- ): Promise<number> {
416
- let proceduresQuery;
417
- const updatedField = entityType === "clinic" ? "clinicInfo" : "doctorInfo";
418
-
419
- // Find all procedures related to this entity
420
- if (entityType === "clinic") {
421
- proceduresQuery = query(
422
- collection(this.db, PROCEDURES_COLLECTION),
423
- where("clinicBranchId", "==", entityId)
424
- );
425
- } else {
426
- proceduresQuery = query(
427
- collection(this.db, PROCEDURES_COLLECTION),
428
- where("practitionerId", "==", entityId)
429
- );
430
- }
431
-
432
- const snapshot = await getDocs(proceduresQuery);
433
-
434
- if (snapshot.empty) {
435
- return 0;
436
- }
437
-
438
- // Create the updated data object
439
- let updatedFieldData;
440
-
441
- if (entityType === "clinic") {
442
- // Clinic info format
443
- updatedFieldData = {
444
- id: entityId,
445
- name: updatedData.name,
446
- description: updatedData.description || "",
447
- featuredPhoto:
448
- updatedData.featuredPhotos && updatedData.featuredPhotos.length > 0
449
- ? updatedData.featuredPhotos[0]
450
- : updatedData.coverPhoto || "",
451
- location: updatedData.location,
452
- contactInfo: updatedData.contactInfo,
453
- };
454
- } else {
455
- // Doctor info format
456
- if (updatedData.basicInfo) {
457
- // If it's a practitioner object
458
- updatedFieldData = {
459
- id: entityId,
460
- name: `${updatedData.basicInfo.firstName} ${updatedData.basicInfo.lastName}`,
461
- description: updatedData.basicInfo.bio || "",
462
- photo: updatedData.basicInfo.profileImageUrl || "",
463
- rating: 0, // This would need to be calculated or passed in
464
- services: [], // This would need to be determined or passed in
465
- };
466
- } else {
467
- // If it's already a doctorInfo object
468
- updatedFieldData = {
469
- id: entityId,
470
- name: updatedData.name,
471
- description: updatedData.description || "",
472
- photo: updatedData.photo || "",
473
- rating: updatedData.rating || 0,
474
- services: updatedData.services || [],
475
- };
476
- }
477
- }
478
-
479
- // Update all found procedures
480
- const batch = writeBatch(this.db);
481
-
482
- snapshot.docs.forEach((doc) => {
483
- batch.update(doc.ref, {
484
- [updatedField]: updatedFieldData,
485
- updatedAt: serverTimestamp(),
486
- });
487
- });
488
-
489
- await batch.commit();
490
-
491
- return snapshot.size;
492
- }
493
-
494
- /**
495
- * Updates the clinicInfo for all procedures associated with a specific clinic
496
- *
497
- * @param clinicId - The ID of the clinic that was updated
498
- * @param clinicData - The updated clinic data
499
- * @returns Number of procedures updated
500
- */
501
- async updateClinicInfoInProcedures(
502
- clinicId: string,
503
- clinicData: any
504
- ): Promise<number> {
505
- return this.updateProcedureAggregateData("clinic", clinicId, clinicData);
506
- }
507
-
508
- /**
509
- * Updates the doctorInfo for all procedures associated with a specific practitioner
510
- *
511
- * @param practitionerId - The ID of the practitioner that was updated
512
- * @param practitionerData - The updated practitioner data
513
- * @returns Number of procedures updated
514
- */
515
- async updateDoctorInfoInProcedures(
516
- practitionerId: string,
517
- practitionerData: any
518
- ): Promise<number> {
519
- return this.updateProcedureAggregateData(
520
- "doctor",
521
- practitionerId,
522
- practitionerData
523
- );
524
- }
525
-
526
- /**
527
- * Updates all existing procedures to include clinicInfo and doctorInfo
528
- * This is a migration helper method that can be used to update existing procedures
529
- *
530
- * @returns Number of procedures updated
531
- */
532
- async migrateAllProceduresWithAggregateData(): Promise<number> {
533
- // Get all procedures
534
- const proceduresQuery = query(collection(this.db, PROCEDURES_COLLECTION));
535
- const proceduresSnapshot = await getDocs(proceduresQuery);
536
-
537
- if (proceduresSnapshot.empty) {
538
- return 0;
539
- }
540
-
541
- let updatedCount = 0;
542
- const batch = writeBatch(this.db);
543
- const batchLimit = 500; // Firestore batch limit
544
- let batchCount = 0;
545
-
546
- // Process each procedure
547
- for (const procedureDoc of proceduresSnapshot.docs) {
548
- const procedure = procedureDoc.data() as Procedure;
549
-
550
- // Skip if already has both required fields
551
- if (procedure.clinicInfo && procedure.doctorInfo) {
552
- continue;
553
- }
554
-
555
- try {
556
- // Get clinic data
557
- const clinicRef = doc(
558
- this.db,
559
- CLINICS_COLLECTION,
560
- procedure.clinicBranchId
561
- );
562
- const clinicSnapshot = await getDoc(clinicRef);
563
-
564
- if (!clinicSnapshot.exists()) {
565
- console.warn(
566
- `Clinic ${procedure.clinicBranchId} not found for procedure ${procedure.id}`
567
- );
568
- continue;
569
- }
570
-
571
- const clinic = clinicSnapshot.data();
572
-
573
- // Create clinic info
574
- const clinicInfo = {
575
- id: clinicSnapshot.id,
576
- name: clinic.name,
577
- description: clinic.description || "",
578
- featuredPhoto:
579
- clinic.featuredPhotos && clinic.featuredPhotos.length > 0
580
- ? clinic.featuredPhotos[0]
581
- : clinic.coverPhoto || "",
582
- location: clinic.location,
583
- contactInfo: clinic.contactInfo,
584
- };
585
-
586
- // Get practitioner data
587
- const practitionerRef = doc(
588
- this.db,
589
- PRACTITIONERS_COLLECTION,
590
- procedure.practitionerId
591
- );
592
- const practitionerSnapshot = await getDoc(practitionerRef);
593
-
594
- if (!practitionerSnapshot.exists()) {
595
- console.warn(
596
- `Practitioner ${procedure.practitionerId} not found for procedure ${procedure.id}`
597
- );
598
- continue;
599
- }
600
-
601
- const practitioner = practitionerSnapshot.data();
602
-
603
- // Find doctor info in clinic's doctorsInfo array
604
- let doctorInfo = clinic.doctorsInfo?.find(
605
- (doctor: any) => doctor.id === procedure.practitionerId
606
- );
607
-
608
- // If not found, create basic doctor info
609
- if (!doctorInfo) {
610
- doctorInfo = {
611
- id: practitionerSnapshot.id,
612
- name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
613
- description: practitioner.basicInfo.bio || "",
614
- photo: practitioner.basicInfo.profileImageUrl || "",
615
- rating: 0,
616
- services: [],
617
- };
618
- }
619
-
620
- // Add to batch
621
- batch.update(procedureDoc.ref, {
622
- clinicInfo,
623
- doctorInfo,
624
- updatedAt: serverTimestamp(),
625
- });
626
-
627
- batchCount++;
628
- updatedCount++;
629
-
630
- // Commit batch if we've reached the limit
631
- if (batchCount >= batchLimit) {
632
- await batch.commit();
633
- console.log(`Committed batch of ${batchCount} procedure updates`);
634
- batchCount = 0;
635
- }
636
- } catch (error) {
637
- console.error(`Error updating procedure ${procedure.id}:`, error);
638
- }
639
- }
640
-
641
- // Commit any remaining updates
642
- if (batchCount > 0) {
643
- await batch.commit();
644
- console.log(`Committed final batch of ${batchCount} procedure updates`);
645
- }
646
-
647
- return updatedCount;
648
- }
649
896
  }