@blackcode_sa/metaestetics-api 1.5.27 → 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 (35) 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 +4184 -2426
  8. package/dist/index.d.ts +4184 -2426
  9. package/dist/index.js +2692 -1546
  10. package/dist/index.mjs +2663 -1502
  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/calendar/calendar-refactored.service.ts +1 -1
  19. package/src/services/clinic/clinic.service.ts +344 -77
  20. package/src/services/clinic/utils/clinic.utils.ts +187 -8
  21. package/src/services/clinic/utils/filter.utils.d.ts +23 -0
  22. package/src/services/clinic/utils/filter.utils.ts +264 -0
  23. package/src/services/practitioner/practitioner.service.ts +616 -5
  24. package/src/services/procedure/procedure.service.ts +678 -52
  25. package/src/services/reviews/reviews.service.ts +842 -0
  26. package/src/types/clinic/index.ts +24 -56
  27. package/src/types/practitioner/index.ts +34 -33
  28. package/src/types/procedure/index.ts +39 -0
  29. package/src/types/profile/index.ts +1 -1
  30. package/src/types/reviews/index.ts +126 -0
  31. package/src/validations/clinic.schema.ts +37 -64
  32. package/src/validations/practitioner.schema.ts +42 -32
  33. package/src/validations/procedure.schema.ts +14 -3
  34. package/src/validations/reviews.schema.ts +189 -0
  35. package/src/services/clinic/utils/review.utils.ts +0 -93
@@ -11,6 +11,11 @@ import {
11
11
  Timestamp,
12
12
  serverTimestamp,
13
13
  DocumentData,
14
+ writeBatch,
15
+ arrayUnion,
16
+ arrayRemove,
17
+ FieldValue,
18
+ orderBy,
14
19
  } from "firebase/firestore";
15
20
  import { BaseService } from "../base.service";
16
21
  import {
@@ -18,6 +23,7 @@ import {
18
23
  CreateProcedureData,
19
24
  UpdateProcedureData,
20
25
  PROCEDURES_COLLECTION,
26
+ ProcedureSummaryInfo,
21
27
  } from "../../types/procedure";
22
28
  import {
23
29
  createProcedureSchema,
@@ -47,12 +53,17 @@ import { CategoryService } from "../../backoffice/services/category.service";
47
53
  import { SubcategoryService } from "../../backoffice/services/subcategory.service";
48
54
  import { TechnologyService } from "../../backoffice/services/technology.service";
49
55
  import { ProductService } from "../../backoffice/services/product.service";
50
- import { Practitioner } from "../../types/practitioner";
56
+ import {
57
+ Practitioner,
58
+ PRACTITIONERS_COLLECTION,
59
+ } from "../../types/practitioner";
51
60
  import {
52
61
  CertificationLevel,
53
62
  CertificationSpecialty,
54
63
  ProcedureFamily,
55
64
  } from "../../backoffice/types";
65
+ import { Clinic, CLINICS_COLLECTION } from "../../types/clinic";
66
+ import { ProcedureReviewInfo } from "../../types/reviews";
56
67
 
57
68
  export class ProcedureService extends BaseService {
58
69
  private categoryService: CategoryService;
@@ -76,16 +87,150 @@ export class ProcedureService extends BaseService {
76
87
  this.productService = productService;
77
88
  }
78
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
+
79
225
  /**
80
- * Creates a new procedure
226
+ * Creates a new procedure and updates related practitioner/clinic aggregates
81
227
  * @param data - The data for creating a new procedure
82
228
  * @returns The created procedure
83
229
  */
84
230
  async createProcedure(data: CreateProcedureData): Promise<Procedure> {
85
- // Validate input data
86
231
  const validatedData = createProcedureSchema.parse(data);
87
232
 
88
- // Get references to related entities
233
+ // Get references to related entities (Category, Subcategory, Technology, Product)
89
234
  const [category, subcategory, technology, product] = await Promise.all([
90
235
  this.categoryService.getById(validatedData.categoryId),
91
236
  this.subcategoryService.getById(
@@ -100,13 +245,66 @@ export class ProcedureService extends BaseService {
100
245
  ]);
101
246
 
102
247
  if (!category || !subcategory || !technology || !product) {
103
- throw new Error("One or more required entities not found");
248
+ throw new Error("One or more required base entities not found");
104
249
  }
105
250
 
251
+ // Get clinic and practitioner information for aggregation
252
+ const clinicRef = doc(
253
+ this.db,
254
+ CLINICS_COLLECTION,
255
+ validatedData.clinicBranchId
256
+ );
257
+ const clinicSnapshot = await getDoc(clinicRef);
258
+ if (!clinicSnapshot.exists()) {
259
+ throw new Error(
260
+ `Clinic with ID ${validatedData.clinicBranchId} not found`
261
+ );
262
+ }
263
+ const clinic = clinicSnapshot.data() as Clinic; // Assert type
264
+
265
+ const practitionerRef = doc(
266
+ this.db,
267
+ PRACTITIONERS_COLLECTION,
268
+ validatedData.practitionerId
269
+ );
270
+ const practitionerSnapshot = await getDoc(practitionerRef);
271
+ if (!practitionerSnapshot.exists()) {
272
+ throw new Error(
273
+ `Practitioner with ID ${validatedData.practitionerId} not found`
274
+ );
275
+ }
276
+ const practitioner = practitionerSnapshot.data() as Practitioner; // Assert type
277
+
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
+ };
290
+
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
+ };
301
+
106
302
  // Create the procedure object
107
- const procedure: Omit<Procedure, "id"> = {
303
+ const procedureId = this.generateId();
304
+ const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
305
+ id: procedureId,
108
306
  ...validatedData,
109
- category,
307
+ category, // Embed full objects
110
308
  subcategory,
111
309
  technology,
112
310
  product,
@@ -116,22 +314,67 @@ export class ProcedureService extends BaseService {
116
314
  postRequirements: technology.requirements.post,
117
315
  certificationRequirement: technology.certificationRequirement,
118
316
  documentationTemplates: technology.documentationTemplates || [],
119
- isActive: true,
120
- createdAt: new Date(),
121
- 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
122
331
  };
123
332
 
124
- // Generate ID and create document
125
- const id = this.generateId();
126
- const docRef = doc(this.db, PROCEDURES_COLLECTION, id);
127
- await setDoc(docRef, {
128
- ...procedure,
129
- 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,
130
340
  createdAt: serverTimestamp(),
131
341
  updatedAt: serverTimestamp(),
132
342
  });
133
343
 
134
- 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;
135
378
  }
136
379
 
137
380
  /**
@@ -185,70 +428,394 @@ export class ProcedureService extends BaseService {
185
428
  }
186
429
 
187
430
  /**
188
- * Updates a procedure
431
+ * Updates a procedure and its related aggregates in Practitioner and Clinic docs
189
432
  * @param id - The ID of the procedure to update
190
- * @param data - The data to update
433
+ * @param data - The data to update the procedure with
191
434
  * @returns The updated procedure
192
435
  */
193
436
  async updateProcedure(
194
437
  id: string,
195
438
  data: UpdateProcedureData
196
439
  ): Promise<Procedure> {
197
- // Validate input data
198
440
  const validatedData = updateProcedureSchema.parse(data);
441
+ const procedureRef = doc(this.db, PROCEDURES_COLLECTION, id);
442
+ const procedureSnapshot = await getDoc(procedureRef);
199
443
 
200
- // Get the existing procedure
201
- const existingProcedure = await this.getProcedure(id);
202
- if (!existingProcedure) {
444
+ if (!procedureSnapshot.exists()) {
203
445
  throw new Error(`Procedure with ID ${id} not found`);
204
446
  }
205
447
 
206
- // Update the procedure
207
- const docRef = doc(this.db, PROCEDURES_COLLECTION, id);
208
- await updateDoc(docRef, {
209
- ...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,
210
587
  updatedAt: serverTimestamp(),
211
588
  });
212
589
 
213
- return {
590
+ // 2. Create the updated summary info object
591
+ // We need the final state of the procedure for the summary
592
+ const finalProcedureStateForSummary = {
214
593
  ...existingProcedure,
215
- ...validatedData,
216
- updatedAt: new Date(),
217
- };
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;
218
702
  }
219
703
 
220
704
  /**
221
- * Deactivates a procedure
705
+ * Deactivates a procedure (soft delete) and updates aggregates
222
706
  * @param id - The ID of the procedure to deactivate
223
707
  */
224
708
  async deactivateProcedure(id: string): Promise<void> {
225
- const docRef = doc(this.db, PROCEDURES_COLLECTION, id);
226
- 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, {
227
725
  isActive: false,
228
726
  updatedAt: serverTimestamp(),
229
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;
230
813
  }
231
814
 
232
815
  /**
233
816
  * Gets all procedures that a practitioner is certified to perform
234
817
  * @param practitioner - The practitioner's profile
235
- * @returns Object containing:
236
- * - procedures: List of procedures the practitioner can perform
237
- * - families: List of procedure families the practitioner can perform
238
- * - categories: List of category IDs the practitioner can perform
239
- * - subcategories: List of subcategory IDs the practitioner can perform
240
- *
241
- * @example
242
- * const practitioner = {
243
- * certification: {
244
- * level: CertificationLevel.DOCTOR,
245
- * specialties: [CertificationSpecialty.INJECTABLES]
246
- * }
247
- * };
248
- * const allowedProcedures = await procedureService.getAllowedProcedures(practitioner);
249
- * console.log(allowedProcedures.families); // [ProcedureFamily.AESTHETICS]
250
- * console.log(allowedProcedures.categories); // ["category1", "category2"]
251
- * console.log(allowedProcedures.subcategories); // ["subcategory1", "subcategory2"]
818
+ * @returns Object containing allowed technologies, families, categories, subcategories
252
819
  */
253
820
  async getAllowedTechnologies(practitioner: Practitioner): Promise<{
254
821
  technologies: Technology[];
@@ -256,7 +823,7 @@ export class ProcedureService extends BaseService {
256
823
  categories: string[];
257
824
  subcategories: string[];
258
825
  }> {
259
- // Get all allowed technologies for the practitioner
826
+ // This logic depends on TechnologyService and remains valid
260
827
  const { technologies, families, categories, subcategories } =
261
828
  await this.technologyService.getAllowedTechnologies(practitioner);
262
829
 
@@ -267,4 +834,63 @@ export class ProcedureService extends BaseService {
267
834
  subcategories,
268
835
  };
269
836
  }
837
+
838
+ /**
839
+ * Gets all procedures with optional pagination
840
+ *
841
+ * @param pagination - Optional number of procedures per page (0 or undefined returns all)
842
+ * @param lastDoc - Optional last document for pagination (if continuing from a previous page)
843
+ * @returns Object containing procedures array and the last document for pagination
844
+ */
845
+ async getAllProcedures(
846
+ pagination?: number,
847
+ lastDoc?: any
848
+ ): Promise<{ procedures: Procedure[]; lastDoc: any }> {
849
+ try {
850
+ const proceduresCollection = collection(this.db, PROCEDURES_COLLECTION);
851
+ let proceduresQuery = query(proceduresCollection);
852
+
853
+ // Apply pagination if specified
854
+ if (pagination && pagination > 0) {
855
+ const { limit, startAfter } = await import("firebase/firestore"); // Use dynamic import if needed top-level
856
+
857
+ if (lastDoc) {
858
+ proceduresQuery = query(
859
+ proceduresCollection,
860
+ orderBy("name"), // Use imported orderBy
861
+ startAfter(lastDoc),
862
+ limit(pagination)
863
+ );
864
+ } else {
865
+ proceduresQuery = query(
866
+ proceduresCollection,
867
+ orderBy("name"),
868
+ limit(pagination)
869
+ ); // Use imported orderBy
870
+ }
871
+ } else {
872
+ proceduresQuery = query(proceduresCollection, orderBy("name")); // Use imported orderBy
873
+ }
874
+
875
+ const proceduresSnapshot = await getDocs(proceduresQuery);
876
+ const lastVisible =
877
+ proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
878
+
879
+ const procedures = proceduresSnapshot.docs.map((doc) => {
880
+ const data = doc.data() as Procedure;
881
+ return {
882
+ ...data,
883
+ id: doc.id, // Ensure ID is present
884
+ };
885
+ });
886
+
887
+ return {
888
+ procedures,
889
+ lastDoc: lastVisible,
890
+ };
891
+ } catch (error) {
892
+ console.error("[PROCEDURE_SERVICE] Error getting all procedures:", error);
893
+ throw error;
894
+ }
895
+ }
270
896
  }