@blackcode_sa/metaestetics-api 1.13.21 → 1.14.1

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.
@@ -1002,6 +1002,296 @@ export class AuthService extends BaseService {
1002
1002
  }
1003
1003
  }
1004
1004
 
1005
+ /**
1006
+ * Signs up or signs in a practitioner with Google authentication.
1007
+ * Checks for existing practitioner account or draft profiles.
1008
+ *
1009
+ * @param idToken - The Google ID token obtained from the mobile app
1010
+ * @returns Object containing user, practitioner (if exists), and draft profiles (if any)
1011
+ */
1012
+ async signUpPractitionerWithGoogle(
1013
+ idToken: string
1014
+ ): Promise<{
1015
+ user: User;
1016
+ practitioner: Practitioner | null;
1017
+ draftProfiles: Practitioner[];
1018
+ }> {
1019
+ try {
1020
+ console.log('[AUTH] Starting practitioner Google Sign-In/Sign-Up');
1021
+
1022
+ // Extract email from Google ID token
1023
+ let email: string | undefined;
1024
+ try {
1025
+ const payloadBase64 = idToken.split('.')[1];
1026
+ const payloadJson = globalThis.atob
1027
+ ? globalThis.atob(payloadBase64)
1028
+ : Buffer.from(payloadBase64, 'base64').toString('utf8');
1029
+ const payload = JSON.parse(payloadJson);
1030
+ email = payload.email as string | undefined;
1031
+ } catch (decodeError) {
1032
+ console.error('[AUTH] Failed to decode email from Google ID token:', decodeError);
1033
+ throw new AuthError(
1034
+ 'Unable to read email from Google token. Please try again.',
1035
+ 'AUTH/INVALID_GOOGLE_TOKEN',
1036
+ 400,
1037
+ );
1038
+ }
1039
+
1040
+ if (!email) {
1041
+ throw new AuthError(
1042
+ 'Unable to read email from Google token. Please try again.',
1043
+ 'AUTH/INVALID_GOOGLE_TOKEN',
1044
+ 400,
1045
+ );
1046
+ }
1047
+
1048
+ const normalizedEmail = email.toLowerCase().trim();
1049
+ console.log('[AUTH] Extracted email from Google token:', normalizedEmail);
1050
+
1051
+ // Check if user already exists in Firebase Auth
1052
+ const methods = await fetchSignInMethodsForEmail(this.auth, normalizedEmail);
1053
+ const hasGoogleMethod = methods.includes(GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD);
1054
+ const hasEmailMethod = methods.includes(EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD);
1055
+
1056
+ const practitionerService = new PractitionerService(this.db, this.auth, this.app);
1057
+
1058
+ // Case 1: User exists with Google provider - sign in and check for practitioner profile
1059
+ if (hasGoogleMethod) {
1060
+ console.log('[AUTH] User exists with Google provider, signing in');
1061
+ const credential = GoogleAuthProvider.credential(idToken);
1062
+ const { user: firebaseUser } = await signInWithCredential(this.auth, credential);
1063
+
1064
+ let existingUser: User | null = null;
1065
+ try {
1066
+ existingUser = await this.userService.getUserById(firebaseUser.uid);
1067
+ console.log('[AUTH] User document found:', existingUser.uid);
1068
+ } catch (userError: any) {
1069
+ // User document doesn't exist in Firestore
1070
+ console.log('[AUTH] User document not found in Firestore, checking for draft profiles', {
1071
+ errorCode: userError?.code,
1072
+ errorMessage: userError?.message,
1073
+ errorType: userError?.constructor?.name,
1074
+ isAuthError: userError instanceof AuthError,
1075
+ });
1076
+
1077
+ // Check for draft profiles before signing out
1078
+ const practitionerService = new PractitionerService(this.db, this.auth, this.app);
1079
+ const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1080
+
1081
+ console.log('[AUTH] Draft profiles check result:', {
1082
+ email: normalizedEmail,
1083
+ draftProfilesCount: draftProfiles.length,
1084
+ draftProfileIds: draftProfiles.map(p => p.id),
1085
+ });
1086
+
1087
+ if (draftProfiles.length === 0) {
1088
+ // No draft profiles - sign out and throw error
1089
+ console.log('[AUTH] No draft profiles found, signing out and throwing error');
1090
+ try {
1091
+ await firebaseSignOut(this.auth);
1092
+ } catch (signOutError) {
1093
+ console.warn('[AUTH] Error signing out Firebase user (non-critical):', signOutError);
1094
+ }
1095
+ throw new AuthError(
1096
+ 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1097
+ 'AUTH/NO_DRAFT_PROFILES',
1098
+ 404,
1099
+ );
1100
+ }
1101
+
1102
+ // Draft profiles exist - create user document and continue
1103
+ console.log('[AUTH] Draft profiles found, creating user document');
1104
+ existingUser = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
1105
+ skipProfileCreation: true,
1106
+ });
1107
+ console.log('[AUTH] Created user document for existing Firebase user with draft profiles:', existingUser.uid);
1108
+ }
1109
+
1110
+ if (!existingUser) {
1111
+ await firebaseSignOut(this.auth);
1112
+ throw new AuthError(
1113
+ 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1114
+ 'AUTH/NO_DRAFT_PROFILES',
1115
+ 404,
1116
+ );
1117
+ }
1118
+
1119
+ // Check if user has practitioner profile
1120
+ let practitioner: Practitioner | null = null;
1121
+ if (existingUser.practitionerProfile) {
1122
+ practitioner = await practitionerService.getPractitioner(existingUser.practitionerProfile);
1123
+ }
1124
+
1125
+ // Check for any new draft profiles
1126
+ const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1127
+
1128
+ return {
1129
+ user: existingUser,
1130
+ practitioner,
1131
+ draftProfiles,
1132
+ };
1133
+ }
1134
+
1135
+ // Case 2: User exists with email/password - need to link Google provider
1136
+ // Firebase doesn't allow linking without being signed in first
1137
+ // So we need to ask user to sign in with email/password first, then link
1138
+ if (hasEmailMethod && !hasGoogleMethod) {
1139
+ console.log('[AUTH] User exists with email/password only');
1140
+ throw new AuthError(
1141
+ 'An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.',
1142
+ 'AUTH/EMAIL_ALREADY_EXISTS',
1143
+ 409,
1144
+ );
1145
+ }
1146
+
1147
+ // Case 3: New user - sign in with Google and check for draft profiles
1148
+ console.log('[AUTH] Signing in with Google credential');
1149
+ const credential = GoogleAuthProvider.credential(idToken);
1150
+
1151
+ let firebaseUser: FirebaseUser;
1152
+ try {
1153
+ const result = await signInWithCredential(this.auth, credential);
1154
+ firebaseUser = result.user;
1155
+ } catch (error: any) {
1156
+ // If sign-in fails because email already exists with different provider
1157
+ if (error.code === 'auth/account-exists-with-different-credential') {
1158
+ throw new AuthError(
1159
+ 'An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.',
1160
+ 'AUTH/EMAIL_ALREADY_EXISTS',
1161
+ 409,
1162
+ );
1163
+ }
1164
+ throw error;
1165
+ }
1166
+
1167
+ // Check for existing User document (in case user had email/password account that was just linked)
1168
+ let existingUser: User | null = null;
1169
+ try {
1170
+ const existingUserDoc = await this.userService.getUserById(firebaseUser.uid);
1171
+ if (existingUserDoc) {
1172
+ existingUser = existingUserDoc;
1173
+ console.log('[AUTH] Found existing User document');
1174
+ }
1175
+ } catch (error) {
1176
+ console.error('[AUTH] Error checking for existing user:', error);
1177
+ // Continue with new user creation
1178
+ }
1179
+
1180
+ // Check for draft profiles
1181
+ console.log('[AUTH] Checking for draft profiles');
1182
+ let draftProfiles: Practitioner[] = [];
1183
+ try {
1184
+ draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1185
+ console.log('[AUTH] Draft profiles check complete', { count: draftProfiles.length });
1186
+ } catch (draftCheckError: any) {
1187
+ console.error('[AUTH] Error checking draft profiles:', draftCheckError);
1188
+ // If checking draft profiles fails, sign out and throw appropriate error
1189
+ try {
1190
+ await firebaseSignOut(this.auth);
1191
+ } catch (signOutError) {
1192
+ console.warn('[AUTH] Error signing out Firebase user (non-critical):', signOutError);
1193
+ }
1194
+ throw new AuthError(
1195
+ 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1196
+ 'AUTH/NO_DRAFT_PROFILES',
1197
+ 404,
1198
+ );
1199
+ }
1200
+
1201
+ let user: User;
1202
+ if (existingUser) {
1203
+ // User exists - use existing account
1204
+ user = existingUser;
1205
+ console.log('[AUTH] Using existing user account');
1206
+ } else {
1207
+ // For new users: Only create account if there are draft profiles OR if user already has a practitioner profile
1208
+ // Since doctors can only join via clinic invitation, we should not create accounts without invitations
1209
+ if (draftProfiles.length === 0) {
1210
+ console.log('[AUTH] No draft profiles found, signing out and throwing error');
1211
+ // Sign out the Firebase user since we're not creating an account
1212
+ // Wrap in try-catch to handle any sign-out errors gracefully
1213
+ try {
1214
+ await firebaseSignOut(this.auth);
1215
+ } catch (signOutError) {
1216
+ console.warn('[AUTH] Error signing out Firebase user (non-critical):', signOutError);
1217
+ // Continue anyway - the important part is we're not creating the account
1218
+ }
1219
+ const noDraftError = new AuthError(
1220
+ 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1221
+ 'AUTH/NO_DRAFT_PROFILES',
1222
+ 404,
1223
+ );
1224
+ console.log('[AUTH] Throwing NO_DRAFT_PROFILES error:', noDraftError.code);
1225
+ throw noDraftError;
1226
+ }
1227
+
1228
+ // Create new user document only if draft profiles exist
1229
+ user = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
1230
+ skipProfileCreation: true,
1231
+ });
1232
+ console.log('[AUTH] Created new user account with draft profiles available');
1233
+ }
1234
+
1235
+ // Check if user already has practitioner profile
1236
+ let practitioner: Practitioner | null = null;
1237
+ if (user.practitionerProfile) {
1238
+ practitioner = await practitionerService.getPractitioner(user.practitionerProfile);
1239
+ }
1240
+
1241
+ console.log('[AUTH] Google Sign-In complete', {
1242
+ userId: user.uid,
1243
+ hasPractitioner: !!practitioner,
1244
+ draftProfilesCount: draftProfiles.length,
1245
+ });
1246
+
1247
+ return {
1248
+ user,
1249
+ practitioner,
1250
+ draftProfiles,
1251
+ };
1252
+ } catch (error: any) {
1253
+ console.error('[AUTH] Error in signUpPractitionerWithGoogle:', error);
1254
+ console.error('[AUTH] Error type:', error?.constructor?.name);
1255
+ console.error('[AUTH] Error instanceof AuthError:', error instanceof AuthError);
1256
+ console.error('[AUTH] Error code:', error?.code);
1257
+ console.error('[AUTH] Error message:', error?.message);
1258
+
1259
+ // Preserve AuthError instances (like NO_DRAFT_PROFILES) without wrapping
1260
+ if (error instanceof AuthError) {
1261
+ console.log('[AUTH] Preserving AuthError:', error.code);
1262
+ throw error;
1263
+ }
1264
+
1265
+ // Check if error message contains NO_DRAFT_PROFILES before wrapping
1266
+ const errorMessage = error?.message || error?.toString() || '';
1267
+ if (errorMessage.includes('NO_DRAFT_PROFILES') || errorMessage.includes('clinic invitation')) {
1268
+ console.log('[AUTH] Detected clinic invitation error in message, converting to AuthError');
1269
+ throw new AuthError(
1270
+ 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1271
+ 'AUTH/NO_DRAFT_PROFILES',
1272
+ 404,
1273
+ );
1274
+ }
1275
+
1276
+ // For other errors, wrap them but preserve the original message if it's helpful
1277
+ const wrappedError = handleFirebaseError(error);
1278
+ console.log('[AUTH] Wrapped error:', wrappedError.message);
1279
+
1280
+ // If the wrapped error message is generic, try to preserve more context
1281
+ if (wrappedError.message.includes('permissions') || wrappedError.message.includes('Account creation failed')) {
1282
+ // This might be a permissions error during sign-out or user creation
1283
+ // If we got here and there were no draft profiles, it's likely the same issue
1284
+ throw new AuthError(
1285
+ 'No clinic invitation found for this email. Please contact your clinic administrator to receive an invitation, or use the token provided by your clinic.',
1286
+ 'AUTH/NO_DRAFT_PROFILES',
1287
+ 404,
1288
+ );
1289
+ }
1290
+
1291
+ throw wrappedError;
1292
+ }
1293
+ }
1294
+
1005
1295
  /**
1006
1296
  * Links a Google account to the currently signed-in user using an ID token.
1007
1297
  * This is used to upgrade an anonymous user or to allow an existing user
@@ -158,6 +158,11 @@ export class PractitionerService extends BaseService {
158
158
  ): Promise<PractitionerBasicInfo> {
159
159
  const processedBasicInfo = { ...basicInfo };
160
160
 
161
+ // Normalize email to lowercase to ensure consistent matching
162
+ if (processedBasicInfo.email) {
163
+ processedBasicInfo.email = processedBasicInfo.email.toLowerCase().trim();
164
+ }
165
+
161
166
  // Handle profile photo upload if needed
162
167
  if (basicInfo.profileImageUrl) {
163
168
  const uploadedUrl = await this.handleProfilePhotoUpload(
@@ -714,6 +719,344 @@ export class PractitionerService extends BaseService {
714
719
  }
715
720
  }
716
721
 
722
+ /**
723
+ * Finds all draft practitioner profiles by email address
724
+ * Used when a doctor signs in with Google to show all clinic invitations
725
+ *
726
+ * @param email - Email address to search for
727
+ * @returns Array of draft practitioner profiles with clinic information
728
+ *
729
+ * @remarks
730
+ * Requires Firestore composite index on:
731
+ * - Collection: practitioners
732
+ * - Fields: basicInfo.email (Ascending), status (Ascending), userRef (Ascending)
733
+ */
734
+ async getDraftProfilesByEmail(
735
+ email: string
736
+ ): Promise<Practitioner[]> {
737
+ try {
738
+ const normalizedEmail = email.toLowerCase().trim();
739
+
740
+ console.log("[PRACTITIONER] Searching for all draft practitioners by email", {
741
+ email: normalizedEmail,
742
+ originalEmail: email,
743
+ });
744
+
745
+ const q = query(
746
+ collection(this.db, PRACTITIONERS_COLLECTION),
747
+ where("basicInfo.email", "==", normalizedEmail),
748
+ where("status", "==", PractitionerStatus.DRAFT),
749
+ where("userRef", "==", "")
750
+ );
751
+
752
+ const querySnapshot = await getDocs(q);
753
+
754
+ if (querySnapshot.empty) {
755
+ console.log("[PRACTITIONER] No draft practitioners found for email", {
756
+ email: normalizedEmail,
757
+ originalEmail: email,
758
+ });
759
+
760
+ // Debug: Try to find ANY practitioners with this email (regardless of status)
761
+ const debugQ = query(
762
+ collection(this.db, PRACTITIONERS_COLLECTION),
763
+ where("basicInfo.email", "==", normalizedEmail),
764
+ limit(5)
765
+ );
766
+ const debugSnapshot = await getDocs(debugQ);
767
+ console.log("[PRACTITIONER] Debug: Found practitioners with this email (any status):", {
768
+ count: debugSnapshot.size,
769
+ practitioners: debugSnapshot.docs.map(doc => ({
770
+ id: doc.id,
771
+ email: doc.data().basicInfo?.email,
772
+ status: doc.data().status,
773
+ userRef: doc.data().userRef,
774
+ })),
775
+ });
776
+
777
+ return [];
778
+ }
779
+
780
+ const draftPractitioners = querySnapshot.docs.map(
781
+ (doc) => doc.data() as Practitioner
782
+ );
783
+
784
+ console.log("[PRACTITIONER] Found draft practitioners", {
785
+ email: normalizedEmail,
786
+ count: draftPractitioners.length,
787
+ practitionerIds: draftPractitioners.map((p) => p.id),
788
+ });
789
+
790
+ return draftPractitioners;
791
+ } catch (error) {
792
+ console.error(
793
+ "[PRACTITIONER] Error finding draft practitioners by email:",
794
+ error
795
+ );
796
+ // If query fails (e.g., index not created), return empty array
797
+ return [];
798
+ }
799
+ }
800
+
801
+ /**
802
+ * Claims a draft practitioner profile and links it to a user account
803
+ * Used when a doctor selects which clinic(s) to join after Google Sign-In
804
+ *
805
+ * @param practitionerId - ID of the draft practitioner profile to claim
806
+ * @param userId - ID of the user account to link the profile to
807
+ * @returns The claimed practitioner profile
808
+ */
809
+ async claimDraftProfileWithGoogle(
810
+ practitionerId: string,
811
+ userId: string
812
+ ): Promise<Practitioner> {
813
+ try {
814
+ console.log("[PRACTITIONER] Claiming draft profile with Google", {
815
+ practitionerId,
816
+ userId,
817
+ });
818
+
819
+ // Get the draft practitioner profile
820
+ const practitioner = await this.getPractitioner(practitionerId);
821
+ if (!practitioner) {
822
+ throw new Error(`Practitioner ${practitionerId} not found`);
823
+ }
824
+
825
+ // Ensure practitioner is in DRAFT status
826
+ if (practitioner.status !== PractitionerStatus.DRAFT) {
827
+ throw new Error("This practitioner profile has already been claimed");
828
+ }
829
+
830
+ // Check if user already has a practitioner profile
831
+ const existingPractitioner = await this.getPractitionerByUserRef(userId);
832
+ if (existingPractitioner) {
833
+ // User already has a profile - merge clinics from draft profile into existing profile
834
+ console.log("[PRACTITIONER] User already has profile, merging clinics");
835
+
836
+ // Merge clinics (avoid duplicates)
837
+ const mergedClinics = Array.from(new Set([
838
+ ...existingPractitioner.clinics,
839
+ ...practitioner.clinics,
840
+ ]));
841
+
842
+ // Merge clinic working hours
843
+ const mergedWorkingHours = [...existingPractitioner.clinicWorkingHours];
844
+ for (const workingHours of practitioner.clinicWorkingHours) {
845
+ if (!mergedWorkingHours.find(wh => wh.clinicId === workingHours.clinicId)) {
846
+ mergedWorkingHours.push(workingHours);
847
+ }
848
+ }
849
+
850
+ // Merge clinics info (avoid duplicates)
851
+ const mergedClinicsInfo = [...existingPractitioner.clinicsInfo];
852
+ for (const clinicInfo of practitioner.clinicsInfo) {
853
+ if (!mergedClinicsInfo.find(ci => ci.id === clinicInfo.id)) {
854
+ mergedClinicsInfo.push(clinicInfo);
855
+ }
856
+ }
857
+
858
+ // Update existing practitioner with merged data
859
+ const updatedPractitioner = await this.updatePractitioner(existingPractitioner.id, {
860
+ clinics: mergedClinics,
861
+ clinicWorkingHours: mergedWorkingHours,
862
+ clinicsInfo: mergedClinicsInfo,
863
+ });
864
+
865
+ // Delete the draft profile since we've merged it
866
+ await deleteDoc(doc(this.db, PRACTITIONERS_COLLECTION, practitionerId));
867
+
868
+ // Mark all active tokens for the draft practitioner as used
869
+ const activeTokens = await this.getPractitionerActiveTokens(practitionerId);
870
+ for (const token of activeTokens) {
871
+ await this.markTokenAsUsed(token.id, practitionerId, userId);
872
+ }
873
+
874
+ return updatedPractitioner;
875
+ }
876
+
877
+ // Claim the profile by linking it to the user
878
+ const updatedPractitioner = await this.updatePractitioner(practitioner.id, {
879
+ userRef: userId,
880
+ status: PractitionerStatus.ACTIVE,
881
+ });
882
+
883
+ // Mark all active tokens for this practitioner as used
884
+ const activeTokens = await this.getPractitionerActiveTokens(practitionerId);
885
+ for (const token of activeTokens) {
886
+ await this.markTokenAsUsed(token.id, practitionerId, userId);
887
+ }
888
+
889
+ console.log("[PRACTITIONER] Draft profile claimed successfully", {
890
+ practitionerId: updatedPractitioner.id,
891
+ userId,
892
+ });
893
+
894
+ return updatedPractitioner;
895
+ } catch (error) {
896
+ console.error(
897
+ "[PRACTITIONER] Error claiming draft profile with Google:",
898
+ error
899
+ );
900
+ throw error;
901
+ }
902
+ }
903
+
904
+ /**
905
+ * Claims multiple draft practitioner profiles and merges them into one profile
906
+ * Used when a doctor selects multiple clinics to join after Google Sign-In
907
+ *
908
+ * @param practitionerIds - Array of draft practitioner profile IDs to claim
909
+ * @param userId - ID of the user account to link the profiles to
910
+ * @returns The claimed practitioner profile (first one becomes main, others merged)
911
+ */
912
+ async claimMultipleDraftProfilesWithGoogle(
913
+ practitionerIds: string[],
914
+ userId: string
915
+ ): Promise<Practitioner> {
916
+ try {
917
+ if (practitionerIds.length === 0) {
918
+ throw new Error("No practitioner IDs provided");
919
+ }
920
+
921
+ console.log("[PRACTITIONER] Claiming multiple draft profiles with Google", {
922
+ practitionerIds,
923
+ userId,
924
+ count: practitionerIds.length,
925
+ });
926
+
927
+ // Get all draft profiles
928
+ const draftProfiles = await Promise.all(
929
+ practitionerIds.map(id => this.getPractitioner(id))
930
+ );
931
+
932
+ // Filter out nulls and ensure all are drafts
933
+ const validDrafts = draftProfiles.filter((p): p is Practitioner => {
934
+ if (!p) return false;
935
+ if (p.status !== PractitionerStatus.DRAFT) {
936
+ throw new Error(`Practitioner ${p.id} has already been claimed`);
937
+ }
938
+ return true;
939
+ });
940
+
941
+ if (validDrafts.length === 0) {
942
+ throw new Error("No valid draft profiles found");
943
+ }
944
+
945
+ // Check if user already has a practitioner profile
946
+ const existingPractitioner = await this.getPractitionerByUserRef(userId);
947
+
948
+ if (existingPractitioner) {
949
+ // Merge all draft profiles into existing profile
950
+ let mergedClinics = new Set(existingPractitioner.clinics);
951
+ let mergedWorkingHours = [...existingPractitioner.clinicWorkingHours];
952
+ let mergedClinicsInfo = [...existingPractitioner.clinicsInfo];
953
+
954
+ for (const draft of validDrafts) {
955
+ // Merge clinics
956
+ draft.clinics.forEach(clinicId => mergedClinics.add(clinicId));
957
+
958
+ // Merge working hours
959
+ for (const workingHours of draft.clinicWorkingHours) {
960
+ if (!mergedWorkingHours.find(wh => wh.clinicId === workingHours.clinicId)) {
961
+ mergedWorkingHours.push(workingHours);
962
+ }
963
+ }
964
+
965
+ // Merge clinics info
966
+ for (const clinicInfo of draft.clinicsInfo) {
967
+ if (!mergedClinicsInfo.find(ci => ci.id === clinicInfo.id)) {
968
+ mergedClinicsInfo.push(clinicInfo);
969
+ }
970
+ }
971
+ }
972
+
973
+ // Update existing practitioner
974
+ const updatedPractitioner = await this.updatePractitioner(existingPractitioner.id, {
975
+ clinics: Array.from(mergedClinics),
976
+ clinicWorkingHours: mergedWorkingHours,
977
+ clinicsInfo: mergedClinicsInfo,
978
+ });
979
+
980
+ // Delete all draft profiles
981
+ for (const draft of validDrafts) {
982
+ await deleteDoc(doc(this.db, PRACTITIONERS_COLLECTION, draft.id));
983
+
984
+ // Mark all active tokens as used
985
+ const activeTokens = await this.getPractitionerActiveTokens(draft.id);
986
+ for (const token of activeTokens) {
987
+ await this.markTokenAsUsed(token.id, draft.id, userId);
988
+ }
989
+ }
990
+
991
+ return updatedPractitioner;
992
+ }
993
+
994
+ // Use first draft as the main profile, merge others into it
995
+ const mainDraft = validDrafts[0];
996
+ const otherDrafts = validDrafts.slice(1);
997
+
998
+ // Merge clinics from other drafts
999
+ let mergedClinics = new Set(mainDraft.clinics);
1000
+ let mergedWorkingHours = [...mainDraft.clinicWorkingHours];
1001
+ let mergedClinicsInfo = [...mainDraft.clinicsInfo];
1002
+
1003
+ for (const draft of otherDrafts) {
1004
+ draft.clinics.forEach(clinicId => mergedClinics.add(clinicId));
1005
+
1006
+ for (const workingHours of draft.clinicWorkingHours) {
1007
+ if (!mergedWorkingHours.find(wh => wh.clinicId === workingHours.clinicId)) {
1008
+ mergedWorkingHours.push(workingHours);
1009
+ }
1010
+ }
1011
+
1012
+ for (const clinicInfo of draft.clinicsInfo) {
1013
+ if (!mergedClinicsInfo.find(ci => ci.id === clinicInfo.id)) {
1014
+ mergedClinicsInfo.push(clinicInfo);
1015
+ }
1016
+ }
1017
+ }
1018
+
1019
+ // Claim the main profile
1020
+ const updatedPractitioner = await this.updatePractitioner(mainDraft.id, {
1021
+ userRef: userId,
1022
+ status: PractitionerStatus.ACTIVE,
1023
+ clinics: Array.from(mergedClinics),
1024
+ clinicWorkingHours: mergedWorkingHours,
1025
+ clinicsInfo: mergedClinicsInfo,
1026
+ });
1027
+
1028
+ // Mark all active tokens for main profile as used
1029
+ const mainActiveTokens = await this.getPractitionerActiveTokens(mainDraft.id);
1030
+ for (const token of mainActiveTokens) {
1031
+ await this.markTokenAsUsed(token.id, mainDraft.id, userId);
1032
+ }
1033
+
1034
+ // Delete other draft profiles
1035
+ for (const draft of otherDrafts) {
1036
+ await deleteDoc(doc(this.db, PRACTITIONERS_COLLECTION, draft.id));
1037
+
1038
+ const activeTokens = await this.getPractitionerActiveTokens(draft.id);
1039
+ for (const token of activeTokens) {
1040
+ await this.markTokenAsUsed(token.id, draft.id, userId);
1041
+ }
1042
+ }
1043
+
1044
+ console.log("[PRACTITIONER] Multiple draft profiles claimed successfully", {
1045
+ practitionerId: updatedPractitioner.id,
1046
+ userId,
1047
+ mergedCount: validDrafts.length,
1048
+ });
1049
+
1050
+ return updatedPractitioner;
1051
+ } catch (error) {
1052
+ console.error(
1053
+ "[PRACTITIONER] Error claiming multiple draft profiles with Google:",
1054
+ error
1055
+ );
1056
+ throw error;
1057
+ }
1058
+ }
1059
+
717
1060
  /**
718
1061
  * Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
719
1062
  */