@blackcode_sa/metaestetics-api 1.13.20 → 1.14.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.
@@ -1002,6 +1002,179 @@ 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
+ const existingUser = await this.userService.getUserById(firebaseUser.uid);
1065
+ if (!existingUser) {
1066
+ await firebaseSignOut(this.auth);
1067
+ throw new AuthError(
1068
+ 'No account found. Please contact support.',
1069
+ 'AUTH/USER_NOT_FOUND',
1070
+ 404,
1071
+ );
1072
+ }
1073
+
1074
+ // Check if user has practitioner profile
1075
+ let practitioner: Practitioner | null = null;
1076
+ if (existingUser.practitionerProfile) {
1077
+ practitioner = await practitionerService.getPractitioner(existingUser.practitionerProfile);
1078
+ }
1079
+
1080
+ // Check for any new draft profiles
1081
+ const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1082
+
1083
+ return {
1084
+ user: existingUser,
1085
+ practitioner,
1086
+ draftProfiles,
1087
+ };
1088
+ }
1089
+
1090
+ // Case 2: User exists with email/password - need to link Google provider
1091
+ // Firebase doesn't allow linking without being signed in first
1092
+ // So we need to ask user to sign in with email/password first, then link
1093
+ if (hasEmailMethod && !hasGoogleMethod) {
1094
+ console.log('[AUTH] User exists with email/password only');
1095
+ throw new AuthError(
1096
+ 'An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.',
1097
+ 'AUTH/EMAIL_ALREADY_EXISTS',
1098
+ 409,
1099
+ );
1100
+ }
1101
+
1102
+ // Case 3: New user - sign in with Google and check for draft profiles
1103
+ console.log('[AUTH] Signing in with Google credential');
1104
+ const credential = GoogleAuthProvider.credential(idToken);
1105
+
1106
+ let firebaseUser: FirebaseUser;
1107
+ try {
1108
+ const result = await signInWithCredential(this.auth, credential);
1109
+ firebaseUser = result.user;
1110
+ } catch (error: any) {
1111
+ // If sign-in fails because email already exists with different provider
1112
+ if (error.code === 'auth/account-exists-with-different-credential') {
1113
+ throw new AuthError(
1114
+ 'An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.',
1115
+ 'AUTH/EMAIL_ALREADY_EXISTS',
1116
+ 409,
1117
+ );
1118
+ }
1119
+ throw error;
1120
+ }
1121
+
1122
+ // Check for existing User document (in case user had email/password account that was just linked)
1123
+ let existingUser: User | null = null;
1124
+ try {
1125
+ const existingUserDoc = await this.userService.getUserById(firebaseUser.uid);
1126
+ if (existingUserDoc) {
1127
+ existingUser = existingUserDoc;
1128
+ console.log('[AUTH] Found existing User document');
1129
+ }
1130
+ } catch (error) {
1131
+ console.error('[AUTH] Error checking for existing user:', error);
1132
+ // Continue with new user creation
1133
+ }
1134
+
1135
+ // Check for draft profiles
1136
+ console.log('[AUTH] Checking for draft profiles');
1137
+ const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
1138
+
1139
+ let user: User;
1140
+ if (existingUser) {
1141
+ // User exists - use existing account
1142
+ user = existingUser;
1143
+ console.log('[AUTH] Using existing user account');
1144
+ } else {
1145
+ // Create new user document
1146
+ user = await this.userService.createUser(firebaseUser, [UserRole.PRACTITIONER], {
1147
+ skipProfileCreation: true,
1148
+ });
1149
+ console.log('[AUTH] Created new user account');
1150
+ }
1151
+
1152
+ // Check if user already has practitioner profile
1153
+ let practitioner: Practitioner | null = null;
1154
+ if (user.practitionerProfile) {
1155
+ practitioner = await practitionerService.getPractitioner(user.practitionerProfile);
1156
+ }
1157
+
1158
+ console.log('[AUTH] Google Sign-In complete', {
1159
+ userId: user.uid,
1160
+ hasPractitioner: !!practitioner,
1161
+ draftProfilesCount: draftProfiles.length,
1162
+ });
1163
+
1164
+ return {
1165
+ user,
1166
+ practitioner,
1167
+ draftProfiles,
1168
+ };
1169
+ } catch (error) {
1170
+ console.error('[AUTH] Error in signUpPractitionerWithGoogle:', error);
1171
+ if (error instanceof AuthError) {
1172
+ throw error;
1173
+ }
1174
+ throw handleFirebaseError(error);
1175
+ }
1176
+ }
1177
+
1005
1178
  /**
1006
1179
  * Links a Google account to the currently signed-in user using an ID token.
1007
1180
  * This is used to upgrade an anonymous user or to allow an existing user
@@ -714,6 +714,324 @@ export class PractitionerService extends BaseService {
714
714
  }
715
715
  }
716
716
 
717
+ /**
718
+ * Finds all draft practitioner profiles by email address
719
+ * Used when a doctor signs in with Google to show all clinic invitations
720
+ *
721
+ * @param email - Email address to search for
722
+ * @returns Array of draft practitioner profiles with clinic information
723
+ *
724
+ * @remarks
725
+ * Requires Firestore composite index on:
726
+ * - Collection: practitioners
727
+ * - Fields: basicInfo.email (Ascending), status (Ascending), userRef (Ascending)
728
+ */
729
+ async getDraftProfilesByEmail(
730
+ email: string
731
+ ): Promise<Practitioner[]> {
732
+ try {
733
+ const normalizedEmail = email.toLowerCase().trim();
734
+
735
+ console.log("[PRACTITIONER] Searching for all draft practitioners by email", {
736
+ email: normalizedEmail,
737
+ });
738
+
739
+ const q = query(
740
+ collection(this.db, PRACTITIONERS_COLLECTION),
741
+ where("basicInfo.email", "==", normalizedEmail),
742
+ where("status", "==", PractitionerStatus.DRAFT),
743
+ where("userRef", "==", "")
744
+ );
745
+
746
+ const querySnapshot = await getDocs(q);
747
+
748
+ if (querySnapshot.empty) {
749
+ console.log("[PRACTITIONER] No draft practitioners found for email", {
750
+ email: normalizedEmail,
751
+ });
752
+ return [];
753
+ }
754
+
755
+ const draftPractitioners = querySnapshot.docs.map(
756
+ (doc) => doc.data() as Practitioner
757
+ );
758
+
759
+ console.log("[PRACTITIONER] Found draft practitioners", {
760
+ email: normalizedEmail,
761
+ count: draftPractitioners.length,
762
+ practitionerIds: draftPractitioners.map((p) => p.id),
763
+ });
764
+
765
+ return draftPractitioners;
766
+ } catch (error) {
767
+ console.error(
768
+ "[PRACTITIONER] Error finding draft practitioners by email:",
769
+ error
770
+ );
771
+ // If query fails (e.g., index not created), return empty array
772
+ return [];
773
+ }
774
+ }
775
+
776
+ /**
777
+ * Claims a draft practitioner profile and links it to a user account
778
+ * Used when a doctor selects which clinic(s) to join after Google Sign-In
779
+ *
780
+ * @param practitionerId - ID of the draft practitioner profile to claim
781
+ * @param userId - ID of the user account to link the profile to
782
+ * @returns The claimed practitioner profile
783
+ */
784
+ async claimDraftProfileWithGoogle(
785
+ practitionerId: string,
786
+ userId: string
787
+ ): Promise<Practitioner> {
788
+ try {
789
+ console.log("[PRACTITIONER] Claiming draft profile with Google", {
790
+ practitionerId,
791
+ userId,
792
+ });
793
+
794
+ // Get the draft practitioner profile
795
+ const practitioner = await this.getPractitioner(practitionerId);
796
+ if (!practitioner) {
797
+ throw new Error(`Practitioner ${practitionerId} not found`);
798
+ }
799
+
800
+ // Ensure practitioner is in DRAFT status
801
+ if (practitioner.status !== PractitionerStatus.DRAFT) {
802
+ throw new Error("This practitioner profile has already been claimed");
803
+ }
804
+
805
+ // Check if user already has a practitioner profile
806
+ const existingPractitioner = await this.getPractitionerByUserRef(userId);
807
+ if (existingPractitioner) {
808
+ // User already has a profile - merge clinics from draft profile into existing profile
809
+ console.log("[PRACTITIONER] User already has profile, merging clinics");
810
+
811
+ // Merge clinics (avoid duplicates)
812
+ const mergedClinics = Array.from(new Set([
813
+ ...existingPractitioner.clinics,
814
+ ...practitioner.clinics,
815
+ ]));
816
+
817
+ // Merge clinic working hours
818
+ const mergedWorkingHours = [...existingPractitioner.clinicWorkingHours];
819
+ for (const workingHours of practitioner.clinicWorkingHours) {
820
+ if (!mergedWorkingHours.find(wh => wh.clinicId === workingHours.clinicId)) {
821
+ mergedWorkingHours.push(workingHours);
822
+ }
823
+ }
824
+
825
+ // Merge clinics info (avoid duplicates)
826
+ const mergedClinicsInfo = [...existingPractitioner.clinicsInfo];
827
+ for (const clinicInfo of practitioner.clinicsInfo) {
828
+ if (!mergedClinicsInfo.find(ci => ci.id === clinicInfo.id)) {
829
+ mergedClinicsInfo.push(clinicInfo);
830
+ }
831
+ }
832
+
833
+ // Update existing practitioner with merged data
834
+ const updatedPractitioner = await this.updatePractitioner(existingPractitioner.id, {
835
+ clinics: mergedClinics,
836
+ clinicWorkingHours: mergedWorkingHours,
837
+ clinicsInfo: mergedClinicsInfo,
838
+ });
839
+
840
+ // Delete the draft profile since we've merged it
841
+ await deleteDoc(doc(this.db, PRACTITIONERS_COLLECTION, practitionerId));
842
+
843
+ // Mark all active tokens for the draft practitioner as used
844
+ const activeTokens = await this.getPractitionerActiveTokens(practitionerId);
845
+ for (const token of activeTokens) {
846
+ await this.markTokenAsUsed(token.id, practitionerId, userId);
847
+ }
848
+
849
+ return updatedPractitioner;
850
+ }
851
+
852
+ // Claim the profile by linking it to the user
853
+ const updatedPractitioner = await this.updatePractitioner(practitioner.id, {
854
+ userRef: userId,
855
+ status: PractitionerStatus.ACTIVE,
856
+ });
857
+
858
+ // Mark all active tokens for this practitioner as used
859
+ const activeTokens = await this.getPractitionerActiveTokens(practitionerId);
860
+ for (const token of activeTokens) {
861
+ await this.markTokenAsUsed(token.id, practitionerId, userId);
862
+ }
863
+
864
+ console.log("[PRACTITIONER] Draft profile claimed successfully", {
865
+ practitionerId: updatedPractitioner.id,
866
+ userId,
867
+ });
868
+
869
+ return updatedPractitioner;
870
+ } catch (error) {
871
+ console.error(
872
+ "[PRACTITIONER] Error claiming draft profile with Google:",
873
+ error
874
+ );
875
+ throw error;
876
+ }
877
+ }
878
+
879
+ /**
880
+ * Claims multiple draft practitioner profiles and merges them into one profile
881
+ * Used when a doctor selects multiple clinics to join after Google Sign-In
882
+ *
883
+ * @param practitionerIds - Array of draft practitioner profile IDs to claim
884
+ * @param userId - ID of the user account to link the profiles to
885
+ * @returns The claimed practitioner profile (first one becomes main, others merged)
886
+ */
887
+ async claimMultipleDraftProfilesWithGoogle(
888
+ practitionerIds: string[],
889
+ userId: string
890
+ ): Promise<Practitioner> {
891
+ try {
892
+ if (practitionerIds.length === 0) {
893
+ throw new Error("No practitioner IDs provided");
894
+ }
895
+
896
+ console.log("[PRACTITIONER] Claiming multiple draft profiles with Google", {
897
+ practitionerIds,
898
+ userId,
899
+ count: practitionerIds.length,
900
+ });
901
+
902
+ // Get all draft profiles
903
+ const draftProfiles = await Promise.all(
904
+ practitionerIds.map(id => this.getPractitioner(id))
905
+ );
906
+
907
+ // Filter out nulls and ensure all are drafts
908
+ const validDrafts = draftProfiles.filter((p): p is Practitioner => {
909
+ if (!p) return false;
910
+ if (p.status !== PractitionerStatus.DRAFT) {
911
+ throw new Error(`Practitioner ${p.id} has already been claimed`);
912
+ }
913
+ return true;
914
+ });
915
+
916
+ if (validDrafts.length === 0) {
917
+ throw new Error("No valid draft profiles found");
918
+ }
919
+
920
+ // Check if user already has a practitioner profile
921
+ const existingPractitioner = await this.getPractitionerByUserRef(userId);
922
+
923
+ if (existingPractitioner) {
924
+ // Merge all draft profiles into existing profile
925
+ let mergedClinics = new Set(existingPractitioner.clinics);
926
+ let mergedWorkingHours = [...existingPractitioner.clinicWorkingHours];
927
+ let mergedClinicsInfo = [...existingPractitioner.clinicsInfo];
928
+
929
+ for (const draft of validDrafts) {
930
+ // Merge clinics
931
+ draft.clinics.forEach(clinicId => mergedClinics.add(clinicId));
932
+
933
+ // Merge working hours
934
+ for (const workingHours of draft.clinicWorkingHours) {
935
+ if (!mergedWorkingHours.find(wh => wh.clinicId === workingHours.clinicId)) {
936
+ mergedWorkingHours.push(workingHours);
937
+ }
938
+ }
939
+
940
+ // Merge clinics info
941
+ for (const clinicInfo of draft.clinicsInfo) {
942
+ if (!mergedClinicsInfo.find(ci => ci.id === clinicInfo.id)) {
943
+ mergedClinicsInfo.push(clinicInfo);
944
+ }
945
+ }
946
+ }
947
+
948
+ // Update existing practitioner
949
+ const updatedPractitioner = await this.updatePractitioner(existingPractitioner.id, {
950
+ clinics: Array.from(mergedClinics),
951
+ clinicWorkingHours: mergedWorkingHours,
952
+ clinicsInfo: mergedClinicsInfo,
953
+ });
954
+
955
+ // Delete all draft profiles
956
+ for (const draft of validDrafts) {
957
+ await deleteDoc(doc(this.db, PRACTITIONERS_COLLECTION, draft.id));
958
+
959
+ // Mark all active tokens as used
960
+ const activeTokens = await this.getPractitionerActiveTokens(draft.id);
961
+ for (const token of activeTokens) {
962
+ await this.markTokenAsUsed(token.id, draft.id, userId);
963
+ }
964
+ }
965
+
966
+ return updatedPractitioner;
967
+ }
968
+
969
+ // Use first draft as the main profile, merge others into it
970
+ const mainDraft = validDrafts[0];
971
+ const otherDrafts = validDrafts.slice(1);
972
+
973
+ // Merge clinics from other drafts
974
+ let mergedClinics = new Set(mainDraft.clinics);
975
+ let mergedWorkingHours = [...mainDraft.clinicWorkingHours];
976
+ let mergedClinicsInfo = [...mainDraft.clinicsInfo];
977
+
978
+ for (const draft of otherDrafts) {
979
+ draft.clinics.forEach(clinicId => mergedClinics.add(clinicId));
980
+
981
+ for (const workingHours of draft.clinicWorkingHours) {
982
+ if (!mergedWorkingHours.find(wh => wh.clinicId === workingHours.clinicId)) {
983
+ mergedWorkingHours.push(workingHours);
984
+ }
985
+ }
986
+
987
+ for (const clinicInfo of draft.clinicsInfo) {
988
+ if (!mergedClinicsInfo.find(ci => ci.id === clinicInfo.id)) {
989
+ mergedClinicsInfo.push(clinicInfo);
990
+ }
991
+ }
992
+ }
993
+
994
+ // Claim the main profile
995
+ const updatedPractitioner = await this.updatePractitioner(mainDraft.id, {
996
+ userRef: userId,
997
+ status: PractitionerStatus.ACTIVE,
998
+ clinics: Array.from(mergedClinics),
999
+ clinicWorkingHours: mergedWorkingHours,
1000
+ clinicsInfo: mergedClinicsInfo,
1001
+ });
1002
+
1003
+ // Mark all active tokens for main profile as used
1004
+ const mainActiveTokens = await this.getPractitionerActiveTokens(mainDraft.id);
1005
+ for (const token of mainActiveTokens) {
1006
+ await this.markTokenAsUsed(token.id, mainDraft.id, userId);
1007
+ }
1008
+
1009
+ // Delete other draft profiles
1010
+ for (const draft of otherDrafts) {
1011
+ await deleteDoc(doc(this.db, PRACTITIONERS_COLLECTION, draft.id));
1012
+
1013
+ const activeTokens = await this.getPractitionerActiveTokens(draft.id);
1014
+ for (const token of activeTokens) {
1015
+ await this.markTokenAsUsed(token.id, draft.id, userId);
1016
+ }
1017
+ }
1018
+
1019
+ console.log("[PRACTITIONER] Multiple draft profiles claimed successfully", {
1020
+ practitionerId: updatedPractitioner.id,
1021
+ userId,
1022
+ mergedCount: validDrafts.length,
1023
+ });
1024
+
1025
+ return updatedPractitioner;
1026
+ } catch (error) {
1027
+ console.error(
1028
+ "[PRACTITIONER] Error claiming multiple draft profiles with Google:",
1029
+ error
1030
+ );
1031
+ throw error;
1032
+ }
1033
+ }
1034
+
717
1035
  /**
718
1036
  * Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
719
1037
  */
@@ -1546,6 +1864,10 @@ export class PractitionerService extends BaseService {
1546
1864
  clinicId: string
1547
1865
  ): Promise<void> {
1548
1866
  try {
1867
+ console.log(
1868
+ `[EnableFreeConsultation] Starting for practitioner ${practitionerId} in clinic ${clinicId}`
1869
+ );
1870
+
1549
1871
  // First, ensure the free consultation infrastructure exists
1550
1872
  await this.ensureFreeConsultationInfrastructure();
1551
1873
 
@@ -1573,9 +1895,15 @@ export class PractitionerService extends BaseService {
1573
1895
  );
1574
1896
  }
1575
1897
 
1576
- // Get all procedures for this practitioner (including inactive ones)
1898
+ // CRITICAL: Double-check for existing procedures to prevent race conditions
1899
+ // Fetch procedures again right before creation/update
1900
+ // IMPORTANT: Pass false for excludeDraftPractitioners to work with draft practitioners
1577
1901
  const [activeProcedures, inactiveProcedures] = await Promise.all([
1578
- this.getProcedureService().getProceduresByPractitioner(practitionerId),
1902
+ this.getProcedureService().getProceduresByPractitioner(
1903
+ practitionerId,
1904
+ undefined, // clinicBranchId
1905
+ false // excludeDraftPractitioners - allow draft practitioners
1906
+ ),
1579
1907
  this.getProcedureService().getInactiveProceduresByPractitioner(
1580
1908
  practitionerId
1581
1909
  ),
@@ -1585,31 +1913,86 @@ export class PractitionerService extends BaseService {
1585
1913
  const allProcedures = [...activeProcedures, ...inactiveProcedures];
1586
1914
 
1587
1915
  // Check if free consultation already exists (active or inactive)
1588
- const existingConsultation = allProcedures.find(
1916
+ const existingConsultations = allProcedures.filter(
1589
1917
  (procedure) =>
1590
1918
  procedure.technology.id === "free-consultation-tech" &&
1591
1919
  procedure.clinicBranchId === clinicId
1592
1920
  );
1593
1921
 
1922
+ console.log(
1923
+ `[EnableFreeConsultation] Found ${existingConsultations.length} existing free consultation(s)`
1924
+ );
1925
+
1926
+ // If multiple consultations exist, log a warning and clean up duplicates
1927
+ if (existingConsultations.length > 1) {
1928
+ console.warn(
1929
+ `[EnableFreeConsultation] WARNING: Found ${existingConsultations.length} duplicate free consultations for practitioner ${practitionerId} in clinic ${clinicId}`
1930
+ );
1931
+ // Keep the first one, deactivate the rest
1932
+ for (let i = 1; i < existingConsultations.length; i++) {
1933
+ console.log(
1934
+ `[EnableFreeConsultation] Deactivating duplicate consultation ${existingConsultations[i].id}`
1935
+ );
1936
+ await this.getProcedureService().deactivateProcedure(
1937
+ existingConsultations[i].id
1938
+ );
1939
+ }
1940
+ }
1941
+
1942
+ const existingConsultation = existingConsultations[0];
1943
+
1594
1944
  if (existingConsultation) {
1595
1945
  if (existingConsultation.isActive) {
1596
1946
  console.log(
1597
- `Free consultation already active for practitioner ${practitionerId} in clinic ${clinicId}`
1947
+ `[EnableFreeConsultation] Free consultation already active for practitioner ${practitionerId} in clinic ${clinicId}`
1598
1948
  );
1599
1949
  return;
1600
1950
  } else {
1601
1951
  // Reactivate the existing disabled consultation
1952
+ console.log(
1953
+ `[EnableFreeConsultation] Reactivating existing consultation ${existingConsultation.id}`
1954
+ );
1602
1955
  await this.getProcedureService().updateProcedure(
1603
1956
  existingConsultation.id,
1604
1957
  { isActive: true }
1605
1958
  );
1606
1959
  console.log(
1607
- `Reactivated existing free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
1960
+ `[EnableFreeConsultation] Reactivated existing free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
1608
1961
  );
1609
1962
  return;
1610
1963
  }
1611
1964
  }
1612
1965
 
1966
+ // Final check before creating - race condition guard
1967
+ // Fetch one more time to ensure no procedure was created in parallel
1968
+ console.log(
1969
+ `[EnableFreeConsultation] Final race condition check before creating new procedure`
1970
+ );
1971
+ const finalCheckProcedures =
1972
+ await this.getProcedureService().getProceduresByPractitioner(
1973
+ practitionerId,
1974
+ undefined, // clinicBranchId
1975
+ false // excludeDraftPractitioners - allow draft practitioners
1976
+ );
1977
+ const raceConditionCheck = finalCheckProcedures.find(
1978
+ (procedure) =>
1979
+ procedure.technology.id === "free-consultation-tech" &&
1980
+ procedure.clinicBranchId === clinicId
1981
+ );
1982
+
1983
+ if (raceConditionCheck) {
1984
+ console.log(
1985
+ `[EnableFreeConsultation] Race condition detected! Procedure was created by another request. Using existing procedure ${raceConditionCheck.id}`
1986
+ );
1987
+ if (!raceConditionCheck.isActive) {
1988
+ await this.getProcedureService().updateProcedure(
1989
+ raceConditionCheck.id,
1990
+ { isActive: true }
1991
+ );
1992
+ }
1993
+ return;
1994
+ }
1995
+
1613
1996
  // Create procedure data for free consultation (without productId or productsMetadata)
1614
1997
  const consultationData: Omit<CreateProcedureData, "productId"> = {
1615
1998
  name: "Free Consultation",
@@ -1631,16 +2014,19 @@ export class PractitionerService extends BaseService {
1631
2014
  };
1632
2015
 
1633
2016
  // Create the consultation procedure using the special method
2017
+ console.log(
2018
+ `[EnableFreeConsultation] Creating new free consultation procedure`
2019
+ );
1634
2020
  await this.getProcedureService().createConsultationProcedure(
1635
2021
  consultationData
1636
2022
  );
1637
2023
 
1638
2024
  console.log(
1639
- `Free consultation enabled for practitioner ${practitionerId} in clinic ${clinicId}`
2025
+ `[EnableFreeConsultation] Successfully created free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
1640
2026
  );
1641
2027
  } catch (error) {
1642
2028
  console.error(
1643
- `Error enabling free consultation for practitioner ${practitionerId} in clinic ${clinicId}:`,
2029
+ `[EnableFreeConsultation] Error enabling free consultation for practitioner ${practitionerId} in clinic ${clinicId}:`,
1644
2030
  error
1645
2031
  );
1646
2032
  throw error;
@@ -1764,10 +2150,18 @@ export class PractitionerService extends BaseService {
1764
2150
 
1765
2151
  // Find the free consultation procedure for this practitioner in this clinic
1766
2152
  // Use the more specific search by technology ID instead of name
2153
+ // IMPORTANT: Pass false for excludeDraftPractitioners to allow disabling for draft practitioners
1767
2154
  const existingProcedures =
1768
2155
  await this.getProcedureService().getProceduresByPractitioner(
1769
- practitionerId
2156
+ practitionerId,
2157
+ undefined, // clinicBranchId (optional)
2158
+ false // excludeDraftPractitioners - must be false to find procedures for draft practitioners
1770
2159
  );
2160
+
2161
+ console.log(
2162
+ `[DisableFreeConsultation] Found ${existingProcedures.length} procedures for practitioner ${practitionerId}`
2163
+ );
2164
+
1771
2165
  const freeConsultation = existingProcedures.find(
1772
2166
  (procedure) =>
1773
2167
  procedure.technology.id === "free-consultation-tech" &&
@@ -1777,10 +2171,24 @@ export class PractitionerService extends BaseService {
1777
2171
 
1778
2172
  if (!freeConsultation) {
1779
2173
  console.log(
1780
- `No active free consultation found for practitioner ${practitionerId} in clinic ${clinicId}`
2174
+ `[DisableFreeConsultation] No active free consultation found for practitioner ${practitionerId} in clinic ${clinicId}`
2175
+ );
2176
+ console.log(
2177
+ `[DisableFreeConsultation] Existing procedures:`,
2178
+ existingProcedures.map(p => ({
2179
+ id: p.id,
2180
+ name: p.name,
2181
+ technologyId: p.technology?.id,
2182
+ clinicBranchId: p.clinicBranchId,
2183
+ isActive: p.isActive
2184
+ }))
1781
2185
  );
1782
2186
  return;
1783
2187
  }
2188
+
2189
+ console.log(
2190
+ `[DisableFreeConsultation] Found free consultation procedure ${freeConsultation.id}, deactivating...`
2191
+ );
1784
2192
 
1785
2193
  // Deactivate the consultation procedure
1786
2194
  await this.getProcedureService().deactivateProcedure(freeConsultation.id);