@blackcode_sa/metaestetics-api 1.7.10 → 1.7.12

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.
@@ -5,13 +5,9 @@ import {
5
5
  getDocs,
6
6
  query,
7
7
  where,
8
- updateDoc,
9
8
  setDoc,
10
9
  deleteDoc,
11
- Timestamp,
12
10
  serverTimestamp,
13
- DocumentData,
14
- writeBatch,
15
11
  } from "firebase/firestore";
16
12
  import { BaseService } from "../base.service";
17
13
  import {
@@ -20,9 +16,6 @@ import {
20
16
  PractitionerReview,
21
17
  ProcedureReview,
22
18
  REVIEWS_COLLECTION,
23
- ClinicReviewInfo,
24
- PractitionerReviewInfo,
25
- ProcedureReviewInfo,
26
19
  } from "../../types/reviews";
27
20
  import {
28
21
  createReviewSchema,
@@ -32,9 +25,6 @@ import { z } from "zod";
32
25
  import { Auth } from "firebase/auth";
33
26
  import { Firestore } from "firebase/firestore";
34
27
  import { FirebaseApp } from "firebase/app";
35
- import { CLINICS_COLLECTION } from "../../types/clinic";
36
- import { PRACTITIONERS_COLLECTION } from "../../types/practitioner";
37
- import { PROCEDURES_COLLECTION } from "../../types/procedure";
38
28
 
39
29
  export class ReviewService extends BaseService {
40
30
  constructor(db: Firestore, auth: Auth, app: FirebaseApp) {
@@ -42,7 +32,7 @@ export class ReviewService extends BaseService {
42
32
  }
43
33
 
44
34
  /**
45
- * Creates a new review and updates related entities
35
+ * Creates a new review
46
36
  * @param data - The review data to create
47
37
  * @param appointmentId - ID of the completed appointment
48
38
  * @returns The created review
@@ -147,34 +137,8 @@ export class ReviewService extends BaseService {
147
137
  updatedAt: serverTimestamp(),
148
138
  });
149
139
 
150
- // Update related entities
151
- const updatePromises: Promise<any>[] = [];
152
-
153
- // Update clinic if clinic review exists
154
- if (data.clinicReview) {
155
- updatePromises.push(
156
- this.updateClinicReviewInfo(data.clinicReview.clinicId)
157
- );
158
- }
159
-
160
- // Update practitioner if practitioner review exists
161
- if (data.practitionerReview) {
162
- updatePromises.push(
163
- this.updatePractitionerReviewInfo(
164
- data.practitionerReview.practitionerId
165
- )
166
- );
167
- }
168
-
169
- // Update procedure if procedure review exists
170
- if (data.procedureReview) {
171
- updatePromises.push(
172
- this.updateProcedureReviewInfo(data.procedureReview.procedureId)
173
- );
174
- }
175
-
176
- // Wait for all updates to complete
177
- await Promise.all(updatePromises);
140
+ // Note: Related entity updates (clinic, practitioner, procedure) are now handled
141
+ // by cloud functions through the ReviewsAggregationService
178
142
 
179
143
  return review;
180
144
  } catch (error) {
@@ -277,7 +241,7 @@ export class ReviewService extends BaseService {
277
241
  }
278
242
 
279
243
  /**
280
- * Deletes a review and updates related entities
244
+ * Deletes a review
281
245
  * @param reviewId The ID of the review to delete
282
246
  */
283
247
  async deleteReview(reviewId: string): Promise<void> {
@@ -289,538 +253,8 @@ export class ReviewService extends BaseService {
289
253
  // Delete the review
290
254
  await deleteDoc(doc(this.db, REVIEWS_COLLECTION, reviewId));
291
255
 
292
- // Update related entities after deletion
293
- const updatePromises: Promise<any>[] = [];
294
-
295
- if (review.clinicReview) {
296
- updatePromises.push(
297
- this.updateClinicReviewInfo(
298
- review.clinicReview.clinicId,
299
- review.clinicReview,
300
- true
301
- )
302
- );
303
- }
304
-
305
- if (review.practitionerReview) {
306
- updatePromises.push(
307
- this.updatePractitionerReviewInfo(
308
- review.practitionerReview.practitionerId,
309
- review.practitionerReview,
310
- true
311
- )
312
- );
313
- }
314
-
315
- if (review.procedureReview) {
316
- updatePromises.push(
317
- this.updateProcedureReviewInfo(
318
- review.procedureReview.procedureId,
319
- review.procedureReview,
320
- true
321
- )
322
- );
323
- }
324
-
325
- // Wait for all updates to complete
326
- await Promise.all(updatePromises);
327
- }
328
-
329
- /**
330
- * Updates the review info for a clinic
331
- * @param clinicId The ID of the clinic to update
332
- * @param newReview Optional new review being added or removed
333
- * @param isRemoval Whether this update is for a review removal
334
- * @returns The updated clinic review info
335
- */
336
- async updateClinicReviewInfo(
337
- clinicId: string,
338
- newReview?: ClinicReview,
339
- isRemoval: boolean = false
340
- ): Promise<ClinicReviewInfo> {
341
- // Get the current clinic document
342
- const clinicDoc = await getDoc(doc(this.db, CLINICS_COLLECTION, clinicId));
343
-
344
- if (!clinicDoc.exists()) {
345
- throw new Error(`Clinic with ID ${clinicId} not found`);
346
- }
347
-
348
- const clinicData = clinicDoc.data();
349
- const currentReviewInfo = (clinicData.reviewInfo as ClinicReviewInfo) || {
350
- totalReviews: 0,
351
- averageRating: 0,
352
- cleanliness: 0,
353
- facilities: 0,
354
- staffFriendliness: 0,
355
- waitingTime: 0,
356
- accessibility: 0,
357
- recommendationPercentage: 0,
358
- };
359
-
360
- // If we have no reviews and aren't adding a new one, return default values
361
- if (currentReviewInfo.totalReviews === 0 && !newReview) {
362
- await updateDoc(doc(this.db, CLINICS_COLLECTION, clinicId), {
363
- reviewInfo: currentReviewInfo,
364
- updatedAt: serverTimestamp(),
365
- });
366
- return currentReviewInfo;
367
- }
368
-
369
- let updatedReviewInfo: ClinicReviewInfo;
370
-
371
- if (newReview) {
372
- // Calculate new values based on existing averages and the new review
373
- const oldTotal = currentReviewInfo.totalReviews;
374
- const newTotal = isRemoval ? oldTotal - 1 : oldTotal + 1;
375
-
376
- // If removing the last review, set all values to 0
377
- if (newTotal === 0) {
378
- updatedReviewInfo = {
379
- totalReviews: 0,
380
- averageRating: 0,
381
- cleanliness: 0,
382
- facilities: 0,
383
- staffFriendliness: 0,
384
- waitingTime: 0,
385
- accessibility: 0,
386
- recommendationPercentage: 0,
387
- };
388
- } else {
389
- // Calculate new averages
390
- const updateAverage = (
391
- currentAvg: number,
392
- newValue: number
393
- ): number => {
394
- const currentSum = currentAvg * oldTotal;
395
- const newSum = isRemoval
396
- ? currentSum - newValue
397
- : currentSum + newValue;
398
- const newAvg = newSum / newTotal;
399
- return Math.round(newAvg * 10) / 10; // Round to 1 decimal
400
- };
401
-
402
- // Update recommendation percentage
403
- const currentRecommendations =
404
- (currentReviewInfo.recommendationPercentage / 100) * oldTotal;
405
- const newRecommendations = isRemoval
406
- ? newReview.wouldRecommend
407
- ? currentRecommendations - 1
408
- : currentRecommendations
409
- : newReview.wouldRecommend
410
- ? currentRecommendations + 1
411
- : currentRecommendations;
412
- const newRecommendationPercentage =
413
- (newRecommendations / newTotal) * 100;
414
-
415
- updatedReviewInfo = {
416
- totalReviews: newTotal,
417
- averageRating: updateAverage(
418
- currentReviewInfo.averageRating,
419
- newReview.overallRating
420
- ),
421
- cleanliness: updateAverage(
422
- currentReviewInfo.cleanliness,
423
- newReview.cleanliness
424
- ),
425
- facilities: updateAverage(
426
- currentReviewInfo.facilities,
427
- newReview.facilities
428
- ),
429
- staffFriendliness: updateAverage(
430
- currentReviewInfo.staffFriendliness,
431
- newReview.staffFriendliness
432
- ),
433
- waitingTime: updateAverage(
434
- currentReviewInfo.waitingTime,
435
- newReview.waitingTime
436
- ),
437
- accessibility: updateAverage(
438
- currentReviewInfo.accessibility,
439
- newReview.accessibility
440
- ),
441
- recommendationPercentage:
442
- Math.round(newRecommendationPercentage * 10) / 10,
443
- };
444
- }
445
- } else {
446
- // If no new review provided, keep the current info
447
- updatedReviewInfo = { ...currentReviewInfo };
448
- }
449
-
450
- // Update the clinic with the new review info
451
- await updateDoc(doc(this.db, CLINICS_COLLECTION, clinicId), {
452
- reviewInfo: updatedReviewInfo,
453
- updatedAt: serverTimestamp(),
454
- });
455
-
456
- return updatedReviewInfo;
457
- }
458
-
459
- /**
460
- * Updates the review info for a practitioner
461
- * @param practitionerId The ID of the practitioner to update
462
- * @param newReview Optional new review being added or removed
463
- * @param isRemoval Whether this update is for a review removal
464
- * @returns The updated practitioner review info
465
- */
466
- async updatePractitionerReviewInfo(
467
- practitionerId: string,
468
- newReview?: PractitionerReview,
469
- isRemoval: boolean = false
470
- ): Promise<PractitionerReviewInfo> {
471
- // Get the current practitioner document
472
- const practitionerDoc = await getDoc(
473
- doc(this.db, PRACTITIONERS_COLLECTION, practitionerId)
474
- );
475
-
476
- if (!practitionerDoc.exists()) {
477
- throw new Error(`Practitioner with ID ${practitionerId} not found`);
478
- }
479
-
480
- const practitionerData = practitionerDoc.data();
481
- const currentReviewInfo =
482
- (practitionerData.reviewInfo as PractitionerReviewInfo) || {
483
- totalReviews: 0,
484
- averageRating: 0,
485
- knowledgeAndExpertise: 0,
486
- communicationSkills: 0,
487
- bedSideManner: 0,
488
- thoroughness: 0,
489
- trustworthiness: 0,
490
- recommendationPercentage: 0,
491
- };
492
-
493
- // If we have no reviews and aren't adding a new one, return default values
494
- if (currentReviewInfo.totalReviews === 0 && !newReview) {
495
- await updateDoc(doc(this.db, PRACTITIONERS_COLLECTION, practitionerId), {
496
- reviewInfo: currentReviewInfo,
497
- updatedAt: serverTimestamp(),
498
- });
499
- return currentReviewInfo;
500
- }
501
-
502
- let updatedReviewInfo: PractitionerReviewInfo;
503
-
504
- if (newReview) {
505
- // Calculate new values based on existing averages and the new review
506
- const oldTotal = currentReviewInfo.totalReviews;
507
- const newTotal = isRemoval ? oldTotal - 1 : oldTotal + 1;
508
-
509
- // If removing the last review, set all values to 0
510
- if (newTotal === 0) {
511
- updatedReviewInfo = {
512
- totalReviews: 0,
513
- averageRating: 0,
514
- knowledgeAndExpertise: 0,
515
- communicationSkills: 0,
516
- bedSideManner: 0,
517
- thoroughness: 0,
518
- trustworthiness: 0,
519
- recommendationPercentage: 0,
520
- };
521
- } else {
522
- // Calculate new averages
523
- const updateAverage = (
524
- currentAvg: number,
525
- newValue: number
526
- ): number => {
527
- const currentSum = currentAvg * oldTotal;
528
- const newSum = isRemoval
529
- ? currentSum - newValue
530
- : currentSum + newValue;
531
- const newAvg = newSum / newTotal;
532
- return Math.round(newAvg * 10) / 10; // Round to 1 decimal
533
- };
534
-
535
- // Update recommendation percentage
536
- const currentRecommendations =
537
- (currentReviewInfo.recommendationPercentage / 100) * oldTotal;
538
- const newRecommendations = isRemoval
539
- ? newReview.wouldRecommend
540
- ? currentRecommendations - 1
541
- : currentRecommendations
542
- : newReview.wouldRecommend
543
- ? currentRecommendations + 1
544
- : currentRecommendations;
545
- const newRecommendationPercentage =
546
- (newRecommendations / newTotal) * 100;
547
-
548
- updatedReviewInfo = {
549
- totalReviews: newTotal,
550
- averageRating: updateAverage(
551
- currentReviewInfo.averageRating,
552
- newReview.overallRating
553
- ),
554
- knowledgeAndExpertise: updateAverage(
555
- currentReviewInfo.knowledgeAndExpertise,
556
- newReview.knowledgeAndExpertise
557
- ),
558
- communicationSkills: updateAverage(
559
- currentReviewInfo.communicationSkills,
560
- newReview.communicationSkills
561
- ),
562
- bedSideManner: updateAverage(
563
- currentReviewInfo.bedSideManner,
564
- newReview.bedSideManner
565
- ),
566
- thoroughness: updateAverage(
567
- currentReviewInfo.thoroughness,
568
- newReview.thoroughness
569
- ),
570
- trustworthiness: updateAverage(
571
- currentReviewInfo.trustworthiness,
572
- newReview.trustworthiness
573
- ),
574
- recommendationPercentage:
575
- Math.round(newRecommendationPercentage * 10) / 10,
576
- };
577
- }
578
- } else {
579
- // If no new review provided, keep the current info
580
- updatedReviewInfo = { ...currentReviewInfo };
581
- }
582
-
583
- // Update the practitioner with the new review info
584
- await updateDoc(doc(this.db, PRACTITIONERS_COLLECTION, practitionerId), {
585
- reviewInfo: updatedReviewInfo,
586
- updatedAt: serverTimestamp(),
587
- });
588
-
589
- // Also update doctor info in procedures with the new rating
590
- await this.updateDoctorInfoInProcedures(
591
- practitionerId,
592
- updatedReviewInfo.averageRating
593
- );
594
-
595
- return updatedReviewInfo;
596
- }
597
-
598
- /**
599
- * Updates the review info for a procedure
600
- * @param procedureId The ID of the procedure to update
601
- * @param newReview Optional new review being added or removed
602
- * @param isRemoval Whether this update is for a review removal
603
- * @returns The updated procedure review info
604
- */
605
- async updateProcedureReviewInfo(
606
- procedureId: string,
607
- newReview?: ProcedureReview,
608
- isRemoval: boolean = false
609
- ): Promise<ProcedureReviewInfo> {
610
- // Get the current procedure document
611
- const procedureDoc = await getDoc(
612
- doc(this.db, PROCEDURES_COLLECTION, procedureId)
613
- );
614
-
615
- if (!procedureDoc.exists()) {
616
- throw new Error(`Procedure with ID ${procedureId} not found`);
617
- }
618
-
619
- const procedureData = procedureDoc.data();
620
- const currentReviewInfo =
621
- (procedureData.reviewInfo as ProcedureReviewInfo) || {
622
- totalReviews: 0,
623
- averageRating: 0,
624
- effectivenessOfTreatment: 0,
625
- outcomeExplanation: 0,
626
- painManagement: 0,
627
- followUpCare: 0,
628
- valueForMoney: 0,
629
- recommendationPercentage: 0,
630
- };
631
-
632
- // If we have no reviews and aren't adding a new one, return default values
633
- if (currentReviewInfo.totalReviews === 0 && !newReview) {
634
- await updateDoc(doc(this.db, PROCEDURES_COLLECTION, procedureId), {
635
- reviewInfo: currentReviewInfo,
636
- updatedAt: serverTimestamp(),
637
- });
638
- return currentReviewInfo;
639
- }
640
-
641
- let updatedReviewInfo: ProcedureReviewInfo;
642
-
643
- if (newReview) {
644
- // Calculate new values based on existing averages and the new review
645
- const oldTotal = currentReviewInfo.totalReviews;
646
- const newTotal = isRemoval ? oldTotal - 1 : oldTotal + 1;
647
-
648
- // If removing the last review, set all values to 0
649
- if (newTotal === 0) {
650
- updatedReviewInfo = {
651
- totalReviews: 0,
652
- averageRating: 0,
653
- effectivenessOfTreatment: 0,
654
- outcomeExplanation: 0,
655
- painManagement: 0,
656
- followUpCare: 0,
657
- valueForMoney: 0,
658
- recommendationPercentage: 0,
659
- };
660
- } else {
661
- // Calculate new averages
662
- const updateAverage = (
663
- currentAvg: number,
664
- newValue: number
665
- ): number => {
666
- const currentSum = currentAvg * oldTotal;
667
- const newSum = isRemoval
668
- ? currentSum - newValue
669
- : currentSum + newValue;
670
- const newAvg = newSum / newTotal;
671
- return Math.round(newAvg * 10) / 10; // Round to 1 decimal
672
- };
673
-
674
- // Update recommendation percentage
675
- const currentRecommendations =
676
- (currentReviewInfo.recommendationPercentage / 100) * oldTotal;
677
- const newRecommendations = isRemoval
678
- ? newReview.wouldRecommend
679
- ? currentRecommendations - 1
680
- : currentRecommendations
681
- : newReview.wouldRecommend
682
- ? currentRecommendations + 1
683
- : currentRecommendations;
684
- const newRecommendationPercentage =
685
- (newRecommendations / newTotal) * 100;
686
-
687
- updatedReviewInfo = {
688
- totalReviews: newTotal,
689
- averageRating: updateAverage(
690
- currentReviewInfo.averageRating,
691
- newReview.overallRating
692
- ),
693
- effectivenessOfTreatment: updateAverage(
694
- currentReviewInfo.effectivenessOfTreatment,
695
- newReview.effectivenessOfTreatment
696
- ),
697
- outcomeExplanation: updateAverage(
698
- currentReviewInfo.outcomeExplanation,
699
- newReview.outcomeExplanation
700
- ),
701
- painManagement: updateAverage(
702
- currentReviewInfo.painManagement,
703
- newReview.painManagement
704
- ),
705
- followUpCare: updateAverage(
706
- currentReviewInfo.followUpCare,
707
- newReview.followUpCare
708
- ),
709
- valueForMoney: updateAverage(
710
- currentReviewInfo.valueForMoney,
711
- newReview.valueForMoney
712
- ),
713
- recommendationPercentage:
714
- Math.round(newRecommendationPercentage * 10) / 10,
715
- };
716
- }
717
- } else {
718
- // If no new review provided, keep the current info
719
- updatedReviewInfo = { ...currentReviewInfo };
720
- }
721
-
722
- // Update the procedure with the new review info
723
- await updateDoc(doc(this.db, PROCEDURES_COLLECTION, procedureId), {
724
- reviewInfo: updatedReviewInfo,
725
- updatedAt: serverTimestamp(),
726
- });
727
-
728
- return updatedReviewInfo;
729
- }
730
-
731
- /**
732
- * Updates doctorInfo rating in all procedures for a practitioner
733
- * @param practitionerId The ID of the practitioner
734
- * @param rating The new rating to set
735
- */
736
- private async updateDoctorInfoInProcedures(
737
- practitionerId: string,
738
- rating: number
739
- ): Promise<void> {
740
- // Find all procedures for this practitioner
741
- const q = query(
742
- collection(this.db, PROCEDURES_COLLECTION),
743
- where("practitionerId", "==", practitionerId)
744
- );
745
-
746
- const snapshot = await getDocs(q);
747
-
748
- if (snapshot.empty) {
749
- return;
750
- }
751
-
752
- // Batch update all procedures
753
- const batch = writeBatch(this.db);
754
-
755
- snapshot.docs.forEach((docSnapshot) => {
756
- const procedureRef = doc(this.db, PROCEDURES_COLLECTION, docSnapshot.id);
757
- batch.update(procedureRef, {
758
- "doctorInfo.rating": rating,
759
- updatedAt: serverTimestamp(),
760
- });
761
- });
762
-
763
- await batch.commit();
764
- }
765
-
766
- /**
767
- * Verifies a review as checked by admin/staff
768
- * @param reviewId The ID of the review to verify
769
- */
770
- async verifyReview(reviewId: string): Promise<void> {
771
- const review = await this.getReview(reviewId);
772
- if (!review) {
773
- throw new Error(`Review with ID ${reviewId} not found`);
774
- }
775
-
776
- const batch = writeBatch(this.db);
777
-
778
- // Update main review document
779
- batch.update(doc(this.db, REVIEWS_COLLECTION, reviewId), {
780
- updatedAt: serverTimestamp(),
781
- });
782
-
783
- // Update clinic review if it exists
784
- if (review.clinicReview) {
785
- review.clinicReview.isVerified = true;
786
- }
787
-
788
- // Update practitioner review if it exists
789
- if (review.practitionerReview) {
790
- review.practitionerReview.isVerified = true;
791
- }
792
-
793
- // Update procedure review if it exists
794
- if (review.procedureReview) {
795
- review.procedureReview.isVerified = true;
796
- }
797
-
798
- await batch.commit();
799
-
800
- // Update all related entities
801
- const updatePromises: Promise<any>[] = [];
802
-
803
- if (review.clinicReview) {
804
- updatePromises.push(
805
- this.updateClinicReviewInfo(review.clinicReview.clinicId)
806
- );
807
- }
808
-
809
- if (review.practitionerReview) {
810
- updatePromises.push(
811
- this.updatePractitionerReviewInfo(
812
- review.practitionerReview.practitionerId
813
- )
814
- );
815
- }
816
-
817
- if (review.procedureReview) {
818
- updatePromises.push(
819
- this.updateProcedureReviewInfo(review.procedureReview.procedureId)
820
- );
821
- }
822
-
823
- await Promise.all(updatePromises);
256
+ // Note: Updates to related entities after deletion are now handled
257
+ // by cloud functions through the ReviewsAggregationService
824
258
  }
825
259
 
826
260
  /**
@@ -19,6 +19,7 @@ import {
19
19
  ClinicTag,
20
20
  ClinicPhotoTag,
21
21
  } from "./preferences.types";
22
+ import type { MediaResource } from "../../services/media/media.service";
22
23
 
23
24
  export * from "./preferences.types";
24
25
 
@@ -203,6 +204,10 @@ export interface ClinicGroup {
203
204
  calendarSyncEnabled?: boolean;
204
205
  autoConfirmAppointments?: boolean;
205
206
  businessIdentificationNumber?: string | null;
207
+ onboarding?: {
208
+ completed?: boolean;
209
+ step?: number;
210
+ };
206
211
  }
207
212
 
208
213
  /**
@@ -223,6 +228,10 @@ export interface CreateClinicGroupData {
223
228
  calendarSyncEnabled?: boolean;
224
229
  autoConfirmAppointments?: boolean;
225
230
  businessIdentificationNumber?: string | null;
231
+ onboarding?: {
232
+ completed?: boolean;
233
+ step?: number;
234
+ };
226
235
  }
227
236
 
228
237
  /**
@@ -253,11 +262,6 @@ export interface DoctorInfo {
253
262
  // TODO: Add aggregated fields, like rating, reviews, services this doctor provides in this clinic and similar
254
263
  }
255
264
 
256
- /**
257
- * Type that allows a field to be either a URL string or a File object
258
- */
259
- export type MediaResource = string | File | Blob;
260
-
261
265
  /**
262
266
  * Interface for clinic
263
267
  */
@@ -330,6 +334,10 @@ export interface CreateDefaultClinicGroupData {
330
334
  practiceType?: PracticeType;
331
335
  languages?: Language[];
332
336
  subscriptionModel?: SubscriptionModel;
337
+ onboarding?: {
338
+ completed?: boolean;
339
+ step?: number;
340
+ };
333
341
  }
334
342
 
335
343
  /**
@@ -364,6 +372,10 @@ export interface ClinicGroupSetupData {
364
372
  calendarSyncEnabled: boolean;
365
373
  autoConfirmAppointments: boolean;
366
374
  businessIdentificationNumber: string | null;
375
+ onboarding?: {
376
+ completed?: boolean;
377
+ step?: number;
378
+ };
367
379
  }
368
380
 
369
381
  /**