@blackcode_sa/metaestetics-api 1.13.5 → 1.13.8
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 +20 -1
- package/dist/admin/index.d.ts +20 -1
- package/dist/admin/index.js +217 -1
- package/dist/admin/index.mjs +217 -1
- package/dist/index.d.mts +26 -3
- package/dist/index.d.ts +26 -3
- package/dist/index.js +168 -6
- package/dist/index.mjs +168 -6
- package/package.json +121 -121
- 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 +1984 -1984
- 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 +966 -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 -689
- package/src/admin/analytics/analytics.admin.service.ts +278 -278
- package/src/admin/analytics/index.ts +2 -2
- 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 +81 -81
- 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 +57 -57
- package/src/backoffice/services/analytics.service.proposal.md +863 -863
- package/src/backoffice/services/analytics.service.summary.md +143 -143
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +384 -384
- 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 +461 -461
- package/src/backoffice/services/technology.service.ts +1151 -1151
- 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 +67 -67
- 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 +168 -168
- 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 +211 -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/analytics/ARCHITECTURE.md +199 -199
- package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
- package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
- package/src/services/analytics/QUICK_START.md +393 -393
- package/src/services/analytics/README.md +304 -304
- package/src/services/analytics/SUMMARY.md +141 -141
- package/src/services/analytics/TRENDS.md +380 -380
- package/src/services/analytics/USAGE_GUIDE.md +518 -518
- package/src/services/analytics/analytics-cloud.service.ts +222 -222
- package/src/services/analytics/analytics.service.ts +2142 -2142
- package/src/services/analytics/index.ts +4 -4
- package/src/services/analytics/review-analytics.service.ts +941 -941
- package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
- package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
- package/src/services/analytics/utils/grouping.utils.ts +434 -434
- package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
- package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
- package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2558 -2558
- 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 +1043 -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 +14 -14
- 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 +1799 -1742
- package/src/services/procedure/README.md +163 -163
- package/src/services/procedure/index.ts +1 -1
- package/src/services/procedure/procedure.service.ts +2307 -2200
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +734 -734
- 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/analytics/analytics.types.ts +597 -597
- package/src/types/analytics/grouped-analytics.types.ts +173 -173
- package/src/types/analytics/index.ts +4 -4
- package/src/types/analytics/stored-analytics.types.ts +137 -137
- 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 +498 -498
- 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 +47 -47
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +286 -286
- 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 -132
- 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 +494 -494
- 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 -195
- package/src/validations/schemas.ts +104 -104
- package/src/validations/shared.schema.ts +78 -78
|
@@ -1,260 +1,260 @@
|
|
|
1
|
-
import { getDoc, updateDoc, setDoc, serverTimestamp, Firestore } from 'firebase/firestore';
|
|
2
|
-
import {
|
|
3
|
-
PatientSensitiveInfo,
|
|
4
|
-
CreatePatientSensitiveInfoData,
|
|
5
|
-
UpdatePatientSensitiveInfoData,
|
|
6
|
-
} from '../../../types/patient';
|
|
7
|
-
import { UserRole } from '../../../types';
|
|
8
|
-
import { createPatientSensitiveInfoSchema } from '../../../validations/patient.schema';
|
|
9
|
-
import { z } from 'zod';
|
|
10
|
-
import {
|
|
11
|
-
getSensitiveInfoDocRef,
|
|
12
|
-
initSensitiveInfoDocIfNotExists,
|
|
13
|
-
getPatientDocRef,
|
|
14
|
-
} from './docs.utils';
|
|
15
|
-
import { MediaService, MediaAccessLevel, MediaResource } from '../../media/media.service';
|
|
16
|
-
import { AuthError } from '../../../errors/auth.errors';
|
|
17
|
-
import { getPractitionerProfileByUserRef } from './practitioner.utils';
|
|
18
|
-
import { getClinicAdminByUserRef } from '../../clinic/utils/admin.utils';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Checks if the requester has permission to access/modify sensitive info.
|
|
22
|
-
* Access is granted to the patient owner, or an associated practitioner/clinic admin.
|
|
23
|
-
*/
|
|
24
|
-
const checkSensitiveAccessUtil = async (
|
|
25
|
-
db: Firestore,
|
|
26
|
-
patientId: string,
|
|
27
|
-
requesterId: string,
|
|
28
|
-
requesterRoles: UserRole[],
|
|
29
|
-
): Promise<void> => {
|
|
30
|
-
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
31
|
-
if (!patientDoc.exists()) {
|
|
32
|
-
throw new Error('Patient profile not found');
|
|
33
|
-
}
|
|
34
|
-
const patientData = patientDoc.data() as any; // Cast to any to access properties
|
|
35
|
-
|
|
36
|
-
// 1. Patient is the owner
|
|
37
|
-
if (patientData.userRef && patientData.userRef === requesterId) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// 2. Requester is an associated practitioner
|
|
42
|
-
if (requesterRoles.includes(UserRole.PRACTITIONER)) {
|
|
43
|
-
const practitionerProfile = await getPractitionerProfileByUserRef(db, requesterId);
|
|
44
|
-
if (practitionerProfile && patientData.doctorIds?.includes(practitionerProfile.id)) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// 3. Requester is an associated clinic admin
|
|
50
|
-
if (requesterRoles.includes(UserRole.CLINIC_ADMIN)) {
|
|
51
|
-
const adminProfile = await getClinicAdminByUserRef(db, requesterId);
|
|
52
|
-
if (adminProfile && adminProfile.clinicsManaged) {
|
|
53
|
-
const hasAccess = adminProfile.clinicsManaged.some(managedClinicId =>
|
|
54
|
-
patientData.clinicIds?.includes(managedClinicId),
|
|
55
|
-
);
|
|
56
|
-
if (hasAccess) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
throw new AuthError(
|
|
63
|
-
'Unauthorized access to sensitive information.',
|
|
64
|
-
'AUTH/UNAUTHORIZED_ACCESS',
|
|
65
|
-
403,
|
|
66
|
-
);
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Handles photoUrl upload for sensitive info (supports MediaResource)
|
|
71
|
-
* @param photoUrl - MediaResource (File, Blob, or URL string)
|
|
72
|
-
* @param patientId - ID of the patient
|
|
73
|
-
* @param mediaService - MediaService instance
|
|
74
|
-
* @returns URL string of the uploaded or existing photo
|
|
75
|
-
*/
|
|
76
|
-
const handlePhotoUrlUpload = async (
|
|
77
|
-
photoUrl: MediaResource | undefined | null,
|
|
78
|
-
patientId: string,
|
|
79
|
-
mediaService: MediaService,
|
|
80
|
-
): Promise<string | null> => {
|
|
81
|
-
if (!photoUrl) {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// If it's already a URL string, return it as is
|
|
86
|
-
if (typeof photoUrl === 'string') {
|
|
87
|
-
return photoUrl;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// If it's a File or Blob, upload it
|
|
91
|
-
if (photoUrl instanceof File || photoUrl instanceof Blob) {
|
|
92
|
-
const mediaMetadata = await mediaService.uploadMedia(
|
|
93
|
-
photoUrl,
|
|
94
|
-
patientId, // Using patientId as ownerId
|
|
95
|
-
MediaAccessLevel.PRIVATE, // Sensitive info should be private
|
|
96
|
-
'patient_sensitive_photos',
|
|
97
|
-
photoUrl instanceof File ? photoUrl.name : `sensitive_photo_${patientId}`,
|
|
98
|
-
);
|
|
99
|
-
return mediaMetadata.url;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return null;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
// Funkcije za rad sa osetljivim informacijama
|
|
106
|
-
export const createSensitiveInfoUtil = async (
|
|
107
|
-
db: Firestore,
|
|
108
|
-
data: CreatePatientSensitiveInfoData,
|
|
109
|
-
requesterId: string,
|
|
110
|
-
requesterRoles: UserRole[],
|
|
111
|
-
mediaService?: MediaService,
|
|
112
|
-
): Promise<PatientSensitiveInfo> => {
|
|
113
|
-
try {
|
|
114
|
-
// Security check
|
|
115
|
-
await checkSensitiveAccessUtil(db, data.patientId, requesterId, requesterRoles);
|
|
116
|
-
|
|
117
|
-
const validatedData = createPatientSensitiveInfoSchema.parse(data);
|
|
118
|
-
|
|
119
|
-
// Proveriti da li dokument već postoji
|
|
120
|
-
const sensitiveDoc = await getDoc(getSensitiveInfoDocRef(db, data.patientId));
|
|
121
|
-
if (sensitiveDoc.exists()) {
|
|
122
|
-
throw new Error('Sensitive information already exists for this patient');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Process photoUrl if it's a MediaResource and mediaService is provided
|
|
126
|
-
let processedPhotoUrl: string | null = null;
|
|
127
|
-
if (validatedData.photoUrl && mediaService) {
|
|
128
|
-
processedPhotoUrl = await handlePhotoUrlUpload(
|
|
129
|
-
validatedData.photoUrl,
|
|
130
|
-
data.patientId,
|
|
131
|
-
mediaService,
|
|
132
|
-
);
|
|
133
|
-
} else if (typeof validatedData.photoUrl === 'string') {
|
|
134
|
-
processedPhotoUrl = validatedData.photoUrl;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const sensitiveInfoData = {
|
|
138
|
-
...validatedData,
|
|
139
|
-
photoUrl: processedPhotoUrl,
|
|
140
|
-
createdAt: serverTimestamp(),
|
|
141
|
-
updatedAt: serverTimestamp(),
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
await setDoc(getSensitiveInfoDocRef(db, data.patientId), sensitiveInfoData);
|
|
145
|
-
|
|
146
|
-
const createdDoc = await getDoc(getSensitiveInfoDocRef(db, data.patientId));
|
|
147
|
-
if (!createdDoc.exists()) {
|
|
148
|
-
throw new Error('Failed to create sensitive information');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return createdDoc.data() as PatientSensitiveInfo;
|
|
152
|
-
} catch (error) {
|
|
153
|
-
if (error instanceof z.ZodError) {
|
|
154
|
-
throw new Error('Invalid sensitive info data: ' + error.message);
|
|
155
|
-
}
|
|
156
|
-
throw error;
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
export const getSensitiveInfoUtil = async (
|
|
161
|
-
db: Firestore,
|
|
162
|
-
patientId: string,
|
|
163
|
-
requesterId: string,
|
|
164
|
-
requesterRoles: UserRole[],
|
|
165
|
-
): Promise<PatientSensitiveInfo | null> => {
|
|
166
|
-
// Security check
|
|
167
|
-
await checkSensitiveAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
168
|
-
|
|
169
|
-
// Inicijalizacija dokumenta ako ne postoji
|
|
170
|
-
await initSensitiveInfoDocIfNotExists(db, patientId, requesterId);
|
|
171
|
-
|
|
172
|
-
const sensitiveDoc = await getDoc(getSensitiveInfoDocRef(db, patientId));
|
|
173
|
-
return sensitiveDoc.exists() ? (sensitiveDoc.data() as PatientSensitiveInfo) : null;
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
export const updateSensitiveInfoUtil = async (
|
|
177
|
-
db: Firestore,
|
|
178
|
-
patientId: string,
|
|
179
|
-
data: UpdatePatientSensitiveInfoData,
|
|
180
|
-
requesterId: string,
|
|
181
|
-
requesterRoles: UserRole[],
|
|
182
|
-
mediaService?: MediaService,
|
|
183
|
-
): Promise<PatientSensitiveInfo> => {
|
|
184
|
-
// Security check
|
|
185
|
-
await checkSensitiveAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
186
|
-
|
|
187
|
-
// Inicijalizacija dokumenta ako ne postoji
|
|
188
|
-
await initSensitiveInfoDocIfNotExists(db, patientId, requesterId);
|
|
189
|
-
|
|
190
|
-
// Process photoUrl if it's a MediaResource and mediaService is provided
|
|
191
|
-
let processedPhotoUrl: string | null | undefined = undefined;
|
|
192
|
-
if (data.photoUrl !== undefined) {
|
|
193
|
-
if (mediaService) {
|
|
194
|
-
processedPhotoUrl = await handlePhotoUrlUpload(data.photoUrl, patientId, mediaService);
|
|
195
|
-
} else if (typeof data.photoUrl === 'string' || data.photoUrl === null) {
|
|
196
|
-
processedPhotoUrl = data.photoUrl;
|
|
197
|
-
} else {
|
|
198
|
-
// If photoUrl is a File/Blob but no mediaService provided, throw error
|
|
199
|
-
throw new Error('MediaService required to process photo upload');
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const updateData = {
|
|
204
|
-
...data,
|
|
205
|
-
photoUrl: processedPhotoUrl,
|
|
206
|
-
updatedAt: serverTimestamp(),
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
await updateDoc(getSensitiveInfoDocRef(db, patientId), updateData);
|
|
210
|
-
|
|
211
|
-
const updatedDoc = await getDoc(getSensitiveInfoDocRef(db, patientId));
|
|
212
|
-
if (!updatedDoc.exists()) {
|
|
213
|
-
throw new Error('Failed to retrieve updated sensitive information');
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return updatedDoc.data() as PatientSensitiveInfo;
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
export const claimPatientSensitiveInfoUtil = async (
|
|
220
|
-
db: Firestore,
|
|
221
|
-
patientId: string,
|
|
222
|
-
userId: string,
|
|
223
|
-
): Promise<PatientSensitiveInfo> => {
|
|
224
|
-
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
225
|
-
if (!patientDoc.exists()) {
|
|
226
|
-
throw new Error('Patient profile not found');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const patientData = patientDoc.data() as any;
|
|
230
|
-
|
|
231
|
-
if (!patientData.isManual) {
|
|
232
|
-
throw new Error('Only manually created patient profiles can be claimed');
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (patientData.userRef) {
|
|
236
|
-
throw new Error('Patient profile has already been claimed');
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const sensitiveDoc = await getDoc(getSensitiveInfoDocRef(db, patientId));
|
|
240
|
-
if (!sensitiveDoc.exists()) {
|
|
241
|
-
throw new Error('Patient sensitive information not found');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const sensitiveData = sensitiveDoc.data() as PatientSensitiveInfo;
|
|
245
|
-
if (sensitiveData.userRef) {
|
|
246
|
-
throw new Error('Patient sensitive information has already been claimed');
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
await updateDoc(getSensitiveInfoDocRef(db, patientId), {
|
|
250
|
-
userRef: userId,
|
|
251
|
-
updatedAt: serverTimestamp(),
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
const updatedDoc = await getDoc(getSensitiveInfoDocRef(db, patientId));
|
|
255
|
-
if (!updatedDoc.exists()) {
|
|
256
|
-
throw new Error('Failed to retrieve updated sensitive information');
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return updatedDoc.data() as PatientSensitiveInfo;
|
|
260
|
-
};
|
|
1
|
+
import { getDoc, updateDoc, setDoc, serverTimestamp, Firestore } from 'firebase/firestore';
|
|
2
|
+
import {
|
|
3
|
+
PatientSensitiveInfo,
|
|
4
|
+
CreatePatientSensitiveInfoData,
|
|
5
|
+
UpdatePatientSensitiveInfoData,
|
|
6
|
+
} from '../../../types/patient';
|
|
7
|
+
import { UserRole } from '../../../types';
|
|
8
|
+
import { createPatientSensitiveInfoSchema } from '../../../validations/patient.schema';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import {
|
|
11
|
+
getSensitiveInfoDocRef,
|
|
12
|
+
initSensitiveInfoDocIfNotExists,
|
|
13
|
+
getPatientDocRef,
|
|
14
|
+
} from './docs.utils';
|
|
15
|
+
import { MediaService, MediaAccessLevel, MediaResource } from '../../media/media.service';
|
|
16
|
+
import { AuthError } from '../../../errors/auth.errors';
|
|
17
|
+
import { getPractitionerProfileByUserRef } from './practitioner.utils';
|
|
18
|
+
import { getClinicAdminByUserRef } from '../../clinic/utils/admin.utils';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Checks if the requester has permission to access/modify sensitive info.
|
|
22
|
+
* Access is granted to the patient owner, or an associated practitioner/clinic admin.
|
|
23
|
+
*/
|
|
24
|
+
const checkSensitiveAccessUtil = async (
|
|
25
|
+
db: Firestore,
|
|
26
|
+
patientId: string,
|
|
27
|
+
requesterId: string,
|
|
28
|
+
requesterRoles: UserRole[],
|
|
29
|
+
): Promise<void> => {
|
|
30
|
+
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
31
|
+
if (!patientDoc.exists()) {
|
|
32
|
+
throw new Error('Patient profile not found');
|
|
33
|
+
}
|
|
34
|
+
const patientData = patientDoc.data() as any; // Cast to any to access properties
|
|
35
|
+
|
|
36
|
+
// 1. Patient is the owner
|
|
37
|
+
if (patientData.userRef && patientData.userRef === requesterId) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Requester is an associated practitioner
|
|
42
|
+
if (requesterRoles.includes(UserRole.PRACTITIONER)) {
|
|
43
|
+
const practitionerProfile = await getPractitionerProfileByUserRef(db, requesterId);
|
|
44
|
+
if (practitionerProfile && patientData.doctorIds?.includes(practitionerProfile.id)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 3. Requester is an associated clinic admin
|
|
50
|
+
if (requesterRoles.includes(UserRole.CLINIC_ADMIN)) {
|
|
51
|
+
const adminProfile = await getClinicAdminByUserRef(db, requesterId);
|
|
52
|
+
if (adminProfile && adminProfile.clinicsManaged) {
|
|
53
|
+
const hasAccess = adminProfile.clinicsManaged.some(managedClinicId =>
|
|
54
|
+
patientData.clinicIds?.includes(managedClinicId),
|
|
55
|
+
);
|
|
56
|
+
if (hasAccess) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw new AuthError(
|
|
63
|
+
'Unauthorized access to sensitive information.',
|
|
64
|
+
'AUTH/UNAUTHORIZED_ACCESS',
|
|
65
|
+
403,
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Handles photoUrl upload for sensitive info (supports MediaResource)
|
|
71
|
+
* @param photoUrl - MediaResource (File, Blob, or URL string)
|
|
72
|
+
* @param patientId - ID of the patient
|
|
73
|
+
* @param mediaService - MediaService instance
|
|
74
|
+
* @returns URL string of the uploaded or existing photo
|
|
75
|
+
*/
|
|
76
|
+
const handlePhotoUrlUpload = async (
|
|
77
|
+
photoUrl: MediaResource | undefined | null,
|
|
78
|
+
patientId: string,
|
|
79
|
+
mediaService: MediaService,
|
|
80
|
+
): Promise<string | null> => {
|
|
81
|
+
if (!photoUrl) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// If it's already a URL string, return it as is
|
|
86
|
+
if (typeof photoUrl === 'string') {
|
|
87
|
+
return photoUrl;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// If it's a File or Blob, upload it
|
|
91
|
+
if (photoUrl instanceof File || photoUrl instanceof Blob) {
|
|
92
|
+
const mediaMetadata = await mediaService.uploadMedia(
|
|
93
|
+
photoUrl,
|
|
94
|
+
patientId, // Using patientId as ownerId
|
|
95
|
+
MediaAccessLevel.PRIVATE, // Sensitive info should be private
|
|
96
|
+
'patient_sensitive_photos',
|
|
97
|
+
photoUrl instanceof File ? photoUrl.name : `sensitive_photo_${patientId}`,
|
|
98
|
+
);
|
|
99
|
+
return mediaMetadata.url;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Funkcije za rad sa osetljivim informacijama
|
|
106
|
+
export const createSensitiveInfoUtil = async (
|
|
107
|
+
db: Firestore,
|
|
108
|
+
data: CreatePatientSensitiveInfoData,
|
|
109
|
+
requesterId: string,
|
|
110
|
+
requesterRoles: UserRole[],
|
|
111
|
+
mediaService?: MediaService,
|
|
112
|
+
): Promise<PatientSensitiveInfo> => {
|
|
113
|
+
try {
|
|
114
|
+
// Security check
|
|
115
|
+
await checkSensitiveAccessUtil(db, data.patientId, requesterId, requesterRoles);
|
|
116
|
+
|
|
117
|
+
const validatedData = createPatientSensitiveInfoSchema.parse(data);
|
|
118
|
+
|
|
119
|
+
// Proveriti da li dokument već postoji
|
|
120
|
+
const sensitiveDoc = await getDoc(getSensitiveInfoDocRef(db, data.patientId));
|
|
121
|
+
if (sensitiveDoc.exists()) {
|
|
122
|
+
throw new Error('Sensitive information already exists for this patient');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Process photoUrl if it's a MediaResource and mediaService is provided
|
|
126
|
+
let processedPhotoUrl: string | null = null;
|
|
127
|
+
if (validatedData.photoUrl && mediaService) {
|
|
128
|
+
processedPhotoUrl = await handlePhotoUrlUpload(
|
|
129
|
+
validatedData.photoUrl,
|
|
130
|
+
data.patientId,
|
|
131
|
+
mediaService,
|
|
132
|
+
);
|
|
133
|
+
} else if (typeof validatedData.photoUrl === 'string') {
|
|
134
|
+
processedPhotoUrl = validatedData.photoUrl;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const sensitiveInfoData = {
|
|
138
|
+
...validatedData,
|
|
139
|
+
photoUrl: processedPhotoUrl,
|
|
140
|
+
createdAt: serverTimestamp(),
|
|
141
|
+
updatedAt: serverTimestamp(),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
await setDoc(getSensitiveInfoDocRef(db, data.patientId), sensitiveInfoData);
|
|
145
|
+
|
|
146
|
+
const createdDoc = await getDoc(getSensitiveInfoDocRef(db, data.patientId));
|
|
147
|
+
if (!createdDoc.exists()) {
|
|
148
|
+
throw new Error('Failed to create sensitive information');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return createdDoc.data() as PatientSensitiveInfo;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof z.ZodError) {
|
|
154
|
+
throw new Error('Invalid sensitive info data: ' + error.message);
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const getSensitiveInfoUtil = async (
|
|
161
|
+
db: Firestore,
|
|
162
|
+
patientId: string,
|
|
163
|
+
requesterId: string,
|
|
164
|
+
requesterRoles: UserRole[],
|
|
165
|
+
): Promise<PatientSensitiveInfo | null> => {
|
|
166
|
+
// Security check
|
|
167
|
+
await checkSensitiveAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
168
|
+
|
|
169
|
+
// Inicijalizacija dokumenta ako ne postoji
|
|
170
|
+
await initSensitiveInfoDocIfNotExists(db, patientId, requesterId);
|
|
171
|
+
|
|
172
|
+
const sensitiveDoc = await getDoc(getSensitiveInfoDocRef(db, patientId));
|
|
173
|
+
return sensitiveDoc.exists() ? (sensitiveDoc.data() as PatientSensitiveInfo) : null;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const updateSensitiveInfoUtil = async (
|
|
177
|
+
db: Firestore,
|
|
178
|
+
patientId: string,
|
|
179
|
+
data: UpdatePatientSensitiveInfoData,
|
|
180
|
+
requesterId: string,
|
|
181
|
+
requesterRoles: UserRole[],
|
|
182
|
+
mediaService?: MediaService,
|
|
183
|
+
): Promise<PatientSensitiveInfo> => {
|
|
184
|
+
// Security check
|
|
185
|
+
await checkSensitiveAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
186
|
+
|
|
187
|
+
// Inicijalizacija dokumenta ako ne postoji
|
|
188
|
+
await initSensitiveInfoDocIfNotExists(db, patientId, requesterId);
|
|
189
|
+
|
|
190
|
+
// Process photoUrl if it's a MediaResource and mediaService is provided
|
|
191
|
+
let processedPhotoUrl: string | null | undefined = undefined;
|
|
192
|
+
if (data.photoUrl !== undefined) {
|
|
193
|
+
if (mediaService) {
|
|
194
|
+
processedPhotoUrl = await handlePhotoUrlUpload(data.photoUrl, patientId, mediaService);
|
|
195
|
+
} else if (typeof data.photoUrl === 'string' || data.photoUrl === null) {
|
|
196
|
+
processedPhotoUrl = data.photoUrl;
|
|
197
|
+
} else {
|
|
198
|
+
// If photoUrl is a File/Blob but no mediaService provided, throw error
|
|
199
|
+
throw new Error('MediaService required to process photo upload');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const updateData = {
|
|
204
|
+
...data,
|
|
205
|
+
photoUrl: processedPhotoUrl,
|
|
206
|
+
updatedAt: serverTimestamp(),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
await updateDoc(getSensitiveInfoDocRef(db, patientId), updateData);
|
|
210
|
+
|
|
211
|
+
const updatedDoc = await getDoc(getSensitiveInfoDocRef(db, patientId));
|
|
212
|
+
if (!updatedDoc.exists()) {
|
|
213
|
+
throw new Error('Failed to retrieve updated sensitive information');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return updatedDoc.data() as PatientSensitiveInfo;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const claimPatientSensitiveInfoUtil = async (
|
|
220
|
+
db: Firestore,
|
|
221
|
+
patientId: string,
|
|
222
|
+
userId: string,
|
|
223
|
+
): Promise<PatientSensitiveInfo> => {
|
|
224
|
+
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
225
|
+
if (!patientDoc.exists()) {
|
|
226
|
+
throw new Error('Patient profile not found');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const patientData = patientDoc.data() as any;
|
|
230
|
+
|
|
231
|
+
if (!patientData.isManual) {
|
|
232
|
+
throw new Error('Only manually created patient profiles can be claimed');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (patientData.userRef) {
|
|
236
|
+
throw new Error('Patient profile has already been claimed');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const sensitiveDoc = await getDoc(getSensitiveInfoDocRef(db, patientId));
|
|
240
|
+
if (!sensitiveDoc.exists()) {
|
|
241
|
+
throw new Error('Patient sensitive information not found');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const sensitiveData = sensitiveDoc.data() as PatientSensitiveInfo;
|
|
245
|
+
if (sensitiveData.userRef) {
|
|
246
|
+
throw new Error('Patient sensitive information has already been claimed');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await updateDoc(getSensitiveInfoDocRef(db, patientId), {
|
|
250
|
+
userRef: userId,
|
|
251
|
+
updatedAt: serverTimestamp(),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const updatedDoc = await getDoc(getSensitiveInfoDocRef(db, patientId));
|
|
255
|
+
if (!updatedDoc.exists()) {
|
|
256
|
+
throw new Error('Failed to retrieve updated sensitive information');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return updatedDoc.data() as PatientSensitiveInfo;
|
|
260
|
+
};
|