@blackcode_sa/metaestetics-api 1.7.45 → 1.8.0
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 +999 -959
- package/dist/admin/index.d.ts +999 -959
- package/dist/admin/index.js +69 -69
- package/dist/admin/index.mjs +67 -69
- package/dist/index.d.mts +14675 -13040
- package/dist/index.d.ts +14675 -13040
- package/dist/index.js +12224 -14615
- package/dist/index.mjs +12452 -14968
- package/package.json +5 -5
- package/src/admin/index.ts +8 -1
- package/src/index.backup.ts +407 -0
- package/src/index.ts +5 -406
- package/src/services/PATIENTAUTH.MD +197 -0
- package/src/services/__tests__/auth/auth.setup.ts +2 -2
- package/src/services/__tests__/auth.service.test.ts +1 -1
- package/src/services/__tests__/user.service.test.ts +1 -1
- package/src/services/appointment/index.ts +1 -2
- package/src/services/{auth.service.ts → auth/auth.service.ts} +36 -22
- package/src/services/{auth.v2.service.ts → auth/auth.v2.service.ts} +17 -17
- package/src/services/auth/index.ts +2 -16
- package/src/services/calendar/calendar-refactored.service.ts +1 -1
- package/src/services/calendar/externalCalendar.service.ts +178 -0
- package/src/services/calendar/index.ts +5 -0
- package/src/services/clinic/index.ts +4 -0
- package/src/services/index.ts +14 -0
- package/src/services/media/index.ts +1 -0
- package/src/services/notifications/index.ts +1 -0
- package/src/services/patient/README.md +48 -0
- package/src/services/patient/To-Do.md +43 -0
- package/src/services/patient/index.ts +2 -0
- package/src/services/patient/patient.service.ts +289 -34
- package/src/services/patient/utils/index.ts +9 -0
- package/src/services/patient/utils/medical.utils.ts +114 -157
- package/src/services/patient/utils/profile.utils.ts +9 -0
- package/src/services/patient/utils/sensitive.utils.ts +79 -14
- package/src/services/patient/utils/token.utils.ts +211 -0
- package/src/services/practitioner/index.ts +1 -0
- package/src/services/procedure/index.ts +1 -0
- package/src/services/reviews/index.ts +1 -0
- package/src/services/user/index.ts +1 -0
- package/src/services/{user.service.ts → user/user.service.ts} +61 -12
- package/src/services/{user.v2.service.ts → user/user.v2.service.ts} +12 -12
- package/src/types/index.ts +42 -42
- package/src/types/patient/index.ts +33 -6
- package/src/types/patient/token.types.ts +61 -0
- package/src/types/user/index.ts +38 -0
- package/src/utils/index.ts +1 -0
- package/src/validations/calendar.schema.ts +6 -45
- package/src/validations/documentation-templates/index.ts +1 -0
- package/src/validations/documentation-templates.schema.ts +1 -1
- package/src/validations/index.ts +20 -0
- package/src/validations/patient/token.schema.ts +29 -0
- package/src/validations/patient.schema.ts +23 -6
- package/src/validations/profile-info.schema.ts +1 -1
- package/src/validations/schemas.ts +24 -24
|
@@ -10,17 +10,77 @@ import {
|
|
|
10
10
|
CreatePatientSensitiveInfoData,
|
|
11
11
|
UpdatePatientSensitiveInfoData,
|
|
12
12
|
} from "../../../types/patient";
|
|
13
|
+
import { UserRole } from "../../../types";
|
|
13
14
|
import { createPatientSensitiveInfoSchema } from "../../../validations/patient.schema";
|
|
14
15
|
import { z } from "zod";
|
|
15
16
|
import {
|
|
16
17
|
getSensitiveInfoDocRef,
|
|
17
18
|
initSensitiveInfoDocIfNotExists,
|
|
19
|
+
getPatientDocRef,
|
|
18
20
|
} from "./docs.utils";
|
|
19
21
|
import {
|
|
20
22
|
MediaService,
|
|
21
23
|
MediaAccessLevel,
|
|
22
24
|
MediaResource,
|
|
23
25
|
} from "../../media/media.service";
|
|
26
|
+
import { AuthError } from "../../../errors/auth.errors";
|
|
27
|
+
import { getPractitionerProfileByUserRef } from "./practitioner.utils";
|
|
28
|
+
import { getClinicAdminByUserRef } from "../../clinic/utils/admin.utils";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Checks if the requester has permission to access/modify sensitive info.
|
|
32
|
+
* Access is granted to the patient owner, or an associated practitioner/clinic admin.
|
|
33
|
+
*/
|
|
34
|
+
const checkSensitiveAccessUtil = async (
|
|
35
|
+
db: Firestore,
|
|
36
|
+
patientId: string,
|
|
37
|
+
requesterId: string,
|
|
38
|
+
requesterRoles: UserRole[]
|
|
39
|
+
): Promise<void> => {
|
|
40
|
+
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
41
|
+
if (!patientDoc.exists()) {
|
|
42
|
+
throw new Error("Patient profile not found");
|
|
43
|
+
}
|
|
44
|
+
const patientData = patientDoc.data() as any; // Cast to any to access properties
|
|
45
|
+
|
|
46
|
+
// 1. Patient is the owner
|
|
47
|
+
if (patientData.userRef && patientData.userRef === requesterId) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Requester is an associated practitioner
|
|
52
|
+
if (requesterRoles.includes(UserRole.PRACTITIONER)) {
|
|
53
|
+
const practitionerProfile = await getPractitionerProfileByUserRef(
|
|
54
|
+
db,
|
|
55
|
+
requesterId
|
|
56
|
+
);
|
|
57
|
+
if (
|
|
58
|
+
practitionerProfile &&
|
|
59
|
+
patientData.doctorIds?.includes(practitionerProfile.id)
|
|
60
|
+
) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 3. Requester is an associated clinic admin
|
|
66
|
+
if (requesterRoles.includes(UserRole.CLINIC_ADMIN)) {
|
|
67
|
+
const adminProfile = await getClinicAdminByUserRef(db, requesterId);
|
|
68
|
+
if (adminProfile && adminProfile.clinicsManaged) {
|
|
69
|
+
const hasAccess = adminProfile.clinicsManaged.some((managedClinicId) =>
|
|
70
|
+
patientData.clinicIds?.includes(managedClinicId)
|
|
71
|
+
);
|
|
72
|
+
if (hasAccess) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
throw new AuthError(
|
|
79
|
+
"Unauthorized access to sensitive information.",
|
|
80
|
+
"AUTH/UNAUTHORIZED_ACCESS",
|
|
81
|
+
403
|
|
82
|
+
);
|
|
83
|
+
};
|
|
24
84
|
|
|
25
85
|
/**
|
|
26
86
|
* Handles photoUrl upload for sensitive info (supports MediaResource)
|
|
@@ -62,13 +122,18 @@ const handlePhotoUrlUpload = async (
|
|
|
62
122
|
export const createSensitiveInfoUtil = async (
|
|
63
123
|
db: Firestore,
|
|
64
124
|
data: CreatePatientSensitiveInfoData,
|
|
65
|
-
|
|
125
|
+
requesterId: string,
|
|
126
|
+
requesterRoles: UserRole[],
|
|
66
127
|
mediaService?: MediaService
|
|
67
128
|
): Promise<PatientSensitiveInfo> => {
|
|
68
129
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
130
|
+
// Security check
|
|
131
|
+
await checkSensitiveAccessUtil(
|
|
132
|
+
db,
|
|
133
|
+
data.patientId,
|
|
134
|
+
requesterId,
|
|
135
|
+
requesterRoles
|
|
136
|
+
);
|
|
72
137
|
|
|
73
138
|
const validatedData = createPatientSensitiveInfoSchema.parse(data);
|
|
74
139
|
|
|
@@ -118,14 +183,14 @@ export const createSensitiveInfoUtil = async (
|
|
|
118
183
|
export const getSensitiveInfoUtil = async (
|
|
119
184
|
db: Firestore,
|
|
120
185
|
patientId: string,
|
|
121
|
-
|
|
186
|
+
requesterId: string,
|
|
187
|
+
requesterRoles: UserRole[]
|
|
122
188
|
): Promise<PatientSensitiveInfo | null> => {
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
// }
|
|
189
|
+
// Security check
|
|
190
|
+
await checkSensitiveAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
126
191
|
|
|
127
192
|
// Inicijalizacija dokumenta ako ne postoji
|
|
128
|
-
await initSensitiveInfoDocIfNotExists(db, patientId,
|
|
193
|
+
await initSensitiveInfoDocIfNotExists(db, patientId, requesterId);
|
|
129
194
|
|
|
130
195
|
const sensitiveDoc = await getDoc(getSensitiveInfoDocRef(db, patientId));
|
|
131
196
|
return sensitiveDoc.exists()
|
|
@@ -137,15 +202,15 @@ export const updateSensitiveInfoUtil = async (
|
|
|
137
202
|
db: Firestore,
|
|
138
203
|
patientId: string,
|
|
139
204
|
data: UpdatePatientSensitiveInfoData,
|
|
140
|
-
|
|
205
|
+
requesterId: string,
|
|
206
|
+
requesterRoles: UserRole[],
|
|
141
207
|
mediaService?: MediaService
|
|
142
208
|
): Promise<PatientSensitiveInfo> => {
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
// }
|
|
209
|
+
// Security check
|
|
210
|
+
await checkSensitiveAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
146
211
|
|
|
147
212
|
// Inicijalizacija dokumenta ako ne postoji
|
|
148
|
-
await initSensitiveInfoDocIfNotExists(db, patientId,
|
|
213
|
+
await initSensitiveInfoDocIfNotExists(db, patientId, requesterId);
|
|
149
214
|
|
|
150
215
|
// Process photoUrl if it's a MediaResource and mediaService is provided
|
|
151
216
|
let processedPhotoUrl: string | null | undefined = undefined;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collection,
|
|
3
|
+
doc,
|
|
4
|
+
getDoc,
|
|
5
|
+
getDocs,
|
|
6
|
+
query,
|
|
7
|
+
where,
|
|
8
|
+
setDoc,
|
|
9
|
+
updateDoc,
|
|
10
|
+
Timestamp,
|
|
11
|
+
collectionGroup,
|
|
12
|
+
} from "firebase/firestore";
|
|
13
|
+
import { Firestore } from "firebase/firestore";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import {
|
|
16
|
+
PatientToken,
|
|
17
|
+
CreatePatientTokenData,
|
|
18
|
+
PatientTokenStatus,
|
|
19
|
+
INVITE_TOKENS_COLLECTION,
|
|
20
|
+
} from "../../../types/patient/token.types";
|
|
21
|
+
import {
|
|
22
|
+
patientTokenSchema,
|
|
23
|
+
createPatientTokenSchema,
|
|
24
|
+
} from "../../../validations/patient/token.schema";
|
|
25
|
+
import { getPatientDocRef } from "./docs.utils";
|
|
26
|
+
import { PATIENTS_COLLECTION } from "../../../types/patient";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a token for inviting a patient to claim their profile.
|
|
30
|
+
*
|
|
31
|
+
* @param {Firestore} db - The Firestore database instance.
|
|
32
|
+
* @param {CreatePatientTokenData} data - Data for creating the token.
|
|
33
|
+
* @param {string} createdBy - ID of the user creating the token.
|
|
34
|
+
* @param {() => string} generateId - Function to generate a unique ID.
|
|
35
|
+
* @returns {Promise<PatientToken>} The created token.
|
|
36
|
+
* @throws {Error} If the patient profile is not found, is not a manual profile, or if data is invalid.
|
|
37
|
+
*/
|
|
38
|
+
export const createPatientTokenUtil = async (
|
|
39
|
+
db: Firestore,
|
|
40
|
+
data: CreatePatientTokenData,
|
|
41
|
+
createdBy: string,
|
|
42
|
+
generateId: () => string
|
|
43
|
+
): Promise<PatientToken> => {
|
|
44
|
+
const validatedData = createPatientTokenSchema.parse(data);
|
|
45
|
+
|
|
46
|
+
// Check if patient exists and is a manual profile
|
|
47
|
+
const patientRef = getPatientDocRef(db, validatedData.patientId);
|
|
48
|
+
const patientDoc = await getDoc(patientRef);
|
|
49
|
+
if (!patientDoc.exists() || !patientDoc.data()?.isManual) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Patient profile not found or is not a manually created profile."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Default expiration is 7 days from now if not specified
|
|
56
|
+
const expiration =
|
|
57
|
+
validatedData.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
58
|
+
|
|
59
|
+
const tokenString = generateId().slice(0, 6).toUpperCase();
|
|
60
|
+
|
|
61
|
+
const token: PatientToken = {
|
|
62
|
+
id: generateId(),
|
|
63
|
+
token: tokenString,
|
|
64
|
+
patientId: validatedData.patientId,
|
|
65
|
+
email: validatedData.email,
|
|
66
|
+
clinicId: validatedData.clinicId,
|
|
67
|
+
status: PatientTokenStatus.ACTIVE,
|
|
68
|
+
createdBy: createdBy,
|
|
69
|
+
createdAt: Timestamp.now(),
|
|
70
|
+
expiresAt: Timestamp.fromDate(expiration),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
patientTokenSchema.parse(token);
|
|
74
|
+
|
|
75
|
+
const tokenRef = doc(
|
|
76
|
+
db,
|
|
77
|
+
PATIENTS_COLLECTION,
|
|
78
|
+
validatedData.patientId,
|
|
79
|
+
INVITE_TOKENS_COLLECTION,
|
|
80
|
+
token.id
|
|
81
|
+
);
|
|
82
|
+
await setDoc(tokenRef, token);
|
|
83
|
+
|
|
84
|
+
return token;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validates a patient invitation token.
|
|
89
|
+
*
|
|
90
|
+
* @param {Firestore} db - The Firestore database instance.
|
|
91
|
+
* @param {string} tokenString - The token string to validate.
|
|
92
|
+
* @returns {Promise<PatientToken | null>} The token if found and valid, otherwise null.
|
|
93
|
+
*/
|
|
94
|
+
export const validatePatientTokenUtil = async (
|
|
95
|
+
db: Firestore,
|
|
96
|
+
tokenString: string
|
|
97
|
+
): Promise<PatientToken | null> => {
|
|
98
|
+
const patientsRef = collection(db, PATIENTS_COLLECTION);
|
|
99
|
+
const patientsSnapshot = await getDocs(patientsRef);
|
|
100
|
+
|
|
101
|
+
for (const patientDoc of patientsSnapshot.docs) {
|
|
102
|
+
const tokensRef = collection(
|
|
103
|
+
db,
|
|
104
|
+
patientDoc.ref.path,
|
|
105
|
+
INVITE_TOKENS_COLLECTION
|
|
106
|
+
);
|
|
107
|
+
const q = query(
|
|
108
|
+
tokensRef,
|
|
109
|
+
where("token", "==", tokenString),
|
|
110
|
+
where("status", "==", PatientTokenStatus.ACTIVE),
|
|
111
|
+
where("expiresAt", ">", Timestamp.now())
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const tokenSnapshot = await getDocs(q);
|
|
115
|
+
if (!tokenSnapshot.empty) {
|
|
116
|
+
return tokenSnapshot.docs[0].data() as PatientToken;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Marks a patient invitation token as used.
|
|
125
|
+
*
|
|
126
|
+
* @param {Firestore} db - The Firestore database instance.
|
|
127
|
+
* @param {string} tokenId - The ID of the token to mark as used.
|
|
128
|
+
* @param {string} patientId - The ID of the patient associated with the token.
|
|
129
|
+
* @param {string} userId - The ID of the user who is using the token.
|
|
130
|
+
* @returns {Promise<void>}
|
|
131
|
+
*/
|
|
132
|
+
export const markPatientTokenAsUsedUtil = async (
|
|
133
|
+
db: Firestore,
|
|
134
|
+
tokenId: string,
|
|
135
|
+
patientId: string,
|
|
136
|
+
userId: string
|
|
137
|
+
): Promise<void> => {
|
|
138
|
+
const tokenRef = doc(
|
|
139
|
+
db,
|
|
140
|
+
PATIENTS_COLLECTION,
|
|
141
|
+
patientId,
|
|
142
|
+
INVITE_TOKENS_COLLECTION,
|
|
143
|
+
tokenId
|
|
144
|
+
);
|
|
145
|
+
await updateDoc(tokenRef, {
|
|
146
|
+
status: PatientTokenStatus.USED,
|
|
147
|
+
usedBy: userId,
|
|
148
|
+
usedAt: Timestamp.now(),
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Retrieves all active invitation tokens for a specific clinic.
|
|
154
|
+
* This uses a collection group query to find tokens across all patients.
|
|
155
|
+
*
|
|
156
|
+
* @param {Firestore} db - The Firestore database instance.
|
|
157
|
+
* @param {string} clinicId - The ID of the clinic.
|
|
158
|
+
* @returns {Promise<PatientToken[]>} An array of active tokens for the clinic.
|
|
159
|
+
*/
|
|
160
|
+
export const getActiveInviteTokensByClinicUtil = async (
|
|
161
|
+
db: Firestore,
|
|
162
|
+
clinicId: string
|
|
163
|
+
): Promise<PatientToken[]> => {
|
|
164
|
+
const tokensQuery = query(
|
|
165
|
+
collectionGroup(db, INVITE_TOKENS_COLLECTION),
|
|
166
|
+
where("clinicId", "==", clinicId),
|
|
167
|
+
where("status", "==", PatientTokenStatus.ACTIVE),
|
|
168
|
+
where("expiresAt", ">", Timestamp.now())
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const querySnapshot = await getDocs(tokensQuery);
|
|
172
|
+
|
|
173
|
+
if (querySnapshot.empty) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return querySnapshot.docs.map((doc) => doc.data() as PatientToken);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Retrieves all active invitation tokens for a specific patient.
|
|
182
|
+
*
|
|
183
|
+
* @param {Firestore} db - The Firestore database instance.
|
|
184
|
+
* @param {string} patientId - The ID of the patient.
|
|
185
|
+
* @returns {Promise<PatientToken[]>} An array of active tokens for the patient.
|
|
186
|
+
*/
|
|
187
|
+
export const getActiveInviteTokensByPatientUtil = async (
|
|
188
|
+
db: Firestore,
|
|
189
|
+
patientId: string
|
|
190
|
+
): Promise<PatientToken[]> => {
|
|
191
|
+
const tokensRef = collection(
|
|
192
|
+
db,
|
|
193
|
+
PATIENTS_COLLECTION,
|
|
194
|
+
patientId,
|
|
195
|
+
INVITE_TOKENS_COLLECTION
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const q = query(
|
|
199
|
+
tokensRef,
|
|
200
|
+
where("status", "==", PatientTokenStatus.ACTIVE),
|
|
201
|
+
where("expiresAt", ">", Timestamp.now())
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const querySnapshot = await getDocs(q);
|
|
205
|
+
|
|
206
|
+
if (querySnapshot.empty) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return querySnapshot.docs.map((doc) => doc.data() as PatientToken);
|
|
211
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./practitioner.service";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./procedure.service";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./reviews.service";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./user.service";
|
|
@@ -13,21 +13,21 @@ import {
|
|
|
13
13
|
serverTimestamp,
|
|
14
14
|
FieldValue,
|
|
15
15
|
} from "firebase/firestore";
|
|
16
|
-
import { initializeFirebase } from "
|
|
17
|
-
import { User, UserRole, USERS_COLLECTION, CreateUserData } from "
|
|
18
|
-
import { userSchema } from "
|
|
19
|
-
import { AuthError } from "
|
|
20
|
-
import { USER_ERRORS } from "
|
|
21
|
-
import { AUTH_ERRORS } from "
|
|
16
|
+
import { initializeFirebase } from "../../config/firebase";
|
|
17
|
+
import { User, UserRole, USERS_COLLECTION, CreateUserData } from "../../types";
|
|
18
|
+
import { userSchema } from "../../validations/schemas";
|
|
19
|
+
import { AuthError } from "../../errors/auth.errors";
|
|
20
|
+
import { USER_ERRORS } from "../../errors/user.errors";
|
|
21
|
+
import { AUTH_ERRORS } from "../../errors/auth.errors";
|
|
22
22
|
import { z } from "zod";
|
|
23
|
-
import { BaseService } from "
|
|
24
|
-
import { PatientService } from "
|
|
25
|
-
import { ClinicAdminService } from "
|
|
26
|
-
import { PatientProfile, PATIENTS_COLLECTION } from "
|
|
23
|
+
import { BaseService } from "../base.service";
|
|
24
|
+
import { PatientService } from "../patient/patient.service";
|
|
25
|
+
import { ClinicAdminService } from "../clinic/clinic-admin.service";
|
|
26
|
+
import { PatientProfile, PATIENTS_COLLECTION } from "../../types/patient";
|
|
27
27
|
import { User as FirebaseUser } from "firebase/auth";
|
|
28
28
|
import { Auth } from "firebase/auth";
|
|
29
|
-
import { PractitionerService } from "
|
|
30
|
-
import { CertificationLevel } from "
|
|
29
|
+
import { PractitionerService } from "../practitioner/practitioner.service";
|
|
30
|
+
import { CertificationLevel } from "../../backoffice/types/static/certification.types";
|
|
31
31
|
import { Firestore } from "firebase/firestore";
|
|
32
32
|
import { FirebaseApp } from "firebase/app";
|
|
33
33
|
|
|
@@ -86,6 +86,7 @@ export class UserService extends BaseService {
|
|
|
86
86
|
groupToken?: string;
|
|
87
87
|
groupId?: string;
|
|
88
88
|
};
|
|
89
|
+
patientInviteToken?: string;
|
|
89
90
|
skipProfileCreation?: boolean;
|
|
90
91
|
}
|
|
91
92
|
): Promise<User> {
|
|
@@ -147,6 +148,7 @@ export class UserService extends BaseService {
|
|
|
147
148
|
groupToken?: string;
|
|
148
149
|
groupId?: string;
|
|
149
150
|
};
|
|
151
|
+
patientInviteToken?: string;
|
|
150
152
|
skipProfileCreation?: boolean;
|
|
151
153
|
}
|
|
152
154
|
): Promise<{
|
|
@@ -163,6 +165,52 @@ export class UserService extends BaseService {
|
|
|
163
165
|
for (const role of roles) {
|
|
164
166
|
switch (role) {
|
|
165
167
|
case UserRole.PATIENT:
|
|
168
|
+
// If a token is provided, claim the existing manual profile
|
|
169
|
+
if (options?.patientInviteToken) {
|
|
170
|
+
const patientService = this.getPatientService();
|
|
171
|
+
const token = await patientService.validatePatientToken(
|
|
172
|
+
options.patientInviteToken
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (!token) {
|
|
176
|
+
throw new Error("Invalid or expired patient invitation token.");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Get the patient profile
|
|
180
|
+
const patientProfile = await patientService.getPatientProfile(
|
|
181
|
+
token.patientId
|
|
182
|
+
);
|
|
183
|
+
if (!patientProfile || !patientProfile.isManual) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
"Patient profile not found or has already been claimed."
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check if user already has a patient profile
|
|
190
|
+
if (
|
|
191
|
+
(await this.getUserById(userId)).patientProfile ||
|
|
192
|
+
patientProfile.userRef
|
|
193
|
+
) {
|
|
194
|
+
throw new Error("User already has a patient profile.");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Claim the profile: link userRef and set isManual to false
|
|
198
|
+
await patientService.updatePatientProfile(patientProfile.id, {
|
|
199
|
+
userRef: userId,
|
|
200
|
+
isManual: false,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Mark the token as used
|
|
204
|
+
await patientService.markPatientTokenAsUsed(
|
|
205
|
+
token.id,
|
|
206
|
+
token.patientId,
|
|
207
|
+
userId
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
profiles.patientProfile = patientProfile.id;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
|
|
166
214
|
const patientProfile =
|
|
167
215
|
await this.getPatientService().createPatientProfile({
|
|
168
216
|
userRef: userId,
|
|
@@ -174,6 +222,7 @@ export class UserService extends BaseService {
|
|
|
174
222
|
},
|
|
175
223
|
isActive: true,
|
|
176
224
|
isVerified: false,
|
|
225
|
+
isManual: false, // Explicitly set to false for standard signups
|
|
177
226
|
});
|
|
178
227
|
profiles.patientProfile = patientProfile.id;
|
|
179
228
|
break;
|
|
@@ -13,21 +13,21 @@ import {
|
|
|
13
13
|
serverTimestamp,
|
|
14
14
|
FieldValue,
|
|
15
15
|
} from "firebase/firestore";
|
|
16
|
-
import { initializeFirebase } from "
|
|
17
|
-
import { User, UserRole, USERS_COLLECTION, CreateUserData } from "
|
|
18
|
-
import { userSchema } from "
|
|
19
|
-
import { AuthError } from "
|
|
20
|
-
import { USER_ERRORS } from "
|
|
21
|
-
import { AUTH_ERRORS } from "
|
|
16
|
+
import { initializeFirebase } from "../../config/firebase";
|
|
17
|
+
import { User, UserRole, USERS_COLLECTION, CreateUserData } from "../../types";
|
|
18
|
+
import { userSchema } from "../../validations/schemas";
|
|
19
|
+
import { AuthError } from "../../errors/auth.errors";
|
|
20
|
+
import { USER_ERRORS } from "../../errors/user.errors";
|
|
21
|
+
import { AUTH_ERRORS } from "../../errors/auth.errors";
|
|
22
22
|
import { z } from "zod";
|
|
23
|
-
import { BaseService } from "
|
|
24
|
-
import { PatientService } from "
|
|
25
|
-
import { ClinicAdminService } from "
|
|
26
|
-
import { PatientProfile, PATIENTS_COLLECTION } from "
|
|
23
|
+
import { BaseService } from "../base.service";
|
|
24
|
+
import { PatientService } from "../patient/patient.service";
|
|
25
|
+
import { ClinicAdminService } from "../clinic/clinic-admin.service";
|
|
26
|
+
import { PatientProfile, PATIENTS_COLLECTION } from "../../types/patient";
|
|
27
27
|
import { User as FirebaseUser } from "firebase/auth";
|
|
28
28
|
import { Auth } from "firebase/auth";
|
|
29
|
-
import { PractitionerService } from "
|
|
30
|
-
import { CertificationLevel } from "
|
|
29
|
+
import { PractitionerService } from "../practitioner/practitioner.service";
|
|
30
|
+
import { CertificationLevel } from "../../backoffice/types/static/certification.types";
|
|
31
31
|
import { Firestore } from "firebase/firestore";
|
|
32
32
|
import { FirebaseApp } from "firebase/app";
|
|
33
33
|
|
package/src/types/index.ts
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export enum UserRole {
|
|
6
|
-
PATIENT = "patient",
|
|
7
|
-
PRACTITIONER = "practitioner",
|
|
8
|
-
APP_ADMIN = "app_admin",
|
|
9
|
-
CLINIC_ADMIN = "clinic_admin",
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface User {
|
|
13
|
-
uid: string;
|
|
14
|
-
email: string | null;
|
|
15
|
-
roles: UserRole[];
|
|
16
|
-
isAnonymous: boolean;
|
|
17
|
-
createdAt: any;
|
|
18
|
-
updatedAt: any;
|
|
19
|
-
lastLoginAt: any;
|
|
20
|
-
patientProfile?: string;
|
|
21
|
-
practitionerProfile?: string;
|
|
22
|
-
adminProfile?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface CreateUserData {
|
|
26
|
-
uid: string;
|
|
27
|
-
email: string | null;
|
|
28
|
-
roles: UserRole[];
|
|
29
|
-
isAnonymous: boolean;
|
|
30
|
-
createdAt: FieldValue;
|
|
31
|
-
updatedAt: FieldValue;
|
|
32
|
-
lastLoginAt: FieldValue;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const USERS_COLLECTION = "users";
|
|
36
|
-
|
|
37
|
-
// Firebase tipovi
|
|
38
|
-
export type FirebaseUser = FirebaseAuthUser;
|
|
39
|
-
|
|
40
|
-
// Documentation Templates
|
|
41
|
-
export * from "./documentation-templates";
|
|
1
|
+
// Export all type definitions for easy access
|
|
2
|
+
|
|
3
|
+
// Top-level user types
|
|
4
|
+
export * from "./user";
|
|
42
5
|
|
|
43
|
-
//
|
|
6
|
+
// Appointment types
|
|
7
|
+
export * from "./appointment";
|
|
8
|
+
|
|
9
|
+
// Calendar types
|
|
44
10
|
export * from "./calendar";
|
|
11
|
+
export * from "./calendar/synced-calendar.types";
|
|
12
|
+
|
|
13
|
+
// Clinic types
|
|
14
|
+
export * from "./clinic";
|
|
15
|
+
export * from "./clinic/practitioner-invite.types";
|
|
16
|
+
export * from "./clinic/preferences.types";
|
|
17
|
+
|
|
18
|
+
// Documentation Templates types
|
|
19
|
+
export * from "./documentation-templates";
|
|
20
|
+
|
|
21
|
+
// Notifications types
|
|
22
|
+
export * from "./notifications";
|
|
23
|
+
|
|
24
|
+
// Patient types
|
|
25
|
+
export * from "./patient";
|
|
26
|
+
export * from "./patient/allergies";
|
|
27
|
+
export * from "./patient/medical-info.types";
|
|
28
|
+
export * from "./patient/patient-requirements";
|
|
29
|
+
export * from "./patient/token.types";
|
|
30
|
+
|
|
31
|
+
// Practitioner types
|
|
32
|
+
export * from "./practitioner";
|
|
33
|
+
|
|
34
|
+
// Procedure types
|
|
35
|
+
export * from "./procedure";
|
|
36
|
+
|
|
37
|
+
// Profile types
|
|
38
|
+
export * from "./profile";
|
|
39
|
+
|
|
40
|
+
// Reviews types
|
|
41
|
+
export * from "./reviews";
|
|
42
|
+
|
|
43
|
+
// User types
|
|
44
|
+
export * from "./user";
|
|
@@ -66,7 +66,7 @@ export interface GamificationInfo {
|
|
|
66
66
|
*/
|
|
67
67
|
export interface PatientLocationInfo {
|
|
68
68
|
patientId: string;
|
|
69
|
-
userRef
|
|
69
|
+
userRef?: string;
|
|
70
70
|
locationData: LocationData; // Samo za geopretragu
|
|
71
71
|
createdAt: Timestamp;
|
|
72
72
|
updatedAt: Timestamp;
|
|
@@ -77,7 +77,7 @@ export interface PatientLocationInfo {
|
|
|
77
77
|
*/
|
|
78
78
|
export interface CreatePatientLocationInfoData {
|
|
79
79
|
patientId: string;
|
|
80
|
-
userRef
|
|
80
|
+
userRef?: string;
|
|
81
81
|
locationData: LocationData;
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -94,7 +94,7 @@ export interface UpdatePatientLocationInfoData
|
|
|
94
94
|
*/
|
|
95
95
|
export interface PatientSensitiveInfo {
|
|
96
96
|
patientId: string;
|
|
97
|
-
userRef
|
|
97
|
+
userRef?: string;
|
|
98
98
|
photoUrl?: string | null;
|
|
99
99
|
firstName: string;
|
|
100
100
|
lastName: string;
|
|
@@ -114,7 +114,7 @@ export interface PatientSensitiveInfo {
|
|
|
114
114
|
*/
|
|
115
115
|
export interface CreatePatientSensitiveInfoData {
|
|
116
116
|
patientId: string;
|
|
117
|
-
userRef
|
|
117
|
+
userRef?: string;
|
|
118
118
|
photoUrl?: MediaResource | null;
|
|
119
119
|
firstName: string;
|
|
120
120
|
lastName: string;
|
|
@@ -162,12 +162,13 @@ export interface PatientClinic {
|
|
|
162
162
|
*/
|
|
163
163
|
export interface PatientProfile {
|
|
164
164
|
id: string;
|
|
165
|
-
userRef
|
|
165
|
+
userRef?: string;
|
|
166
166
|
displayName: string; // Inicijali ili pseudonim
|
|
167
167
|
gamification: GamificationInfo;
|
|
168
168
|
expoTokens: string[];
|
|
169
169
|
isActive: boolean;
|
|
170
170
|
isVerified: boolean;
|
|
171
|
+
isManual: boolean; // Indicates if the profile was created manually by a clinic
|
|
171
172
|
phoneNumber?: string | null;
|
|
172
173
|
dateOfBirth?: Timestamp | null;
|
|
173
174
|
doctors: PatientDoctor[]; // Lista doktora pacijenta
|
|
@@ -182,12 +183,13 @@ export interface PatientProfile {
|
|
|
182
183
|
* Tip za kreiranje novog Patient profila
|
|
183
184
|
*/
|
|
184
185
|
export interface CreatePatientProfileData {
|
|
185
|
-
userRef
|
|
186
|
+
userRef?: string;
|
|
186
187
|
displayName: string;
|
|
187
188
|
expoTokens: string[];
|
|
188
189
|
gamification?: GamificationInfo;
|
|
189
190
|
isActive: boolean;
|
|
190
191
|
isVerified: boolean;
|
|
192
|
+
isManual: boolean;
|
|
191
193
|
doctors?: PatientDoctor[];
|
|
192
194
|
clinics?: PatientClinic[];
|
|
193
195
|
doctorIds?: string[]; // Initialize as empty or with initial doctors
|
|
@@ -204,6 +206,31 @@ export interface UpdatePatientProfileData
|
|
|
204
206
|
// Note: doctors, clinics, doctorIds, clinicIds should ideally be updated via specific methods (add/removeDoctor/Clinic)
|
|
205
207
|
}
|
|
206
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Data required for a clinic admin to manually create a new patient profile.
|
|
211
|
+
* This patient does not have a user account initially.
|
|
212
|
+
*/
|
|
213
|
+
export interface CreateManualPatientData {
|
|
214
|
+
/** The clinic ID where the patient is being created. */
|
|
215
|
+
clinicId: string;
|
|
216
|
+
/** Patient's first name. */
|
|
217
|
+
firstName: string;
|
|
218
|
+
/** Patient's last name. */
|
|
219
|
+
lastName: string;
|
|
220
|
+
/** Patient's date of birth. */
|
|
221
|
+
dateOfBirth: Timestamp | null;
|
|
222
|
+
/** Patient's gender. */
|
|
223
|
+
gender: Gender;
|
|
224
|
+
/** Optional phone number. */
|
|
225
|
+
phoneNumber?: string;
|
|
226
|
+
/** Optional email address. */
|
|
227
|
+
email?: string;
|
|
228
|
+
/** Optional address information. */
|
|
229
|
+
addressData?: AddressData;
|
|
230
|
+
/** Optional notes about the patient. */
|
|
231
|
+
notes?: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
207
234
|
/**
|
|
208
235
|
* Parameters for searching patient profiles.
|
|
209
236
|
*/
|