@blackcode_sa/metaestetics-api 1.12.64 → 1.12.66
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.
- package/dist/admin/index.d.mts +2 -0
- package/dist/admin/index.d.ts +2 -0
- package/dist/admin/index.js +45 -4
- package/dist/admin/index.mjs +45 -4
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +80 -11
- package/dist/index.mjs +80 -11
- package/package.json +119 -119
- package/src/__mocks__/firstore.ts +10 -10
- package/src/admin/aggregation/README.md +79 -79
- package/src/admin/aggregation/appointment/README.md +128 -128
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1844 -1844
- package/src/admin/aggregation/appointment/index.ts +1 -1
- package/src/admin/aggregation/clinic/README.md +52 -52
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
- package/src/admin/aggregation/clinic/index.ts +1 -1
- package/src/admin/aggregation/forms/README.md +13 -13
- package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
- package/src/admin/aggregation/forms/index.ts +1 -1
- package/src/admin/aggregation/index.ts +8 -8
- package/src/admin/aggregation/patient/README.md +27 -27
- package/src/admin/aggregation/patient/index.ts +1 -1
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
- package/src/admin/aggregation/practitioner/README.md +42 -42
- package/src/admin/aggregation/practitioner/index.ts +1 -1
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
- package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
- package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
- package/src/admin/aggregation/procedure/README.md +43 -43
- package/src/admin/aggregation/procedure/index.ts +1 -1
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
- package/src/admin/aggregation/reviews/index.ts +1 -1
- package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -641
- package/src/admin/booking/README.md +125 -125
- package/src/admin/booking/booking.admin.ts +1037 -1037
- package/src/admin/booking/booking.calculator.ts +712 -712
- package/src/admin/booking/booking.types.ts +59 -59
- package/src/admin/booking/index.ts +3 -3
- package/src/admin/booking/timezones-problem.md +185 -185
- package/src/admin/calendar/README.md +7 -7
- package/src/admin/calendar/calendar.admin.service.ts +345 -345
- package/src/admin/calendar/index.ts +1 -1
- package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
- package/src/admin/documentation-templates/index.ts +1 -1
- package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
- package/src/admin/free-consultation/index.ts +1 -1
- package/src/admin/index.ts +75 -75
- package/src/admin/logger/index.ts +78 -78
- package/src/admin/mailing/README.md +95 -95
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
- package/src/admin/mailing/appointment/index.ts +1 -1
- package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
- package/src/admin/mailing/base.mailing.service.ts +208 -208
- package/src/admin/mailing/index.ts +3 -3
- package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
- package/src/admin/mailing/practitionerInvite/index.ts +2 -2
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
- package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
- package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
- package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
- package/src/admin/notifications/index.ts +1 -1
- package/src/admin/notifications/notifications.admin.ts +710 -710
- package/src/admin/requirements/README.md +128 -128
- package/src/admin/requirements/index.ts +1 -1
- package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
- package/src/admin/users/index.ts +1 -1
- package/src/admin/users/user-profile.admin.ts +405 -405
- package/src/backoffice/constants/certification.constants.ts +13 -13
- package/src/backoffice/constants/index.ts +1 -1
- package/src/backoffice/errors/backoffice.errors.ts +181 -181
- package/src/backoffice/errors/index.ts +1 -1
- package/src/backoffice/expo-safe/README.md +26 -26
- package/src/backoffice/expo-safe/index.ts +41 -41
- package/src/backoffice/index.ts +5 -5
- package/src/backoffice/services/FIXES_README.md +102 -102
- package/src/backoffice/services/README.md +40 -40
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +318 -318
- package/src/backoffice/services/constants.service.ts +385 -385
- package/src/backoffice/services/documentation-template.service.ts +202 -202
- package/src/backoffice/services/index.ts +10 -10
- package/src/backoffice/services/migrate-products.ts +116 -116
- package/src/backoffice/services/product.service.ts +553 -553
- package/src/backoffice/services/requirement.service.ts +235 -235
- package/src/backoffice/services/subcategory.service.ts +395 -395
- package/src/backoffice/services/technology.service.ts +1083 -1083
- package/src/backoffice/types/README.md +12 -12
- package/src/backoffice/types/admin-constants.types.ts +69 -69
- package/src/backoffice/types/brand.types.ts +29 -29
- package/src/backoffice/types/category.types.ts +62 -62
- package/src/backoffice/types/documentation-templates.types.ts +28 -28
- package/src/backoffice/types/index.ts +10 -10
- package/src/backoffice/types/procedure-product.types.ts +38 -38
- package/src/backoffice/types/product.types.ts +240 -240
- package/src/backoffice/types/requirement.types.ts +63 -63
- package/src/backoffice/types/static/README.md +18 -18
- package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
- package/src/backoffice/types/static/certification.types.ts +37 -37
- package/src/backoffice/types/static/contraindication.types.ts +19 -19
- package/src/backoffice/types/static/index.ts +6 -6
- package/src/backoffice/types/static/pricing.types.ts +16 -16
- package/src/backoffice/types/static/procedure-family.types.ts +14 -14
- package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
- package/src/backoffice/types/subcategory.types.ts +34 -34
- package/src/backoffice/types/technology.types.ts +163 -163
- package/src/backoffice/validations/index.ts +1 -1
- package/src/backoffice/validations/schemas.ts +164 -164
- package/src/config/__mocks__/firebase.ts +99 -99
- package/src/config/firebase.ts +78 -78
- package/src/config/index.ts +9 -9
- package/src/errors/auth.error.ts +6 -6
- package/src/errors/auth.errors.ts +200 -200
- package/src/errors/clinic.errors.ts +32 -32
- package/src/errors/firebase.errors.ts +47 -47
- package/src/errors/user.errors.ts +99 -99
- package/src/index.backup.ts +407 -407
- package/src/index.ts +6 -6
- package/src/locales/en.ts +31 -31
- package/src/recommender/admin/index.ts +1 -1
- package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
- package/src/recommender/front/index.ts +1 -1
- package/src/recommender/front/services/onboarding.service.ts +5 -5
- package/src/recommender/front/services/recommender.service.ts +3 -3
- package/src/recommender/index.ts +1 -1
- package/src/services/PATIENTAUTH.MD +197 -197
- package/src/services/README.md +106 -106
- package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
- package/src/services/__tests__/auth/auth.setup.ts +293 -293
- package/src/services/__tests__/auth.service.test.ts +346 -346
- package/src/services/__tests__/base.service.test.ts +77 -77
- package/src/services/__tests__/user.service.test.ts +528 -528
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2505 -2505
- package/src/services/appointment/index.ts +1 -1
- package/src/services/appointment/utils/appointment.utils.ts +552 -552
- package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
- package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
- package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
- package/src/services/appointment/utils/zone-management.utils.ts +353 -353
- package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
- package/src/services/auth/auth.service.ts +989 -989
- package/src/services/auth/auth.v2.service.ts +961 -961
- package/src/services/auth/index.ts +7 -7
- package/src/services/auth/utils/error.utils.ts +90 -90
- package/src/services/auth/utils/firebase.utils.ts +49 -49
- package/src/services/auth/utils/index.ts +21 -21
- package/src/services/auth/utils/practitioner.utils.ts +125 -125
- package/src/services/base.service.ts +41 -41
- package/src/services/calendar/calendar.service.ts +1077 -1077
- package/src/services/calendar/calendar.v2.service.ts +1683 -1683
- package/src/services/calendar/calendar.v3.service.ts +313 -313
- package/src/services/calendar/externalCalendar.service.ts +178 -178
- package/src/services/calendar/index.ts +5 -5
- package/src/services/calendar/synced-calendars.service.ts +743 -743
- package/src/services/calendar/utils/appointment.utils.ts +265 -265
- package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
- package/src/services/calendar/utils/clinic.utils.ts +237 -237
- package/src/services/calendar/utils/docs.utils.ts +157 -157
- package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
- package/src/services/calendar/utils/index.ts +8 -8
- package/src/services/calendar/utils/patient.utils.ts +198 -198
- package/src/services/calendar/utils/practitioner.utils.ts +221 -221
- package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
- package/src/services/clinic/README.md +204 -204
- package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
- package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
- package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
- package/src/services/clinic/billing-transactions.service.ts +217 -217
- package/src/services/clinic/clinic-admin.service.ts +202 -202
- package/src/services/clinic/clinic-group.service.ts +310 -310
- package/src/services/clinic/clinic.service.ts +708 -708
- package/src/services/clinic/index.ts +5 -5
- package/src/services/clinic/practitioner-invite.service.ts +519 -519
- package/src/services/clinic/utils/admin.utils.ts +551 -551
- package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
- package/src/services/clinic/utils/clinic.utils.ts +949 -949
- package/src/services/clinic/utils/filter.utils.d.ts +23 -23
- package/src/services/clinic/utils/filter.utils.ts +446 -446
- package/src/services/clinic/utils/index.ts +11 -11
- package/src/services/clinic/utils/photos.utils.ts +188 -188
- package/src/services/clinic/utils/search.utils.ts +84 -84
- package/src/services/clinic/utils/tag.utils.ts +124 -124
- package/src/services/documentation-templates/documentation-template.service.ts +537 -537
- package/src/services/documentation-templates/filled-document.service.ts +587 -587
- package/src/services/documentation-templates/index.ts +2 -2
- package/src/services/index.ts +13 -13
- package/src/services/media/index.ts +1 -1
- package/src/services/media/media.service.ts +418 -418
- package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/notification.service.ts +215 -215
- package/src/services/patient/README.md +48 -48
- package/src/services/patient/To-Do.md +43 -43
- package/src/services/patient/__tests__/patient.service.test.ts +294 -294
- package/src/services/patient/index.ts +2 -2
- package/src/services/patient/patient.service.ts +883 -883
- package/src/services/patient/patientRequirements.service.ts +285 -285
- package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
- package/src/services/patient/utils/clinic.utils.ts +80 -80
- package/src/services/patient/utils/docs.utils.ts +142 -142
- package/src/services/patient/utils/index.ts +9 -9
- package/src/services/patient/utils/location.utils.ts +126 -126
- package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
- package/src/services/patient/utils/medical.utils.ts +458 -458
- package/src/services/patient/utils/practitioner.utils.ts +260 -260
- package/src/services/patient/utils/profile.utils.ts +510 -510
- package/src/services/patient/utils/sensitive.utils.ts +260 -260
- package/src/services/patient/utils/token.utils.ts +211 -211
- package/src/services/practitioner/README.md +145 -145
- package/src/services/practitioner/index.ts +1 -1
- package/src/services/practitioner/practitioner.service.ts +1742 -1742
- package/src/services/procedure/README.md +163 -163
- package/src/services/procedure/index.ts +1 -1
- package/src/services/procedure/procedure.service.ts +1715 -1682
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +683 -636
- package/src/services/user/index.ts +1 -1
- package/src/services/user/user.service.ts +489 -489
- package/src/services/user/user.v2.service.ts +466 -466
- package/src/types/appointment/index.ts +480 -480
- package/src/types/calendar/index.ts +258 -258
- package/src/types/calendar/synced-calendar.types.ts +66 -66
- package/src/types/clinic/index.ts +489 -489
- package/src/types/clinic/practitioner-invite.types.ts +91 -91
- package/src/types/clinic/preferences.types.ts +159 -159
- package/src/types/clinic/to-do +3 -3
- package/src/types/documentation-templates/index.ts +308 -308
- package/src/types/index.ts +44 -44
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +265 -265
- package/src/types/patient/aesthetic-analysis.types.ts +66 -66
- package/src/types/patient/allergies.ts +58 -58
- package/src/types/patient/index.ts +275 -275
- package/src/types/patient/medical-info.types.ts +152 -152
- package/src/types/patient/patient-requirements.ts +92 -92
- package/src/types/patient/token.types.ts +61 -61
- package/src/types/practitioner/index.ts +206 -206
- package/src/types/procedure/index.ts +181 -181
- package/src/types/profile/index.ts +39 -39
- package/src/types/reviews/index.ts +132 -130
- package/src/types/tz-lookup.d.ts +4 -4
- package/src/types/user/index.ts +38 -38
- package/src/utils/TIMESTAMPS.md +176 -176
- package/src/utils/TimestampUtils.ts +241 -241
- package/src/utils/index.ts +1 -1
- package/src/validations/appointment.schema.ts +574 -574
- package/src/validations/calendar.schema.ts +225 -225
- package/src/validations/clinic.schema.ts +493 -493
- package/src/validations/common.schema.ts +25 -25
- package/src/validations/documentation-templates/index.ts +1 -1
- package/src/validations/documentation-templates/template.schema.ts +220 -220
- package/src/validations/documentation-templates.schema.ts +10 -10
- package/src/validations/index.ts +20 -20
- package/src/validations/media.schema.ts +10 -10
- package/src/validations/notification.schema.ts +90 -90
- package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
- package/src/validations/patient/medical-info.schema.ts +125 -125
- package/src/validations/patient/patient-requirements.schema.ts +84 -84
- package/src/validations/patient/token.schema.ts +29 -29
- package/src/validations/patient.schema.ts +217 -217
- package/src/validations/practitioner.schema.ts +222 -222
- package/src/validations/procedure-product.schema.ts +41 -41
- package/src/validations/procedure.schema.ts +124 -124
- package/src/validations/profile-info.schema.ts +41 -41
- package/src/validations/reviews.schema.ts +195 -189
- package/src/validations/schemas.ts +104 -104
- package/src/validations/shared.schema.ts +78 -78
|
@@ -1,641 +1,689 @@
|
|
|
1
|
-
import * as admin from "firebase-admin";
|
|
2
|
-
import {
|
|
3
|
-
Review,
|
|
4
|
-
ClinicReview,
|
|
5
|
-
PractitionerReview,
|
|
6
|
-
ProcedureReview,
|
|
7
|
-
REVIEWS_COLLECTION,
|
|
8
|
-
ClinicReviewInfo,
|
|
9
|
-
PractitionerReviewInfo,
|
|
10
|
-
ProcedureReviewInfo,
|
|
11
|
-
} from "../../../types/reviews";
|
|
12
|
-
import { CLINICS_COLLECTION } from "../../../types/clinic";
|
|
13
|
-
import { PRACTITIONERS_COLLECTION } from "../../../types/practitioner";
|
|
14
|
-
import { PROCEDURES_COLLECTION } from "../../../types/procedure";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @class ReviewsAggregationService
|
|
18
|
-
* @description Handles aggregation tasks related to review data updates.
|
|
19
|
-
* This service is intended to be used primarily by background functions (e.g., Cloud Functions)
|
|
20
|
-
* triggered by changes in the reviews collection.
|
|
21
|
-
*/
|
|
22
|
-
export class ReviewsAggregationService {
|
|
23
|
-
private db: admin.firestore.Firestore;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Constructor for ReviewsAggregationService.
|
|
27
|
-
* @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
|
|
28
|
-
*/
|
|
29
|
-
constructor(firestore?: admin.firestore.Firestore) {
|
|
30
|
-
this.db = firestore || admin.firestore();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Process a newly created review and update all related entities
|
|
35
|
-
* @param review The newly created review
|
|
36
|
-
* @returns Promise resolving when all updates are complete
|
|
37
|
-
*/
|
|
38
|
-
async processNewReview(review: Review): Promise<void> {
|
|
39
|
-
console.log(
|
|
40
|
-
`[ReviewsAggregationService] Processing new review: ${review.id}`
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const updatePromises: Promise<any>[] = [];
|
|
44
|
-
|
|
45
|
-
// Update clinic if clinic review exists
|
|
46
|
-
if (review.clinicReview) {
|
|
47
|
-
updatePromises.push(
|
|
48
|
-
this.updateClinicReviewInfo(review.clinicReview.clinicId)
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Update practitioner if practitioner review exists
|
|
53
|
-
if (review.practitionerReview) {
|
|
54
|
-
updatePromises.push(
|
|
55
|
-
this.updatePractitionerReviewInfo(
|
|
56
|
-
review.practitionerReview.practitionerId
|
|
57
|
-
)
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Update procedure if procedure review exists
|
|
62
|
-
if (review.procedureReview) {
|
|
63
|
-
updatePromises.push(
|
|
64
|
-
this.updateProcedureReviewInfo(review.procedureReview.procedureId)
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
review.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
review.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
.
|
|
374
|
-
.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
.
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
)
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
.
|
|
574
|
-
.
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1
|
+
import * as admin from "firebase-admin";
|
|
2
|
+
import {
|
|
3
|
+
Review,
|
|
4
|
+
ClinicReview,
|
|
5
|
+
PractitionerReview,
|
|
6
|
+
ProcedureReview,
|
|
7
|
+
REVIEWS_COLLECTION,
|
|
8
|
+
ClinicReviewInfo,
|
|
9
|
+
PractitionerReviewInfo,
|
|
10
|
+
ProcedureReviewInfo,
|
|
11
|
+
} from "../../../types/reviews";
|
|
12
|
+
import { CLINICS_COLLECTION } from "../../../types/clinic";
|
|
13
|
+
import { PRACTITIONERS_COLLECTION } from "../../../types/practitioner";
|
|
14
|
+
import { PROCEDURES_COLLECTION } from "../../../types/procedure";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @class ReviewsAggregationService
|
|
18
|
+
* @description Handles aggregation tasks related to review data updates.
|
|
19
|
+
* This service is intended to be used primarily by background functions (e.g., Cloud Functions)
|
|
20
|
+
* triggered by changes in the reviews collection.
|
|
21
|
+
*/
|
|
22
|
+
export class ReviewsAggregationService {
|
|
23
|
+
private db: admin.firestore.Firestore;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Constructor for ReviewsAggregationService.
|
|
27
|
+
* @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
|
|
28
|
+
*/
|
|
29
|
+
constructor(firestore?: admin.firestore.Firestore) {
|
|
30
|
+
this.db = firestore || admin.firestore();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Process a newly created review and update all related entities
|
|
35
|
+
* @param review The newly created review
|
|
36
|
+
* @returns Promise resolving when all updates are complete
|
|
37
|
+
*/
|
|
38
|
+
async processNewReview(review: Review): Promise<void> {
|
|
39
|
+
console.log(
|
|
40
|
+
`[ReviewsAggregationService] Processing new review: ${review.id}`
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const updatePromises: Promise<any>[] = [];
|
|
44
|
+
|
|
45
|
+
// Update clinic if clinic review exists
|
|
46
|
+
if (review.clinicReview) {
|
|
47
|
+
updatePromises.push(
|
|
48
|
+
this.updateClinicReviewInfo(review.clinicReview.clinicId)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Update practitioner if practitioner review exists
|
|
53
|
+
if (review.practitionerReview) {
|
|
54
|
+
updatePromises.push(
|
|
55
|
+
this.updatePractitionerReviewInfo(
|
|
56
|
+
review.practitionerReview.practitionerId
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Update procedure if procedure review exists
|
|
62
|
+
if (review.procedureReview) {
|
|
63
|
+
updatePromises.push(
|
|
64
|
+
this.updateProcedureReviewInfo(review.procedureReview.procedureId)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Update extended procedures if they exist
|
|
69
|
+
if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
|
|
70
|
+
console.log(
|
|
71
|
+
`[ReviewsAggregationService] Processing ${review.extendedProcedureReviews.length} extended procedure reviews`
|
|
72
|
+
);
|
|
73
|
+
review.extendedProcedureReviews.forEach((extendedReview) => {
|
|
74
|
+
updatePromises.push(
|
|
75
|
+
this.updateProcedureReviewInfo(extendedReview.procedureId)
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Wait for all updates to complete
|
|
81
|
+
await Promise.all(updatePromises);
|
|
82
|
+
console.log(
|
|
83
|
+
`[ReviewsAggregationService] Successfully processed review: ${review.id}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Process a deleted review and update all related entities
|
|
89
|
+
* @param review The deleted review
|
|
90
|
+
* @returns Promise resolving when all updates are complete
|
|
91
|
+
*/
|
|
92
|
+
async processDeletedReview(review: Review): Promise<void> {
|
|
93
|
+
console.log(
|
|
94
|
+
`[ReviewsAggregationService] Processing deleted review: ${review.id}`
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const updatePromises: Promise<any>[] = [];
|
|
98
|
+
|
|
99
|
+
// Update clinic if clinic review exists
|
|
100
|
+
if (review.clinicReview) {
|
|
101
|
+
updatePromises.push(
|
|
102
|
+
this.updateClinicReviewInfo(
|
|
103
|
+
review.clinicReview.clinicId,
|
|
104
|
+
review.clinicReview,
|
|
105
|
+
true
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Update practitioner if practitioner review exists
|
|
111
|
+
if (review.practitionerReview) {
|
|
112
|
+
updatePromises.push(
|
|
113
|
+
this.updatePractitionerReviewInfo(
|
|
114
|
+
review.practitionerReview.practitionerId,
|
|
115
|
+
review.practitionerReview,
|
|
116
|
+
true
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Update procedure if procedure review exists
|
|
122
|
+
if (review.procedureReview) {
|
|
123
|
+
updatePromises.push(
|
|
124
|
+
this.updateProcedureReviewInfo(
|
|
125
|
+
review.procedureReview.procedureId,
|
|
126
|
+
review.procedureReview,
|
|
127
|
+
true
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Update extended procedures if they exist
|
|
133
|
+
if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
|
|
134
|
+
console.log(
|
|
135
|
+
`[ReviewsAggregationService] Processing deletion of ${review.extendedProcedureReviews.length} extended procedure reviews`
|
|
136
|
+
);
|
|
137
|
+
review.extendedProcedureReviews.forEach((extendedReview) => {
|
|
138
|
+
updatePromises.push(
|
|
139
|
+
this.updateProcedureReviewInfo(
|
|
140
|
+
extendedReview.procedureId,
|
|
141
|
+
extendedReview,
|
|
142
|
+
true
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Wait for all updates to complete
|
|
149
|
+
await Promise.all(updatePromises);
|
|
150
|
+
console.log(
|
|
151
|
+
`[ReviewsAggregationService] Successfully processed deleted review: ${review.id}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Updates the review info for a clinic
|
|
157
|
+
* @param clinicId The ID of the clinic to update
|
|
158
|
+
* @param removedReview Optional review being removed
|
|
159
|
+
* @param isRemoval Whether this update is for a review removal
|
|
160
|
+
* @returns The updated clinic review info
|
|
161
|
+
*/
|
|
162
|
+
async updateClinicReviewInfo(
|
|
163
|
+
clinicId: string,
|
|
164
|
+
removedReview?: ClinicReview,
|
|
165
|
+
isRemoval: boolean = false
|
|
166
|
+
): Promise<ClinicReviewInfo> {
|
|
167
|
+
console.log(
|
|
168
|
+
`[ReviewsAggregationService] Updating review info for clinic: ${clinicId}`
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Get the current clinic document
|
|
172
|
+
const clinicDoc = await this.db
|
|
173
|
+
.collection(CLINICS_COLLECTION)
|
|
174
|
+
.doc(clinicId)
|
|
175
|
+
.get();
|
|
176
|
+
|
|
177
|
+
if (!clinicDoc.exists) {
|
|
178
|
+
console.error(
|
|
179
|
+
`[ReviewsAggregationService] Clinic with ID ${clinicId} not found`
|
|
180
|
+
);
|
|
181
|
+
throw new Error(`Clinic with ID ${clinicId} not found`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const clinicData = clinicDoc.data();
|
|
185
|
+
const currentReviewInfo = (clinicData?.reviewInfo as ClinicReviewInfo) || {
|
|
186
|
+
totalReviews: 0,
|
|
187
|
+
averageRating: 0,
|
|
188
|
+
cleanliness: 0,
|
|
189
|
+
facilities: 0,
|
|
190
|
+
staffFriendliness: 0,
|
|
191
|
+
waitingTime: 0,
|
|
192
|
+
accessibility: 0,
|
|
193
|
+
recommendationPercentage: 0,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Get all reviews for this clinic
|
|
197
|
+
const reviewsQuery = await this.db
|
|
198
|
+
.collection(REVIEWS_COLLECTION)
|
|
199
|
+
.where("clinicReview.clinicId", "==", clinicId)
|
|
200
|
+
.get();
|
|
201
|
+
|
|
202
|
+
// If we're removing the last review or there are no reviews, set default values
|
|
203
|
+
if ((isRemoval && reviewsQuery.size <= 1) || reviewsQuery.empty) {
|
|
204
|
+
const updatedReviewInfo: ClinicReviewInfo = {
|
|
205
|
+
totalReviews: 0,
|
|
206
|
+
averageRating: 0,
|
|
207
|
+
cleanliness: 0,
|
|
208
|
+
facilities: 0,
|
|
209
|
+
staffFriendliness: 0,
|
|
210
|
+
waitingTime: 0,
|
|
211
|
+
accessibility: 0,
|
|
212
|
+
recommendationPercentage: 0,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
await this.db.collection(CLINICS_COLLECTION).doc(clinicId).update({
|
|
216
|
+
reviewInfo: updatedReviewInfo,
|
|
217
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
console.log(
|
|
221
|
+
`[ReviewsAggregationService] Reset review info for clinic: ${clinicId}`
|
|
222
|
+
);
|
|
223
|
+
return updatedReviewInfo;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Calculate new averages from all reviews
|
|
227
|
+
const reviews = reviewsQuery.docs.map((doc) => doc.data() as Review);
|
|
228
|
+
const clinicReviews = reviews
|
|
229
|
+
.map((review) => review.clinicReview)
|
|
230
|
+
.filter((review): review is ClinicReview => review !== undefined);
|
|
231
|
+
|
|
232
|
+
// Calculate averages
|
|
233
|
+
let totalRating = 0;
|
|
234
|
+
let totalCleanliness = 0;
|
|
235
|
+
let totalFacilities = 0;
|
|
236
|
+
let totalStaffFriendliness = 0;
|
|
237
|
+
let totalWaitingTime = 0;
|
|
238
|
+
let totalAccessibility = 0;
|
|
239
|
+
let totalRecommendations = 0;
|
|
240
|
+
|
|
241
|
+
clinicReviews.forEach((review) => {
|
|
242
|
+
totalRating += review.overallRating;
|
|
243
|
+
totalCleanliness += review.cleanliness;
|
|
244
|
+
totalFacilities += review.facilities;
|
|
245
|
+
totalStaffFriendliness += review.staffFriendliness;
|
|
246
|
+
totalWaitingTime += review.waitingTime;
|
|
247
|
+
totalAccessibility += review.accessibility;
|
|
248
|
+
if (review.wouldRecommend) totalRecommendations++;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const count = clinicReviews.length;
|
|
252
|
+
const roundToOneDecimal = (value: number): number =>
|
|
253
|
+
Math.round((value / count) * 10) / 10;
|
|
254
|
+
|
|
255
|
+
const updatedReviewInfo: ClinicReviewInfo = {
|
|
256
|
+
totalReviews: count,
|
|
257
|
+
averageRating: roundToOneDecimal(totalRating),
|
|
258
|
+
cleanliness: roundToOneDecimal(totalCleanliness),
|
|
259
|
+
facilities: roundToOneDecimal(totalFacilities),
|
|
260
|
+
staffFriendliness: roundToOneDecimal(totalStaffFriendliness),
|
|
261
|
+
waitingTime: roundToOneDecimal(totalWaitingTime),
|
|
262
|
+
accessibility: roundToOneDecimal(totalAccessibility),
|
|
263
|
+
recommendationPercentage:
|
|
264
|
+
Math.round((totalRecommendations / count) * 1000) / 10,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Update the clinic with the new review info
|
|
268
|
+
await this.db.collection(CLINICS_COLLECTION).doc(clinicId).update({
|
|
269
|
+
reviewInfo: updatedReviewInfo,
|
|
270
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
console.log(
|
|
274
|
+
`[ReviewsAggregationService] Updated review info for clinic: ${clinicId}`
|
|
275
|
+
);
|
|
276
|
+
return updatedReviewInfo;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Updates the review info for a practitioner
|
|
281
|
+
* @param practitionerId The ID of the practitioner to update
|
|
282
|
+
* @param removedReview Optional review being removed
|
|
283
|
+
* @param isRemoval Whether this update is for a review removal
|
|
284
|
+
* @returns The updated practitioner review info
|
|
285
|
+
*/
|
|
286
|
+
async updatePractitionerReviewInfo(
|
|
287
|
+
practitionerId: string,
|
|
288
|
+
removedReview?: PractitionerReview,
|
|
289
|
+
isRemoval: boolean = false
|
|
290
|
+
): Promise<PractitionerReviewInfo> {
|
|
291
|
+
console.log(
|
|
292
|
+
`[ReviewsAggregationService] Updating review info for practitioner: ${practitionerId}`
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Get the current practitioner document
|
|
296
|
+
const practitionerDoc = await this.db
|
|
297
|
+
.collection(PRACTITIONERS_COLLECTION)
|
|
298
|
+
.doc(practitionerId)
|
|
299
|
+
.get();
|
|
300
|
+
|
|
301
|
+
if (!practitionerDoc.exists) {
|
|
302
|
+
console.error(
|
|
303
|
+
`[ReviewsAggregationService] Practitioner with ID ${practitionerId} not found`
|
|
304
|
+
);
|
|
305
|
+
throw new Error(`Practitioner with ID ${practitionerId} not found`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const practitionerData = practitionerDoc.data();
|
|
309
|
+
const currentReviewInfo =
|
|
310
|
+
(practitionerData?.reviewInfo as PractitionerReviewInfo) || {
|
|
311
|
+
totalReviews: 0,
|
|
312
|
+
averageRating: 0,
|
|
313
|
+
knowledgeAndExpertise: 0,
|
|
314
|
+
communicationSkills: 0,
|
|
315
|
+
bedSideManner: 0,
|
|
316
|
+
thoroughness: 0,
|
|
317
|
+
trustworthiness: 0,
|
|
318
|
+
recommendationPercentage: 0,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Get all reviews for this practitioner
|
|
322
|
+
const reviewsQuery = await this.db
|
|
323
|
+
.collection(REVIEWS_COLLECTION)
|
|
324
|
+
.where("practitionerReview.practitionerId", "==", practitionerId)
|
|
325
|
+
.get();
|
|
326
|
+
|
|
327
|
+
// If we're removing the last review or there are no reviews, set default values
|
|
328
|
+
if ((isRemoval && reviewsQuery.size <= 1) || reviewsQuery.empty) {
|
|
329
|
+
const updatedReviewInfo: PractitionerReviewInfo = {
|
|
330
|
+
totalReviews: 0,
|
|
331
|
+
averageRating: 0,
|
|
332
|
+
knowledgeAndExpertise: 0,
|
|
333
|
+
communicationSkills: 0,
|
|
334
|
+
bedSideManner: 0,
|
|
335
|
+
thoroughness: 0,
|
|
336
|
+
trustworthiness: 0,
|
|
337
|
+
recommendationPercentage: 0,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
await this.db
|
|
341
|
+
.collection(PRACTITIONERS_COLLECTION)
|
|
342
|
+
.doc(practitionerId)
|
|
343
|
+
.update({
|
|
344
|
+
reviewInfo: updatedReviewInfo,
|
|
345
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Also update doctor info in procedures with the new rating
|
|
349
|
+
await this.updateDoctorInfoInProcedures(practitionerId, 0);
|
|
350
|
+
|
|
351
|
+
console.log(
|
|
352
|
+
`[ReviewsAggregationService] Reset review info for practitioner: ${practitionerId}`
|
|
353
|
+
);
|
|
354
|
+
return updatedReviewInfo;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Calculate new averages from all reviews
|
|
358
|
+
const reviews = reviewsQuery.docs.map((doc) => doc.data() as Review);
|
|
359
|
+
const practitionerReviews = reviews
|
|
360
|
+
.map((review) => review.practitionerReview)
|
|
361
|
+
.filter((review): review is PractitionerReview => review !== undefined);
|
|
362
|
+
|
|
363
|
+
// Calculate averages
|
|
364
|
+
let totalRating = 0;
|
|
365
|
+
let totalKnowledgeAndExpertise = 0;
|
|
366
|
+
let totalCommunicationSkills = 0;
|
|
367
|
+
let totalBedSideManner = 0;
|
|
368
|
+
let totalThoroughness = 0;
|
|
369
|
+
let totalTrustworthiness = 0;
|
|
370
|
+
let totalRecommendations = 0;
|
|
371
|
+
|
|
372
|
+
practitionerReviews.forEach((review) => {
|
|
373
|
+
totalRating += review.overallRating;
|
|
374
|
+
totalKnowledgeAndExpertise += review.knowledgeAndExpertise;
|
|
375
|
+
totalCommunicationSkills += review.communicationSkills;
|
|
376
|
+
totalBedSideManner += review.bedSideManner;
|
|
377
|
+
totalThoroughness += review.thoroughness;
|
|
378
|
+
totalTrustworthiness += review.trustworthiness;
|
|
379
|
+
if (review.wouldRecommend) totalRecommendations++;
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const count = practitionerReviews.length;
|
|
383
|
+
const roundToOneDecimal = (value: number): number =>
|
|
384
|
+
Math.round((value / count) * 10) / 10;
|
|
385
|
+
|
|
386
|
+
const updatedReviewInfo: PractitionerReviewInfo = {
|
|
387
|
+
totalReviews: count,
|
|
388
|
+
averageRating: roundToOneDecimal(totalRating),
|
|
389
|
+
knowledgeAndExpertise: roundToOneDecimal(totalKnowledgeAndExpertise),
|
|
390
|
+
communicationSkills: roundToOneDecimal(totalCommunicationSkills),
|
|
391
|
+
bedSideManner: roundToOneDecimal(totalBedSideManner),
|
|
392
|
+
thoroughness: roundToOneDecimal(totalThoroughness),
|
|
393
|
+
trustworthiness: roundToOneDecimal(totalTrustworthiness),
|
|
394
|
+
recommendationPercentage:
|
|
395
|
+
Math.round((totalRecommendations / count) * 1000) / 10,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// Update the practitioner with the new review info
|
|
399
|
+
await this.db
|
|
400
|
+
.collection(PRACTITIONERS_COLLECTION)
|
|
401
|
+
.doc(practitionerId)
|
|
402
|
+
.update({
|
|
403
|
+
reviewInfo: updatedReviewInfo,
|
|
404
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Also update doctor info in procedures with the new rating
|
|
408
|
+
await this.updateDoctorInfoInProcedures(
|
|
409
|
+
practitionerId,
|
|
410
|
+
updatedReviewInfo.averageRating
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
console.log(
|
|
414
|
+
`[ReviewsAggregationService] Updated review info for practitioner: ${practitionerId}`
|
|
415
|
+
);
|
|
416
|
+
return updatedReviewInfo;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Updates the review info for a procedure
|
|
421
|
+
* @param procedureId The ID of the procedure to update
|
|
422
|
+
* @param removedReview Optional review being removed
|
|
423
|
+
* @param isRemoval Whether this update is for a review removal
|
|
424
|
+
* @returns The updated procedure review info
|
|
425
|
+
*/
|
|
426
|
+
async updateProcedureReviewInfo(
|
|
427
|
+
procedureId: string,
|
|
428
|
+
removedReview?: ProcedureReview,
|
|
429
|
+
isRemoval: boolean = false
|
|
430
|
+
): Promise<ProcedureReviewInfo> {
|
|
431
|
+
console.log(
|
|
432
|
+
`[ReviewsAggregationService] Updating review info for procedure: ${procedureId}`
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// Get the current procedure document
|
|
436
|
+
const procedureDoc = await this.db
|
|
437
|
+
.collection(PROCEDURES_COLLECTION)
|
|
438
|
+
.doc(procedureId)
|
|
439
|
+
.get();
|
|
440
|
+
|
|
441
|
+
if (!procedureDoc.exists) {
|
|
442
|
+
console.error(
|
|
443
|
+
`[ReviewsAggregationService] Procedure with ID ${procedureId} not found`
|
|
444
|
+
);
|
|
445
|
+
throw new Error(`Procedure with ID ${procedureId} not found`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const procedureData = procedureDoc.data();
|
|
449
|
+
const currentReviewInfo =
|
|
450
|
+
(procedureData?.reviewInfo as ProcedureReviewInfo) || {
|
|
451
|
+
totalReviews: 0,
|
|
452
|
+
averageRating: 0,
|
|
453
|
+
effectivenessOfTreatment: 0,
|
|
454
|
+
outcomeExplanation: 0,
|
|
455
|
+
painManagement: 0,
|
|
456
|
+
followUpCare: 0,
|
|
457
|
+
valueForMoney: 0,
|
|
458
|
+
recommendationPercentage: 0,
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Get all reviews for this procedure (both main and extended)
|
|
462
|
+
// We need to check both procedureReview.procedureId and extendedProcedureReviews array
|
|
463
|
+
const allReviewsQuery = await this.db
|
|
464
|
+
.collection(REVIEWS_COLLECTION)
|
|
465
|
+
.get();
|
|
466
|
+
|
|
467
|
+
// Filter reviews that contain this procedure (either as main or extended)
|
|
468
|
+
const reviews = allReviewsQuery.docs.map((doc) => doc.data() as Review);
|
|
469
|
+
const procedureReviews: ProcedureReview[] = [];
|
|
470
|
+
|
|
471
|
+
reviews.forEach((review) => {
|
|
472
|
+
// Check if this is the main procedure
|
|
473
|
+
if (review.procedureReview && review.procedureReview.procedureId === procedureId) {
|
|
474
|
+
procedureReviews.push(review.procedureReview);
|
|
475
|
+
}
|
|
476
|
+
// Check if this is in extended procedures
|
|
477
|
+
if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
|
|
478
|
+
const matchingExtended = review.extendedProcedureReviews.filter(
|
|
479
|
+
(extReview) => extReview.procedureId === procedureId
|
|
480
|
+
);
|
|
481
|
+
procedureReviews.push(...matchingExtended);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// If there are no reviews, set default values
|
|
486
|
+
if (procedureReviews.length === 0) {
|
|
487
|
+
const updatedReviewInfo: ProcedureReviewInfo = {
|
|
488
|
+
totalReviews: 0,
|
|
489
|
+
averageRating: 0,
|
|
490
|
+
effectivenessOfTreatment: 0,
|
|
491
|
+
outcomeExplanation: 0,
|
|
492
|
+
painManagement: 0,
|
|
493
|
+
followUpCare: 0,
|
|
494
|
+
valueForMoney: 0,
|
|
495
|
+
recommendationPercentage: 0,
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
await this.db.collection(PROCEDURES_COLLECTION).doc(procedureId).update({
|
|
499
|
+
reviewInfo: updatedReviewInfo,
|
|
500
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
console.log(
|
|
504
|
+
`[ReviewsAggregationService] Reset review info for procedure: ${procedureId}`
|
|
505
|
+
);
|
|
506
|
+
return updatedReviewInfo;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Calculate averages
|
|
510
|
+
let totalRating = 0;
|
|
511
|
+
let totalEffectivenessOfTreatment = 0;
|
|
512
|
+
let totalOutcomeExplanation = 0;
|
|
513
|
+
let totalPainManagement = 0;
|
|
514
|
+
let totalFollowUpCare = 0;
|
|
515
|
+
let totalValueForMoney = 0;
|
|
516
|
+
let totalRecommendations = 0;
|
|
517
|
+
|
|
518
|
+
procedureReviews.forEach((review) => {
|
|
519
|
+
totalRating += review.overallRating;
|
|
520
|
+
totalEffectivenessOfTreatment += review.effectivenessOfTreatment;
|
|
521
|
+
totalOutcomeExplanation += review.outcomeExplanation;
|
|
522
|
+
totalPainManagement += review.painManagement;
|
|
523
|
+
totalFollowUpCare += review.followUpCare;
|
|
524
|
+
totalValueForMoney += review.valueForMoney;
|
|
525
|
+
if (review.wouldRecommend) totalRecommendations++;
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
const count = procedureReviews.length;
|
|
529
|
+
const roundToOneDecimal = (value: number): number =>
|
|
530
|
+
Math.round((value / count) * 10) / 10;
|
|
531
|
+
|
|
532
|
+
const updatedReviewInfo: ProcedureReviewInfo = {
|
|
533
|
+
totalReviews: count,
|
|
534
|
+
averageRating: roundToOneDecimal(totalRating),
|
|
535
|
+
effectivenessOfTreatment: roundToOneDecimal(
|
|
536
|
+
totalEffectivenessOfTreatment
|
|
537
|
+
),
|
|
538
|
+
outcomeExplanation: roundToOneDecimal(totalOutcomeExplanation),
|
|
539
|
+
painManagement: roundToOneDecimal(totalPainManagement),
|
|
540
|
+
followUpCare: roundToOneDecimal(totalFollowUpCare),
|
|
541
|
+
valueForMoney: roundToOneDecimal(totalValueForMoney),
|
|
542
|
+
recommendationPercentage:
|
|
543
|
+
Math.round((totalRecommendations / count) * 1000) / 10,
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
// Update the procedure with the new review info
|
|
547
|
+
await this.db.collection(PROCEDURES_COLLECTION).doc(procedureId).update({
|
|
548
|
+
reviewInfo: updatedReviewInfo,
|
|
549
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
console.log(
|
|
553
|
+
`[ReviewsAggregationService] Updated review info for procedure: ${procedureId}`
|
|
554
|
+
);
|
|
555
|
+
return updatedReviewInfo;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Updates doctorInfo rating in all procedures for a practitioner
|
|
560
|
+
* @param practitionerId The ID of the practitioner
|
|
561
|
+
* @param rating The new rating to set
|
|
562
|
+
*/
|
|
563
|
+
private async updateDoctorInfoInProcedures(
|
|
564
|
+
practitionerId: string,
|
|
565
|
+
rating: number
|
|
566
|
+
): Promise<void> {
|
|
567
|
+
console.log(
|
|
568
|
+
`[ReviewsAggregationService] Updating doctor info in procedures for practitioner: ${practitionerId}`
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
// Find all procedures for this practitioner
|
|
572
|
+
const proceduresQuery = await this.db
|
|
573
|
+
.collection(PROCEDURES_COLLECTION)
|
|
574
|
+
.where("practitionerId", "==", practitionerId)
|
|
575
|
+
.get();
|
|
576
|
+
|
|
577
|
+
if (proceduresQuery.empty) {
|
|
578
|
+
console.log(
|
|
579
|
+
`[ReviewsAggregationService] No procedures found for practitioner: ${practitionerId}`
|
|
580
|
+
);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Batch update all procedures
|
|
585
|
+
const batch = this.db.batch();
|
|
586
|
+
|
|
587
|
+
proceduresQuery.docs.forEach((docSnapshot) => {
|
|
588
|
+
const procedureRef = this.db
|
|
589
|
+
.collection(PROCEDURES_COLLECTION)
|
|
590
|
+
.doc(docSnapshot.id);
|
|
591
|
+
batch.update(procedureRef, {
|
|
592
|
+
"doctorInfo.rating": rating,
|
|
593
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
await batch.commit();
|
|
598
|
+
console.log(
|
|
599
|
+
`[ReviewsAggregationService] Updated doctor info in ${proceduresQuery.size} procedures for practitioner: ${practitionerId}`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Verifies a review as checked by admin/staff
|
|
605
|
+
* @param reviewId The ID of the review to verify
|
|
606
|
+
*/
|
|
607
|
+
async verifyReview(reviewId: string): Promise<void> {
|
|
608
|
+
console.log(`[ReviewsAggregationService] Verifying review: ${reviewId}`);
|
|
609
|
+
|
|
610
|
+
// Get the review
|
|
611
|
+
const reviewDoc = await this.db
|
|
612
|
+
.collection(REVIEWS_COLLECTION)
|
|
613
|
+
.doc(reviewId)
|
|
614
|
+
.get();
|
|
615
|
+
|
|
616
|
+
if (!reviewDoc.exists) {
|
|
617
|
+
console.error(
|
|
618
|
+
`[ReviewsAggregationService] Review with ID ${reviewId} not found`
|
|
619
|
+
);
|
|
620
|
+
throw new Error(`Review with ID ${reviewId} not found`);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const review = reviewDoc.data() as Review;
|
|
624
|
+
const batch = this.db.batch();
|
|
625
|
+
const reviewRef = this.db.collection(REVIEWS_COLLECTION).doc(reviewId);
|
|
626
|
+
|
|
627
|
+
// Update clinic review if it exists
|
|
628
|
+
if (review.clinicReview) {
|
|
629
|
+
review.clinicReview.isVerified = true;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Update practitioner review if it exists
|
|
633
|
+
if (review.practitionerReview) {
|
|
634
|
+
review.practitionerReview.isVerified = true;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Update procedure review if it exists
|
|
638
|
+
if (review.procedureReview) {
|
|
639
|
+
review.procedureReview.isVerified = true;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Update extended procedure reviews if they exist
|
|
643
|
+
if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
|
|
644
|
+
review.extendedProcedureReviews.forEach((extReview) => {
|
|
645
|
+
extReview.isVerified = true;
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Update the review
|
|
650
|
+
batch.update(reviewRef, {
|
|
651
|
+
clinicReview: review.clinicReview,
|
|
652
|
+
practitionerReview: review.practitionerReview,
|
|
653
|
+
procedureReview: review.procedureReview,
|
|
654
|
+
extendedProcedureReviews: review.extendedProcedureReviews,
|
|
655
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
await batch.commit();
|
|
659
|
+
console.log(
|
|
660
|
+
`[ReviewsAggregationService] Successfully verified review: ${reviewId}`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Calculate the average of all reviews for an entity
|
|
666
|
+
* @param entityId The entity ID
|
|
667
|
+
* @param entityType The type of entity ('clinic', 'practitioner', or 'procedure')
|
|
668
|
+
* @returns Promise that resolves to the calculated review info
|
|
669
|
+
*/
|
|
670
|
+
async calculateEntityReviewInfo(
|
|
671
|
+
entityId: string,
|
|
672
|
+
entityType: "clinic" | "practitioner" | "procedure"
|
|
673
|
+
): Promise<ClinicReviewInfo | PractitionerReviewInfo | ProcedureReviewInfo> {
|
|
674
|
+
console.log(
|
|
675
|
+
`[ReviewsAggregationService] Calculating review info for ${entityType}: ${entityId}`
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
switch (entityType) {
|
|
679
|
+
case "clinic":
|
|
680
|
+
return this.updateClinicReviewInfo(entityId);
|
|
681
|
+
case "practitioner":
|
|
682
|
+
return this.updatePractitionerReviewInfo(entityId);
|
|
683
|
+
case "procedure":
|
|
684
|
+
return this.updateProcedureReviewInfo(entityId);
|
|
685
|
+
default:
|
|
686
|
+
throw new Error(`Invalid entity type: ${entityType}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|