@blackcode_sa/metaestetics-api 1.14.78 → 1.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/admin/index.d.mts +5 -0
  2. package/dist/admin/index.d.ts +5 -0
  3. package/dist/admin/index.js +47 -5
  4. package/dist/admin/index.mjs +47 -5
  5. package/dist/index.d.mts +302 -1
  6. package/dist/index.d.ts +302 -1
  7. package/dist/index.js +2655 -1754
  8. package/dist/index.mjs +1880 -983
  9. package/package.json +1 -1
  10. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +59 -6
  11. package/src/services/__tests__/auth/auth.mock.test.ts +2 -2
  12. package/src/services/__tests__/auth/auth.setup.ts +6 -1
  13. package/src/services/__tests__/auth.service.test.ts +8 -44
  14. package/src/services/__tests__/base.service.test.ts +4 -45
  15. package/src/services/__tests__/user.service.test.ts +6 -4
  16. package/src/services/appointment/utils/appointment.utils.ts +0 -3
  17. package/src/services/appointment/utils/extended-procedure.utils.ts +1 -0
  18. package/src/services/auth/auth.v2.service.ts +7 -7
  19. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +11 -33
  20. package/src/services/clinic/__tests__/clinic-group.service.test.ts +21 -151
  21. package/src/services/clinic/__tests__/clinic.service.test.ts +17 -69
  22. package/src/services/clinic/utils/clinic-group.utils.ts +2 -2
  23. package/src/services/clinic/utils/clinic.utils.ts +28 -22
  24. package/src/services/clinic/utils/index.ts +0 -1
  25. package/src/services/notifications/__tests__/notification.service.test.ts +5 -5
  26. package/src/services/patient/__tests__/patient.service.test.ts +17 -25
  27. package/src/services/patient/patient.service.ts +136 -0
  28. package/src/services/patient/utils/body-assessment.utils.ts +159 -0
  29. package/src/services/patient/utils/docs.utils.ts +1 -1
  30. package/src/services/patient/utils/hair-scalp-assessment.utils.ts +158 -0
  31. package/src/services/patient/utils/pre-surgical-assessment.utils.ts +161 -0
  32. package/src/services/patient/utils/skin-quality-assessment.utils.ts +160 -0
  33. package/src/services/user/user.v2.service.ts +4 -3
  34. package/src/types/patient/body-assessment.types.ts +93 -0
  35. package/src/types/patient/hair-scalp-assessment.types.ts +98 -0
  36. package/src/types/patient/index.ts +4 -0
  37. package/src/types/patient/pre-surgical-assessment.types.ts +95 -0
  38. package/src/types/patient/skin-quality-assessment.types.ts +105 -0
  39. package/src/validations/patient/body-assessment.schema.ts +82 -0
  40. package/src/validations/patient/hair-scalp-assessment.schema.ts +70 -0
  41. package/src/validations/patient/pre-surgical-assessment.schema.ts +78 -0
  42. package/src/validations/patient/skin-quality-assessment.schema.ts +70 -0
@@ -103,8 +103,40 @@ import {
103
103
  AestheticAnalysis,
104
104
  CreateAestheticAnalysisData,
105
105
  UpdateAestheticAnalysisData,
106
+ SkinQualityAssessment,
107
+ CreateSkinQualityAssessmentData,
108
+ UpdateSkinQualityAssessmentData,
109
+ BodyAssessment,
110
+ CreateBodyAssessmentData,
111
+ UpdateBodyAssessmentData,
112
+ HairScalpAssessment,
113
+ CreateHairScalpAssessmentData,
114
+ UpdateHairScalpAssessmentData,
115
+ PreSurgicalAssessment,
116
+ CreatePreSurgicalAssessmentData,
117
+ UpdatePreSurgicalAssessmentData,
106
118
  } from '../../types/patient';
107
119
 
120
+ import {
121
+ getSkinQualityAssessmentUtil,
122
+ createOrUpdateSkinQualityAssessmentUtil,
123
+ } from './utils/skin-quality-assessment.utils';
124
+
125
+ import {
126
+ getBodyAssessmentUtil,
127
+ createOrUpdateBodyAssessmentUtil,
128
+ } from './utils/body-assessment.utils';
129
+
130
+ import {
131
+ getHairScalpAssessmentUtil,
132
+ createOrUpdateHairScalpAssessmentUtil,
133
+ } from './utils/hair-scalp-assessment.utils';
134
+
135
+ import {
136
+ getPreSurgicalAssessmentUtil,
137
+ createOrUpdatePreSurgicalAssessmentUtil,
138
+ } from './utils/pre-surgical-assessment.utils';
139
+
108
140
  import { CreatePatientTokenData, PatientToken } from '../../types/patient/token.types';
109
141
 
110
142
  export class PatientService extends BaseService {
@@ -882,4 +914,108 @@ export class PatientService extends BaseService {
882
914
  true
883
915
  );
884
916
  }
917
+
918
+ // Skin Quality Assessment methods
919
+ async getSkinQualityAssessment(patientId: string): Promise<SkinQualityAssessment | null> {
920
+ const currentUser = await this.getCurrentUser();
921
+ return getSkinQualityAssessmentUtil(this.db, patientId, currentUser.uid, currentUser.roles);
922
+ }
923
+
924
+ async createSkinQualityAssessment(
925
+ patientId: string,
926
+ data: CreateSkinQualityAssessmentData
927
+ ): Promise<void> {
928
+ const currentUser = await this.getCurrentUser();
929
+ return createOrUpdateSkinQualityAssessmentUtil(
930
+ this.db, patientId, data, currentUser.uid, currentUser.roles, false
931
+ );
932
+ }
933
+
934
+ async updateSkinQualityAssessment(
935
+ patientId: string,
936
+ data: UpdateSkinQualityAssessmentData
937
+ ): Promise<void> {
938
+ const currentUser = await this.getCurrentUser();
939
+ return createOrUpdateSkinQualityAssessmentUtil(
940
+ this.db, patientId, data, currentUser.uid, currentUser.roles, true
941
+ );
942
+ }
943
+
944
+ // Body Assessment methods
945
+ async getBodyAssessment(patientId: string): Promise<BodyAssessment | null> {
946
+ const currentUser = await this.getCurrentUser();
947
+ return getBodyAssessmentUtil(this.db, patientId, currentUser.uid, currentUser.roles);
948
+ }
949
+
950
+ async createBodyAssessment(
951
+ patientId: string,
952
+ data: CreateBodyAssessmentData
953
+ ): Promise<void> {
954
+ const currentUser = await this.getCurrentUser();
955
+ return createOrUpdateBodyAssessmentUtil(
956
+ this.db, patientId, data, currentUser.uid, currentUser.roles, false
957
+ );
958
+ }
959
+
960
+ async updateBodyAssessment(
961
+ patientId: string,
962
+ data: UpdateBodyAssessmentData
963
+ ): Promise<void> {
964
+ const currentUser = await this.getCurrentUser();
965
+ return createOrUpdateBodyAssessmentUtil(
966
+ this.db, patientId, data, currentUser.uid, currentUser.roles, true
967
+ );
968
+ }
969
+
970
+ // Hair & Scalp Assessment methods
971
+ async getHairScalpAssessment(patientId: string): Promise<HairScalpAssessment | null> {
972
+ const currentUser = await this.getCurrentUser();
973
+ return getHairScalpAssessmentUtil(this.db, patientId, currentUser.uid, currentUser.roles);
974
+ }
975
+
976
+ async createHairScalpAssessment(
977
+ patientId: string,
978
+ data: CreateHairScalpAssessmentData
979
+ ): Promise<void> {
980
+ const currentUser = await this.getCurrentUser();
981
+ return createOrUpdateHairScalpAssessmentUtil(
982
+ this.db, patientId, data, currentUser.uid, currentUser.roles, false
983
+ );
984
+ }
985
+
986
+ async updateHairScalpAssessment(
987
+ patientId: string,
988
+ data: UpdateHairScalpAssessmentData
989
+ ): Promise<void> {
990
+ const currentUser = await this.getCurrentUser();
991
+ return createOrUpdateHairScalpAssessmentUtil(
992
+ this.db, patientId, data, currentUser.uid, currentUser.roles, true
993
+ );
994
+ }
995
+
996
+ // Pre-Surgical Assessment methods
997
+ async getPreSurgicalAssessment(patientId: string): Promise<PreSurgicalAssessment | null> {
998
+ const currentUser = await this.getCurrentUser();
999
+ return getPreSurgicalAssessmentUtil(this.db, patientId, currentUser.uid, currentUser.roles);
1000
+ }
1001
+
1002
+ async createPreSurgicalAssessment(
1003
+ patientId: string,
1004
+ data: CreatePreSurgicalAssessmentData
1005
+ ): Promise<void> {
1006
+ const currentUser = await this.getCurrentUser();
1007
+ return createOrUpdatePreSurgicalAssessmentUtil(
1008
+ this.db, patientId, data, currentUser.uid, currentUser.roles, false
1009
+ );
1010
+ }
1011
+
1012
+ async updatePreSurgicalAssessment(
1013
+ patientId: string,
1014
+ data: UpdatePreSurgicalAssessmentData
1015
+ ): Promise<void> {
1016
+ const currentUser = await this.getCurrentUser();
1017
+ return createOrUpdatePreSurgicalAssessmentUtil(
1018
+ this.db, patientId, data, currentUser.uid, currentUser.roles, true
1019
+ );
1020
+ }
885
1021
  }
@@ -0,0 +1,159 @@
1
+ import { getDoc, updateDoc, setDoc, serverTimestamp, Firestore, doc } from 'firebase/firestore';
2
+ import {
3
+ BodyAssessment,
4
+ CreateBodyAssessmentData,
5
+ UpdateBodyAssessmentData,
6
+ BODY_ASSESSMENT_COLLECTION,
7
+ PATIENTS_COLLECTION,
8
+ BodyAssessmentStatus,
9
+ } from '../../../types/patient';
10
+ import { UserRole } from '../../../types';
11
+ import {
12
+ createBodyAssessmentSchema,
13
+ updateBodyAssessmentSchema,
14
+ bodyAssessmentSchema,
15
+ } from '../../../validations/patient/body-assessment.schema';
16
+ import { getPatientDocRef } from './docs.utils';
17
+ import { AuthError } from '../../../errors/auth.errors';
18
+ import { getPractitionerProfileByUserRef } from './practitioner.utils';
19
+ import { getClinicAdminByUserRef } from '../../clinic/utils/admin.utils';
20
+
21
+ export const getBodyAssessmentDocRef = (db: Firestore, patientId: string) => {
22
+ return doc(db, PATIENTS_COLLECTION, patientId, BODY_ASSESSMENT_COLLECTION, patientId);
23
+ };
24
+
25
+ const checkBodyAssessmentAccessUtil = async (
26
+ db: Firestore,
27
+ patientId: string,
28
+ requesterId: string,
29
+ requesterRoles: UserRole[]
30
+ ): Promise<void> => {
31
+ const patientDoc = await getDoc(getPatientDocRef(db, patientId));
32
+ if (!patientDoc.exists()) {
33
+ throw new Error('Patient profile not found');
34
+ }
35
+ const patientData = patientDoc.data() as any;
36
+
37
+ if (patientData.userRef && patientData.userRef === requesterId) {
38
+ return;
39
+ }
40
+
41
+ if (requesterRoles.includes(UserRole.PRACTITIONER)) {
42
+ const practitionerProfile = await getPractitionerProfileByUserRef(db, requesterId);
43
+ if (practitionerProfile && patientData.doctorIds?.includes(practitionerProfile.id)) {
44
+ return;
45
+ }
46
+ }
47
+
48
+ if (requesterRoles.includes(UserRole.CLINIC_ADMIN)) {
49
+ const adminProfile = await getClinicAdminByUserRef(db, requesterId);
50
+ if (adminProfile && adminProfile.clinicsManaged) {
51
+ const hasAccess = adminProfile.clinicsManaged.some((managedClinicId) =>
52
+ patientData.clinicIds?.includes(managedClinicId)
53
+ );
54
+ if (hasAccess) {
55
+ return;
56
+ }
57
+ }
58
+ }
59
+
60
+ throw new AuthError(
61
+ 'Unauthorized access to body assessment.',
62
+ 'AUTH/UNAUTHORIZED_ACCESS',
63
+ 403
64
+ );
65
+ };
66
+
67
+ export const calculateBodyAssessmentCompletionPercentage = (data: Partial<BodyAssessment>): number => {
68
+ let completed = 0;
69
+ const total = 4;
70
+
71
+ if (data.selectedZones && data.selectedZones.length > 0) completed++;
72
+ if (data.zoneAssessments && data.zoneAssessments.length > 0) completed++;
73
+ if (data.composition && (data.composition.bmi || data.composition.bodyFatPercentage || data.composition.waistHipRatio)) completed++;
74
+ if ((data.measurements && data.measurements.waist) || (data.symmetry && data.symmetry.overallSymmetry)) completed++;
75
+
76
+ return Math.round((completed / total) * 100);
77
+ };
78
+
79
+ export const determineBodyAssessmentStatus = (completionPercentage: number): BodyAssessmentStatus => {
80
+ if (completionPercentage < 50) return 'incomplete';
81
+ if (completionPercentage >= 50) return 'ready_for_planning';
82
+ return 'incomplete';
83
+ };
84
+
85
+ export const getBodyAssessmentUtil = async (
86
+ db: Firestore,
87
+ patientId: string,
88
+ requesterId: string,
89
+ requesterRoles: UserRole[]
90
+ ): Promise<BodyAssessment | null> => {
91
+ await checkBodyAssessmentAccessUtil(db, patientId, requesterId, requesterRoles);
92
+
93
+ const docRef = getBodyAssessmentDocRef(db, patientId);
94
+ const snapshot = await getDoc(docRef);
95
+
96
+ if (!snapshot.exists()) {
97
+ return null;
98
+ }
99
+
100
+ const data = snapshot.data();
101
+ return bodyAssessmentSchema.parse({
102
+ ...data,
103
+ id: patientId,
104
+ });
105
+ };
106
+
107
+ export const createOrUpdateBodyAssessmentUtil = async (
108
+ db: Firestore,
109
+ patientId: string,
110
+ data: CreateBodyAssessmentData | UpdateBodyAssessmentData,
111
+ requesterId: string,
112
+ requesterRoles: UserRole[],
113
+ isUpdate: boolean = false
114
+ ): Promise<void> => {
115
+ await checkBodyAssessmentAccessUtil(db, patientId, requesterId, requesterRoles);
116
+
117
+ const validatedData = isUpdate
118
+ ? updateBodyAssessmentSchema.parse(data)
119
+ : createBodyAssessmentSchema.parse(data);
120
+
121
+ const docRef = getBodyAssessmentDocRef(db, patientId);
122
+ const snapshot = await getDoc(docRef);
123
+
124
+ const requesterRole = requesterRoles.includes(UserRole.PRACTITIONER) ? 'PRACTITIONER' : 'PATIENT';
125
+
126
+ const existingData = snapshot.exists() ? snapshot.data() : null;
127
+ const mergedData: any = {
128
+ ...(existingData || {}),
129
+ ...validatedData,
130
+ };
131
+
132
+ const completionPercentage = calculateBodyAssessmentCompletionPercentage(mergedData);
133
+ const status = determineBodyAssessmentStatus(completionPercentage);
134
+
135
+ if (!snapshot.exists()) {
136
+ await setDoc(docRef, {
137
+ id: patientId,
138
+ patientId,
139
+ selectedZones: [],
140
+ zoneAssessments: [],
141
+ ...validatedData,
142
+ completionPercentage,
143
+ status,
144
+ lastUpdatedBy: requesterId,
145
+ lastUpdatedByRole: requesterRole,
146
+ createdAt: serverTimestamp(),
147
+ updatedAt: serverTimestamp(),
148
+ });
149
+ } else {
150
+ await updateDoc(docRef, {
151
+ ...validatedData,
152
+ completionPercentage,
153
+ status,
154
+ lastUpdatedBy: requesterId,
155
+ lastUpdatedByRole: requesterRole,
156
+ updatedAt: serverTimestamp(),
157
+ });
158
+ }
159
+ };
@@ -120,7 +120,7 @@ export const initSensitiveInfoDocIfNotExists = async (
120
120
  )
121
121
  );
122
122
 
123
- await createSensitiveInfoUtil(db, defaultSensitiveInfo, userRef);
123
+ await createSensitiveInfoUtil(db, defaultSensitiveInfo, userRef, []);
124
124
 
125
125
  // Verify document was created
126
126
  const verifyDoc = await getDoc(sensitiveInfoRef);
@@ -0,0 +1,158 @@
1
+ import { getDoc, updateDoc, setDoc, serverTimestamp, Firestore, doc } from 'firebase/firestore';
2
+ import {
3
+ HairScalpAssessment,
4
+ CreateHairScalpAssessmentData,
5
+ UpdateHairScalpAssessmentData,
6
+ HAIR_SCALP_ASSESSMENT_COLLECTION,
7
+ PATIENTS_COLLECTION,
8
+ HairScalpAssessmentStatus,
9
+ } from '../../../types/patient';
10
+ import { UserRole } from '../../../types';
11
+ import {
12
+ createHairScalpAssessmentSchema,
13
+ updateHairScalpAssessmentSchema,
14
+ hairScalpAssessmentSchema,
15
+ } from '../../../validations/patient/hair-scalp-assessment.schema';
16
+ import { getPatientDocRef } from './docs.utils';
17
+ import { AuthError } from '../../../errors/auth.errors';
18
+ import { getPractitionerProfileByUserRef } from './practitioner.utils';
19
+ import { getClinicAdminByUserRef } from '../../clinic/utils/admin.utils';
20
+
21
+ export const getHairScalpAssessmentDocRef = (db: Firestore, patientId: string) => {
22
+ return doc(db, PATIENTS_COLLECTION, patientId, HAIR_SCALP_ASSESSMENT_COLLECTION, patientId);
23
+ };
24
+
25
+ const checkHairScalpAssessmentAccessUtil = async (
26
+ db: Firestore,
27
+ patientId: string,
28
+ requesterId: string,
29
+ requesterRoles: UserRole[]
30
+ ): Promise<void> => {
31
+ const patientDoc = await getDoc(getPatientDocRef(db, patientId));
32
+ if (!patientDoc.exists()) {
33
+ throw new Error('Patient profile not found');
34
+ }
35
+ const patientData = patientDoc.data() as any;
36
+
37
+ if (patientData.userRef && patientData.userRef === requesterId) {
38
+ return;
39
+ }
40
+
41
+ if (requesterRoles.includes(UserRole.PRACTITIONER)) {
42
+ const practitionerProfile = await getPractitionerProfileByUserRef(db, requesterId);
43
+ if (practitionerProfile && patientData.doctorIds?.includes(practitionerProfile.id)) {
44
+ return;
45
+ }
46
+ }
47
+
48
+ if (requesterRoles.includes(UserRole.CLINIC_ADMIN)) {
49
+ const adminProfile = await getClinicAdminByUserRef(db, requesterId);
50
+ if (adminProfile && adminProfile.clinicsManaged) {
51
+ const hasAccess = adminProfile.clinicsManaged.some((managedClinicId) =>
52
+ patientData.clinicIds?.includes(managedClinicId)
53
+ );
54
+ if (hasAccess) {
55
+ return;
56
+ }
57
+ }
58
+ }
59
+
60
+ throw new AuthError(
61
+ 'Unauthorized access to hair & scalp assessment.',
62
+ 'AUTH/UNAUTHORIZED_ACCESS',
63
+ 403
64
+ );
65
+ };
66
+
67
+ export const calculateHairScalpCompletionPercentage = (data: Partial<HairScalpAssessment>): number => {
68
+ let completed = 0;
69
+ const total = 4;
70
+
71
+ if (data.hairLossPattern && data.hairLossPattern.type) completed++;
72
+ if (data.hairCharacteristics && data.hairCharacteristics.type) completed++;
73
+ if (data.scalpCondition) completed++;
74
+ if (data.zoneAssessments && data.zoneAssessments.length > 0) completed++;
75
+
76
+ return Math.round((completed / total) * 100);
77
+ };
78
+
79
+ export const determineHairScalpStatus = (completionPercentage: number): HairScalpAssessmentStatus => {
80
+ if (completionPercentage < 50) return 'incomplete';
81
+ if (completionPercentage >= 50) return 'ready_for_planning';
82
+ return 'incomplete';
83
+ };
84
+
85
+ export const getHairScalpAssessmentUtil = async (
86
+ db: Firestore,
87
+ patientId: string,
88
+ requesterId: string,
89
+ requesterRoles: UserRole[]
90
+ ): Promise<HairScalpAssessment | null> => {
91
+ await checkHairScalpAssessmentAccessUtil(db, patientId, requesterId, requesterRoles);
92
+
93
+ const docRef = getHairScalpAssessmentDocRef(db, patientId);
94
+ const snapshot = await getDoc(docRef);
95
+
96
+ if (!snapshot.exists()) {
97
+ return null;
98
+ }
99
+
100
+ const data = snapshot.data();
101
+ return hairScalpAssessmentSchema.parse({
102
+ ...data,
103
+ id: patientId,
104
+ });
105
+ };
106
+
107
+ export const createOrUpdateHairScalpAssessmentUtil = async (
108
+ db: Firestore,
109
+ patientId: string,
110
+ data: CreateHairScalpAssessmentData | UpdateHairScalpAssessmentData,
111
+ requesterId: string,
112
+ requesterRoles: UserRole[],
113
+ isUpdate: boolean = false
114
+ ): Promise<void> => {
115
+ await checkHairScalpAssessmentAccessUtil(db, patientId, requesterId, requesterRoles);
116
+
117
+ const validatedData = isUpdate
118
+ ? updateHairScalpAssessmentSchema.parse(data)
119
+ : createHairScalpAssessmentSchema.parse(data);
120
+
121
+ const docRef = getHairScalpAssessmentDocRef(db, patientId);
122
+ const snapshot = await getDoc(docRef);
123
+
124
+ const requesterRole = requesterRoles.includes(UserRole.PRACTITIONER) ? 'PRACTITIONER' : 'PATIENT';
125
+
126
+ const existingData = snapshot.exists() ? snapshot.data() : null;
127
+ const mergedData: any = {
128
+ ...(existingData || {}),
129
+ ...validatedData,
130
+ };
131
+
132
+ const completionPercentage = calculateHairScalpCompletionPercentage(mergedData);
133
+ const status = determineHairScalpStatus(completionPercentage);
134
+
135
+ if (!snapshot.exists()) {
136
+ await setDoc(docRef, {
137
+ id: patientId,
138
+ patientId,
139
+ zoneAssessments: [],
140
+ ...validatedData,
141
+ completionPercentage,
142
+ status,
143
+ lastUpdatedBy: requesterId,
144
+ lastUpdatedByRole: requesterRole,
145
+ createdAt: serverTimestamp(),
146
+ updatedAt: serverTimestamp(),
147
+ });
148
+ } else {
149
+ await updateDoc(docRef, {
150
+ ...validatedData,
151
+ completionPercentage,
152
+ status,
153
+ lastUpdatedBy: requesterId,
154
+ lastUpdatedByRole: requesterRole,
155
+ updatedAt: serverTimestamp(),
156
+ });
157
+ }
158
+ };
@@ -0,0 +1,161 @@
1
+ import { getDoc, updateDoc, setDoc, serverTimestamp, Firestore, doc } from 'firebase/firestore';
2
+ import {
3
+ PreSurgicalAssessment,
4
+ CreatePreSurgicalAssessmentData,
5
+ UpdatePreSurgicalAssessmentData,
6
+ PRE_SURGICAL_ASSESSMENT_COLLECTION,
7
+ PATIENTS_COLLECTION,
8
+ PreSurgicalAssessmentStatus,
9
+ } from '../../../types/patient';
10
+ import { UserRole } from '../../../types';
11
+ import {
12
+ createPreSurgicalAssessmentSchema,
13
+ updatePreSurgicalAssessmentSchema,
14
+ preSurgicalAssessmentSchema,
15
+ } from '../../../validations/patient/pre-surgical-assessment.schema';
16
+ import { getPatientDocRef } from './docs.utils';
17
+ import { AuthError } from '../../../errors/auth.errors';
18
+ import { getPractitionerProfileByUserRef } from './practitioner.utils';
19
+ import { getClinicAdminByUserRef } from '../../clinic/utils/admin.utils';
20
+
21
+ export const getPreSurgicalAssessmentDocRef = (db: Firestore, patientId: string) => {
22
+ return doc(db, PATIENTS_COLLECTION, patientId, PRE_SURGICAL_ASSESSMENT_COLLECTION, patientId);
23
+ };
24
+
25
+ const checkPreSurgicalAssessmentAccessUtil = async (
26
+ db: Firestore,
27
+ patientId: string,
28
+ requesterId: string,
29
+ requesterRoles: UserRole[]
30
+ ): Promise<void> => {
31
+ const patientDoc = await getDoc(getPatientDocRef(db, patientId));
32
+ if (!patientDoc.exists()) {
33
+ throw new Error('Patient profile not found');
34
+ }
35
+ const patientData = patientDoc.data() as any;
36
+
37
+ if (patientData.userRef && patientData.userRef === requesterId) {
38
+ return;
39
+ }
40
+
41
+ if (requesterRoles.includes(UserRole.PRACTITIONER)) {
42
+ const practitionerProfile = await getPractitionerProfileByUserRef(db, requesterId);
43
+ if (practitionerProfile && patientData.doctorIds?.includes(practitionerProfile.id)) {
44
+ return;
45
+ }
46
+ }
47
+
48
+ if (requesterRoles.includes(UserRole.CLINIC_ADMIN)) {
49
+ const adminProfile = await getClinicAdminByUserRef(db, requesterId);
50
+ if (adminProfile && adminProfile.clinicsManaged) {
51
+ const hasAccess = adminProfile.clinicsManaged.some((managedClinicId) =>
52
+ patientData.clinicIds?.includes(managedClinicId)
53
+ );
54
+ if (hasAccess) {
55
+ return;
56
+ }
57
+ }
58
+ }
59
+
60
+ throw new AuthError(
61
+ 'Unauthorized access to pre-surgical assessment.',
62
+ 'AUTH/UNAUTHORIZED_ACCESS',
63
+ 403
64
+ );
65
+ };
66
+
67
+ export const calculatePreSurgicalCompletionPercentage = (data: Partial<PreSurgicalAssessment>): number => {
68
+ let completed = 0;
69
+ const total = 5;
70
+
71
+ if (data.asaClassification) completed++;
72
+ if (data.anesthesiaHistory) completed++;
73
+ if (data.bleedingRisk) completed++;
74
+ if (data.surgicalSiteAssessments && data.surgicalSiteAssessments.length > 0) completed++;
75
+ if ((data.labResults && data.labResults.length > 0) || data.clearance === 'obtained') completed++;
76
+
77
+ return Math.round((completed / total) * 100);
78
+ };
79
+
80
+ export const determinePreSurgicalStatus = (completionPercentage: number): PreSurgicalAssessmentStatus => {
81
+ if (completionPercentage < 40) return 'incomplete';
82
+ if (completionPercentage >= 40) return 'ready_for_planning';
83
+ return 'incomplete';
84
+ };
85
+
86
+ export const getPreSurgicalAssessmentUtil = async (
87
+ db: Firestore,
88
+ patientId: string,
89
+ requesterId: string,
90
+ requesterRoles: UserRole[]
91
+ ): Promise<PreSurgicalAssessment | null> => {
92
+ await checkPreSurgicalAssessmentAccessUtil(db, patientId, requesterId, requesterRoles);
93
+
94
+ const docRef = getPreSurgicalAssessmentDocRef(db, patientId);
95
+ const snapshot = await getDoc(docRef);
96
+
97
+ if (!snapshot.exists()) {
98
+ return null;
99
+ }
100
+
101
+ const data = snapshot.data();
102
+ return preSurgicalAssessmentSchema.parse({
103
+ ...data,
104
+ id: patientId,
105
+ });
106
+ };
107
+
108
+ export const createOrUpdatePreSurgicalAssessmentUtil = async (
109
+ db: Firestore,
110
+ patientId: string,
111
+ data: CreatePreSurgicalAssessmentData | UpdatePreSurgicalAssessmentData,
112
+ requesterId: string,
113
+ requesterRoles: UserRole[],
114
+ isUpdate: boolean = false
115
+ ): Promise<void> => {
116
+ await checkPreSurgicalAssessmentAccessUtil(db, patientId, requesterId, requesterRoles);
117
+
118
+ const validatedData = isUpdate
119
+ ? updatePreSurgicalAssessmentSchema.parse(data)
120
+ : createPreSurgicalAssessmentSchema.parse(data);
121
+
122
+ const docRef = getPreSurgicalAssessmentDocRef(db, patientId);
123
+ const snapshot = await getDoc(docRef);
124
+
125
+ const requesterRole = requesterRoles.includes(UserRole.PRACTITIONER) ? 'PRACTITIONER' : 'PATIENT';
126
+
127
+ const existingData = snapshot.exists() ? snapshot.data() : null;
128
+ const mergedData: any = {
129
+ ...(existingData || {}),
130
+ ...validatedData,
131
+ };
132
+
133
+ const completionPercentage = calculatePreSurgicalCompletionPercentage(mergedData);
134
+ const status = determinePreSurgicalStatus(completionPercentage);
135
+
136
+ if (!snapshot.exists()) {
137
+ await setDoc(docRef, {
138
+ id: patientId,
139
+ patientId,
140
+ surgicalSiteAssessments: [],
141
+ labResults: [],
142
+ clearance: 'not_required',
143
+ ...validatedData,
144
+ completionPercentage,
145
+ status,
146
+ lastUpdatedBy: requesterId,
147
+ lastUpdatedByRole: requesterRole,
148
+ createdAt: serverTimestamp(),
149
+ updatedAt: serverTimestamp(),
150
+ });
151
+ } else {
152
+ await updateDoc(docRef, {
153
+ ...validatedData,
154
+ completionPercentage,
155
+ status,
156
+ lastUpdatedBy: requesterId,
157
+ lastUpdatedByRole: requesterRole,
158
+ updatedAt: serverTimestamp(),
159
+ });
160
+ }
161
+ };