@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.
package/dist/index.mjs CHANGED
@@ -7778,7 +7778,7 @@ var contraindicationSchema = z6.object({
7778
7778
  notes: z6.string().optional().nullable(),
7779
7779
  isActive: z6.boolean()
7780
7780
  });
7781
- var medicationSchema = z6.object({
7781
+ var baseMedicationSchema = z6.object({
7782
7782
  name: z6.string().min(1),
7783
7783
  dosage: z6.string().min(1),
7784
7784
  frequency: z6.string().min(1),
@@ -7786,6 +7786,24 @@ var medicationSchema = z6.object({
7786
7786
  endDate: timestampSchema.optional().nullable(),
7787
7787
  prescribedBy: z6.string().optional().nullable()
7788
7788
  });
7789
+ var medicationSchema = baseMedicationSchema.refine(
7790
+ (data) => {
7791
+ if (!data.endDate) {
7792
+ return true;
7793
+ }
7794
+ if (!data.startDate) {
7795
+ return false;
7796
+ }
7797
+ const startDate = data.startDate.toDate();
7798
+ const endDate = data.endDate.toDate();
7799
+ return endDate >= startDate;
7800
+ },
7801
+ {
7802
+ message: "End date requires a start date and must be equal to or after start date",
7803
+ path: ["endDate"]
7804
+ // This will attach the error to the endDate field
7805
+ }
7806
+ );
7789
7807
  var patientMedicalInfoSchema = z6.object({
7790
7808
  patientId: z6.string(),
7791
7809
  vitalStats: vitalStatsSchema,
@@ -7821,9 +7839,26 @@ var updateContraindicationSchema = contraindicationSchema.partial().extend({
7821
7839
  contraindicationIndex: z6.number().min(0)
7822
7840
  });
7823
7841
  var addMedicationSchema = medicationSchema;
7824
- var updateMedicationSchema = medicationSchema.partial().extend({
7842
+ var updateMedicationSchema = baseMedicationSchema.partial().extend({
7825
7843
  medicationIndex: z6.number().min(0)
7826
- });
7844
+ }).refine(
7845
+ (data) => {
7846
+ if (!data.endDate) {
7847
+ return true;
7848
+ }
7849
+ if (!data.startDate) {
7850
+ return false;
7851
+ }
7852
+ const startDate = data.startDate.toDate();
7853
+ const endDate = data.endDate.toDate();
7854
+ return endDate >= startDate;
7855
+ },
7856
+ {
7857
+ message: "End date requires a start date and must be equal to or after start date",
7858
+ path: ["endDate"]
7859
+ // This will attach the error to the endDate field
7860
+ }
7861
+ );
7827
7862
 
7828
7863
  // src/validations/patient.schema.ts
7829
7864
  var locationDataSchema = z7.object({
@@ -11703,6 +11738,240 @@ var PractitionerService = class extends BaseService {
11703
11738
  return null;
11704
11739
  }
11705
11740
  }
11741
+ /**
11742
+ * Finds all draft practitioner profiles by email address
11743
+ * Used when a doctor signs in with Google to show all clinic invitations
11744
+ *
11745
+ * @param email - Email address to search for
11746
+ * @returns Array of draft practitioner profiles with clinic information
11747
+ *
11748
+ * @remarks
11749
+ * Requires Firestore composite index on:
11750
+ * - Collection: practitioners
11751
+ * - Fields: basicInfo.email (Ascending), status (Ascending), userRef (Ascending)
11752
+ */
11753
+ async getDraftProfilesByEmail(email) {
11754
+ try {
11755
+ const normalizedEmail = email.toLowerCase().trim();
11756
+ console.log("[PRACTITIONER] Searching for all draft practitioners by email", {
11757
+ email: normalizedEmail
11758
+ });
11759
+ const q = query13(
11760
+ collection13(this.db, PRACTITIONERS_COLLECTION),
11761
+ where13("basicInfo.email", "==", normalizedEmail),
11762
+ where13("status", "==", "draft" /* DRAFT */),
11763
+ where13("userRef", "==", "")
11764
+ );
11765
+ const querySnapshot = await getDocs13(q);
11766
+ if (querySnapshot.empty) {
11767
+ console.log("[PRACTITIONER] No draft practitioners found for email", {
11768
+ email: normalizedEmail
11769
+ });
11770
+ return [];
11771
+ }
11772
+ const draftPractitioners = querySnapshot.docs.map(
11773
+ (doc47) => doc47.data()
11774
+ );
11775
+ console.log("[PRACTITIONER] Found draft practitioners", {
11776
+ email: normalizedEmail,
11777
+ count: draftPractitioners.length,
11778
+ practitionerIds: draftPractitioners.map((p) => p.id)
11779
+ });
11780
+ return draftPractitioners;
11781
+ } catch (error) {
11782
+ console.error(
11783
+ "[PRACTITIONER] Error finding draft practitioners by email:",
11784
+ error
11785
+ );
11786
+ return [];
11787
+ }
11788
+ }
11789
+ /**
11790
+ * Claims a draft practitioner profile and links it to a user account
11791
+ * Used when a doctor selects which clinic(s) to join after Google Sign-In
11792
+ *
11793
+ * @param practitionerId - ID of the draft practitioner profile to claim
11794
+ * @param userId - ID of the user account to link the profile to
11795
+ * @returns The claimed practitioner profile
11796
+ */
11797
+ async claimDraftProfileWithGoogle(practitionerId, userId) {
11798
+ try {
11799
+ console.log("[PRACTITIONER] Claiming draft profile with Google", {
11800
+ practitionerId,
11801
+ userId
11802
+ });
11803
+ const practitioner = await this.getPractitioner(practitionerId);
11804
+ if (!practitioner) {
11805
+ throw new Error(`Practitioner ${practitionerId} not found`);
11806
+ }
11807
+ if (practitioner.status !== "draft" /* DRAFT */) {
11808
+ throw new Error("This practitioner profile has already been claimed");
11809
+ }
11810
+ const existingPractitioner = await this.getPractitionerByUserRef(userId);
11811
+ if (existingPractitioner) {
11812
+ console.log("[PRACTITIONER] User already has profile, merging clinics");
11813
+ const mergedClinics = Array.from(/* @__PURE__ */ new Set([
11814
+ ...existingPractitioner.clinics,
11815
+ ...practitioner.clinics
11816
+ ]));
11817
+ const mergedWorkingHours = [...existingPractitioner.clinicWorkingHours];
11818
+ for (const workingHours of practitioner.clinicWorkingHours) {
11819
+ if (!mergedWorkingHours.find((wh) => wh.clinicId === workingHours.clinicId)) {
11820
+ mergedWorkingHours.push(workingHours);
11821
+ }
11822
+ }
11823
+ const mergedClinicsInfo = [...existingPractitioner.clinicsInfo];
11824
+ for (const clinicInfo of practitioner.clinicsInfo) {
11825
+ if (!mergedClinicsInfo.find((ci) => ci.id === clinicInfo.id)) {
11826
+ mergedClinicsInfo.push(clinicInfo);
11827
+ }
11828
+ }
11829
+ const updatedPractitioner2 = await this.updatePractitioner(existingPractitioner.id, {
11830
+ clinics: mergedClinics,
11831
+ clinicWorkingHours: mergedWorkingHours,
11832
+ clinicsInfo: mergedClinicsInfo
11833
+ });
11834
+ await deleteDoc4(doc20(this.db, PRACTITIONERS_COLLECTION, practitionerId));
11835
+ const activeTokens2 = await this.getPractitionerActiveTokens(practitionerId);
11836
+ for (const token of activeTokens2) {
11837
+ await this.markTokenAsUsed(token.id, practitionerId, userId);
11838
+ }
11839
+ return updatedPractitioner2;
11840
+ }
11841
+ const updatedPractitioner = await this.updatePractitioner(practitioner.id, {
11842
+ userRef: userId,
11843
+ status: "active" /* ACTIVE */
11844
+ });
11845
+ const activeTokens = await this.getPractitionerActiveTokens(practitionerId);
11846
+ for (const token of activeTokens) {
11847
+ await this.markTokenAsUsed(token.id, practitionerId, userId);
11848
+ }
11849
+ console.log("[PRACTITIONER] Draft profile claimed successfully", {
11850
+ practitionerId: updatedPractitioner.id,
11851
+ userId
11852
+ });
11853
+ return updatedPractitioner;
11854
+ } catch (error) {
11855
+ console.error(
11856
+ "[PRACTITIONER] Error claiming draft profile with Google:",
11857
+ error
11858
+ );
11859
+ throw error;
11860
+ }
11861
+ }
11862
+ /**
11863
+ * Claims multiple draft practitioner profiles and merges them into one profile
11864
+ * Used when a doctor selects multiple clinics to join after Google Sign-In
11865
+ *
11866
+ * @param practitionerIds - Array of draft practitioner profile IDs to claim
11867
+ * @param userId - ID of the user account to link the profiles to
11868
+ * @returns The claimed practitioner profile (first one becomes main, others merged)
11869
+ */
11870
+ async claimMultipleDraftProfilesWithGoogle(practitionerIds, userId) {
11871
+ try {
11872
+ if (practitionerIds.length === 0) {
11873
+ throw new Error("No practitioner IDs provided");
11874
+ }
11875
+ console.log("[PRACTITIONER] Claiming multiple draft profiles with Google", {
11876
+ practitionerIds,
11877
+ userId,
11878
+ count: practitionerIds.length
11879
+ });
11880
+ const draftProfiles = await Promise.all(
11881
+ practitionerIds.map((id) => this.getPractitioner(id))
11882
+ );
11883
+ const validDrafts = draftProfiles.filter((p) => {
11884
+ if (!p) return false;
11885
+ if (p.status !== "draft" /* DRAFT */) {
11886
+ throw new Error(`Practitioner ${p.id} has already been claimed`);
11887
+ }
11888
+ return true;
11889
+ });
11890
+ if (validDrafts.length === 0) {
11891
+ throw new Error("No valid draft profiles found");
11892
+ }
11893
+ const existingPractitioner = await this.getPractitionerByUserRef(userId);
11894
+ if (existingPractitioner) {
11895
+ let mergedClinics2 = new Set(existingPractitioner.clinics);
11896
+ let mergedWorkingHours2 = [...existingPractitioner.clinicWorkingHours];
11897
+ let mergedClinicsInfo2 = [...existingPractitioner.clinicsInfo];
11898
+ for (const draft of validDrafts) {
11899
+ draft.clinics.forEach((clinicId) => mergedClinics2.add(clinicId));
11900
+ for (const workingHours of draft.clinicWorkingHours) {
11901
+ if (!mergedWorkingHours2.find((wh) => wh.clinicId === workingHours.clinicId)) {
11902
+ mergedWorkingHours2.push(workingHours);
11903
+ }
11904
+ }
11905
+ for (const clinicInfo of draft.clinicsInfo) {
11906
+ if (!mergedClinicsInfo2.find((ci) => ci.id === clinicInfo.id)) {
11907
+ mergedClinicsInfo2.push(clinicInfo);
11908
+ }
11909
+ }
11910
+ }
11911
+ const updatedPractitioner2 = await this.updatePractitioner(existingPractitioner.id, {
11912
+ clinics: Array.from(mergedClinics2),
11913
+ clinicWorkingHours: mergedWorkingHours2,
11914
+ clinicsInfo: mergedClinicsInfo2
11915
+ });
11916
+ for (const draft of validDrafts) {
11917
+ await deleteDoc4(doc20(this.db, PRACTITIONERS_COLLECTION, draft.id));
11918
+ const activeTokens = await this.getPractitionerActiveTokens(draft.id);
11919
+ for (const token of activeTokens) {
11920
+ await this.markTokenAsUsed(token.id, draft.id, userId);
11921
+ }
11922
+ }
11923
+ return updatedPractitioner2;
11924
+ }
11925
+ const mainDraft = validDrafts[0];
11926
+ const otherDrafts = validDrafts.slice(1);
11927
+ let mergedClinics = new Set(mainDraft.clinics);
11928
+ let mergedWorkingHours = [...mainDraft.clinicWorkingHours];
11929
+ let mergedClinicsInfo = [...mainDraft.clinicsInfo];
11930
+ for (const draft of otherDrafts) {
11931
+ draft.clinics.forEach((clinicId) => mergedClinics.add(clinicId));
11932
+ for (const workingHours of draft.clinicWorkingHours) {
11933
+ if (!mergedWorkingHours.find((wh) => wh.clinicId === workingHours.clinicId)) {
11934
+ mergedWorkingHours.push(workingHours);
11935
+ }
11936
+ }
11937
+ for (const clinicInfo of draft.clinicsInfo) {
11938
+ if (!mergedClinicsInfo.find((ci) => ci.id === clinicInfo.id)) {
11939
+ mergedClinicsInfo.push(clinicInfo);
11940
+ }
11941
+ }
11942
+ }
11943
+ const updatedPractitioner = await this.updatePractitioner(mainDraft.id, {
11944
+ userRef: userId,
11945
+ status: "active" /* ACTIVE */,
11946
+ clinics: Array.from(mergedClinics),
11947
+ clinicWorkingHours: mergedWorkingHours,
11948
+ clinicsInfo: mergedClinicsInfo
11949
+ });
11950
+ const mainActiveTokens = await this.getPractitionerActiveTokens(mainDraft.id);
11951
+ for (const token of mainActiveTokens) {
11952
+ await this.markTokenAsUsed(token.id, mainDraft.id, userId);
11953
+ }
11954
+ for (const draft of otherDrafts) {
11955
+ await deleteDoc4(doc20(this.db, PRACTITIONERS_COLLECTION, draft.id));
11956
+ const activeTokens = await this.getPractitionerActiveTokens(draft.id);
11957
+ for (const token of activeTokens) {
11958
+ await this.markTokenAsUsed(token.id, draft.id, userId);
11959
+ }
11960
+ }
11961
+ console.log("[PRACTITIONER] Multiple draft profiles claimed successfully", {
11962
+ practitionerId: updatedPractitioner.id,
11963
+ userId,
11964
+ mergedCount: validDrafts.length
11965
+ });
11966
+ return updatedPractitioner;
11967
+ } catch (error) {
11968
+ console.error(
11969
+ "[PRACTITIONER] Error claiming multiple draft profiles with Google:",
11970
+ error
11971
+ );
11972
+ throw error;
11973
+ }
11974
+ }
11706
11975
  /**
11707
11976
  * Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
11708
11977
  */
@@ -12338,6 +12607,9 @@ var PractitionerService = class extends BaseService {
12338
12607
  */
12339
12608
  async EnableFreeConsultation(practitionerId, clinicId) {
12340
12609
  try {
12610
+ console.log(
12611
+ `[EnableFreeConsultation] Starting for practitioner ${practitionerId} in clinic ${clinicId}`
12612
+ );
12341
12613
  await this.ensureFreeConsultationInfrastructure();
12342
12614
  const practitioner = await this.getPractitioner(practitionerId);
12343
12615
  if (!practitioner) {
@@ -12353,32 +12625,83 @@ var PractitionerService = class extends BaseService {
12353
12625
  );
12354
12626
  }
12355
12627
  const [activeProcedures, inactiveProcedures] = await Promise.all([
12356
- this.getProcedureService().getProceduresByPractitioner(practitionerId),
12628
+ this.getProcedureService().getProceduresByPractitioner(
12629
+ practitionerId,
12630
+ void 0,
12631
+ // clinicBranchId
12632
+ false
12633
+ // excludeDraftPractitioners - allow draft practitioners
12634
+ ),
12357
12635
  this.getProcedureService().getInactiveProceduresByPractitioner(
12358
12636
  practitionerId
12359
12637
  )
12360
12638
  ]);
12361
12639
  const allProcedures = [...activeProcedures, ...inactiveProcedures];
12362
- const existingConsultation = allProcedures.find(
12640
+ const existingConsultations = allProcedures.filter(
12363
12641
  (procedure) => procedure.technology.id === "free-consultation-tech" && procedure.clinicBranchId === clinicId
12364
12642
  );
12643
+ console.log(
12644
+ `[EnableFreeConsultation] Found ${existingConsultations.length} existing free consultation(s)`
12645
+ );
12646
+ if (existingConsultations.length > 1) {
12647
+ console.warn(
12648
+ `[EnableFreeConsultation] WARNING: Found ${existingConsultations.length} duplicate free consultations for practitioner ${practitionerId} in clinic ${clinicId}`
12649
+ );
12650
+ for (let i = 1; i < existingConsultations.length; i++) {
12651
+ console.log(
12652
+ `[EnableFreeConsultation] Deactivating duplicate consultation ${existingConsultations[i].id}`
12653
+ );
12654
+ await this.getProcedureService().deactivateProcedure(
12655
+ existingConsultations[i].id
12656
+ );
12657
+ }
12658
+ }
12659
+ const existingConsultation = existingConsultations[0];
12365
12660
  if (existingConsultation) {
12366
12661
  if (existingConsultation.isActive) {
12367
12662
  console.log(
12368
- `Free consultation already active for practitioner ${practitionerId} in clinic ${clinicId}`
12663
+ `[EnableFreeConsultation] Free consultation already active for practitioner ${practitionerId} in clinic ${clinicId}`
12369
12664
  );
12370
12665
  return;
12371
12666
  } else {
12667
+ console.log(
12668
+ `[EnableFreeConsultation] Reactivating existing consultation ${existingConsultation.id}`
12669
+ );
12372
12670
  await this.getProcedureService().updateProcedure(
12373
12671
  existingConsultation.id,
12374
12672
  { isActive: true }
12375
12673
  );
12376
12674
  console.log(
12377
- `Reactivated existing free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
12675
+ `[EnableFreeConsultation] Reactivated existing free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
12378
12676
  );
12379
12677
  return;
12380
12678
  }
12381
12679
  }
12680
+ console.log(
12681
+ `[EnableFreeConsultation] Final race condition check before creating new procedure`
12682
+ );
12683
+ const finalCheckProcedures = await this.getProcedureService().getProceduresByPractitioner(
12684
+ practitionerId,
12685
+ void 0,
12686
+ // clinicBranchId
12687
+ false
12688
+ // excludeDraftPractitioners - allow draft practitioners
12689
+ );
12690
+ const raceConditionCheck = finalCheckProcedures.find(
12691
+ (procedure) => procedure.technology.id === "free-consultation-tech" && procedure.clinicBranchId === clinicId
12692
+ );
12693
+ if (raceConditionCheck) {
12694
+ console.log(
12695
+ `[EnableFreeConsultation] Race condition detected! Procedure was created by another request. Using existing procedure ${raceConditionCheck.id}`
12696
+ );
12697
+ if (!raceConditionCheck.isActive) {
12698
+ await this.getProcedureService().updateProcedure(
12699
+ raceConditionCheck.id,
12700
+ { isActive: true }
12701
+ );
12702
+ }
12703
+ return;
12704
+ }
12382
12705
  const consultationData = {
12383
12706
  name: "Free Consultation",
12384
12707
  nameLower: "free consultation",
@@ -12398,15 +12721,18 @@ var PractitionerService = class extends BaseService {
12398
12721
  photos: []
12399
12722
  // No photos for consultation
12400
12723
  };
12724
+ console.log(
12725
+ `[EnableFreeConsultation] Creating new free consultation procedure`
12726
+ );
12401
12727
  await this.getProcedureService().createConsultationProcedure(
12402
12728
  consultationData
12403
12729
  );
12404
12730
  console.log(
12405
- `Free consultation enabled for practitioner ${practitionerId} in clinic ${clinicId}`
12731
+ `[EnableFreeConsultation] Successfully created free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
12406
12732
  );
12407
12733
  } catch (error) {
12408
12734
  console.error(
12409
- `Error enabling free consultation for practitioner ${practitionerId} in clinic ${clinicId}:`,
12735
+ `[EnableFreeConsultation] Error enabling free consultation for practitioner ${practitionerId} in clinic ${clinicId}:`,
12410
12736
  error
12411
12737
  );
12412
12738
  throw error;
@@ -12502,17 +12828,40 @@ var PractitionerService = class extends BaseService {
12502
12828
  );
12503
12829
  }
12504
12830
  const existingProcedures = await this.getProcedureService().getProceduresByPractitioner(
12505
- practitionerId
12831
+ practitionerId,
12832
+ void 0,
12833
+ // clinicBranchId (optional)
12834
+ false
12835
+ // excludeDraftPractitioners - must be false to find procedures for draft practitioners
12836
+ );
12837
+ console.log(
12838
+ `[DisableFreeConsultation] Found ${existingProcedures.length} procedures for practitioner ${practitionerId}`
12506
12839
  );
12507
12840
  const freeConsultation = existingProcedures.find(
12508
12841
  (procedure) => procedure.technology.id === "free-consultation-tech" && procedure.clinicBranchId === clinicId && procedure.isActive
12509
12842
  );
12510
12843
  if (!freeConsultation) {
12511
12844
  console.log(
12512
- `No active free consultation found for practitioner ${practitionerId} in clinic ${clinicId}`
12845
+ `[DisableFreeConsultation] No active free consultation found for practitioner ${practitionerId} in clinic ${clinicId}`
12846
+ );
12847
+ console.log(
12848
+ `[DisableFreeConsultation] Existing procedures:`,
12849
+ existingProcedures.map((p) => {
12850
+ var _a;
12851
+ return {
12852
+ id: p.id,
12853
+ name: p.name,
12854
+ technologyId: (_a = p.technology) == null ? void 0 : _a.id,
12855
+ clinicBranchId: p.clinicBranchId,
12856
+ isActive: p.isActive
12857
+ };
12858
+ })
12513
12859
  );
12514
12860
  return;
12515
12861
  }
12862
+ console.log(
12863
+ `[DisableFreeConsultation] Found free consultation procedure ${freeConsultation.id}, deactivating...`
12864
+ );
12516
12865
  await this.getProcedureService().deactivateProcedure(freeConsultation.id);
12517
12866
  console.log(
12518
12867
  `Free consultation disabled for practitioner ${practitionerId} in clinic ${clinicId}`
@@ -15759,6 +16108,135 @@ var AuthService = class extends BaseService {
15759
16108
  throw handleFirebaseError(error);
15760
16109
  }
15761
16110
  }
16111
+ /**
16112
+ * Signs up or signs in a practitioner with Google authentication.
16113
+ * Checks for existing practitioner account or draft profiles.
16114
+ *
16115
+ * @param idToken - The Google ID token obtained from the mobile app
16116
+ * @returns Object containing user, practitioner (if exists), and draft profiles (if any)
16117
+ */
16118
+ async signUpPractitionerWithGoogle(idToken) {
16119
+ try {
16120
+ console.log("[AUTH] Starting practitioner Google Sign-In/Sign-Up");
16121
+ let email;
16122
+ try {
16123
+ const payloadBase64 = idToken.split(".")[1];
16124
+ const payloadJson = globalThis.atob ? globalThis.atob(payloadBase64) : Buffer.from(payloadBase64, "base64").toString("utf8");
16125
+ const payload = JSON.parse(payloadJson);
16126
+ email = payload.email;
16127
+ } catch (decodeError) {
16128
+ console.error("[AUTH] Failed to decode email from Google ID token:", decodeError);
16129
+ throw new AuthError(
16130
+ "Unable to read email from Google token. Please try again.",
16131
+ "AUTH/INVALID_GOOGLE_TOKEN",
16132
+ 400
16133
+ );
16134
+ }
16135
+ if (!email) {
16136
+ throw new AuthError(
16137
+ "Unable to read email from Google token. Please try again.",
16138
+ "AUTH/INVALID_GOOGLE_TOKEN",
16139
+ 400
16140
+ );
16141
+ }
16142
+ const normalizedEmail = email.toLowerCase().trim();
16143
+ console.log("[AUTH] Extracted email from Google token:", normalizedEmail);
16144
+ const methods = await fetchSignInMethodsForEmail2(this.auth, normalizedEmail);
16145
+ const hasGoogleMethod = methods.includes(GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD);
16146
+ const hasEmailMethod = methods.includes(EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD);
16147
+ const practitionerService = new PractitionerService(this.db, this.auth, this.app);
16148
+ if (hasGoogleMethod) {
16149
+ console.log("[AUTH] User exists with Google provider, signing in");
16150
+ const credential2 = GoogleAuthProvider.credential(idToken);
16151
+ const { user: firebaseUser2 } = await signInWithCredential(this.auth, credential2);
16152
+ const existingUser2 = await this.userService.getUserById(firebaseUser2.uid);
16153
+ if (!existingUser2) {
16154
+ await firebaseSignOut(this.auth);
16155
+ throw new AuthError(
16156
+ "No account found. Please contact support.",
16157
+ "AUTH/USER_NOT_FOUND",
16158
+ 404
16159
+ );
16160
+ }
16161
+ let practitioner2 = null;
16162
+ if (existingUser2.practitionerProfile) {
16163
+ practitioner2 = await practitionerService.getPractitioner(existingUser2.practitionerProfile);
16164
+ }
16165
+ const draftProfiles2 = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
16166
+ return {
16167
+ user: existingUser2,
16168
+ practitioner: practitioner2,
16169
+ draftProfiles: draftProfiles2
16170
+ };
16171
+ }
16172
+ if (hasEmailMethod && !hasGoogleMethod) {
16173
+ console.log("[AUTH] User exists with email/password only");
16174
+ throw new AuthError(
16175
+ "An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.",
16176
+ "AUTH/EMAIL_ALREADY_EXISTS",
16177
+ 409
16178
+ );
16179
+ }
16180
+ console.log("[AUTH] Signing in with Google credential");
16181
+ const credential = GoogleAuthProvider.credential(idToken);
16182
+ let firebaseUser;
16183
+ try {
16184
+ const result = await signInWithCredential(this.auth, credential);
16185
+ firebaseUser = result.user;
16186
+ } catch (error) {
16187
+ if (error.code === "auth/account-exists-with-different-credential") {
16188
+ throw new AuthError(
16189
+ "An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.",
16190
+ "AUTH/EMAIL_ALREADY_EXISTS",
16191
+ 409
16192
+ );
16193
+ }
16194
+ throw error;
16195
+ }
16196
+ let existingUser = null;
16197
+ try {
16198
+ const existingUserDoc = await this.userService.getUserById(firebaseUser.uid);
16199
+ if (existingUserDoc) {
16200
+ existingUser = existingUserDoc;
16201
+ console.log("[AUTH] Found existing User document");
16202
+ }
16203
+ } catch (error) {
16204
+ console.error("[AUTH] Error checking for existing user:", error);
16205
+ }
16206
+ console.log("[AUTH] Checking for draft profiles");
16207
+ const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
16208
+ let user;
16209
+ if (existingUser) {
16210
+ user = existingUser;
16211
+ console.log("[AUTH] Using existing user account");
16212
+ } else {
16213
+ user = await this.userService.createUser(firebaseUser, ["practitioner" /* PRACTITIONER */], {
16214
+ skipProfileCreation: true
16215
+ });
16216
+ console.log("[AUTH] Created new user account");
16217
+ }
16218
+ let practitioner = null;
16219
+ if (user.practitionerProfile) {
16220
+ practitioner = await practitionerService.getPractitioner(user.practitionerProfile);
16221
+ }
16222
+ console.log("[AUTH] Google Sign-In complete", {
16223
+ userId: user.uid,
16224
+ hasPractitioner: !!practitioner,
16225
+ draftProfilesCount: draftProfiles.length
16226
+ });
16227
+ return {
16228
+ user,
16229
+ practitioner,
16230
+ draftProfiles
16231
+ };
16232
+ } catch (error) {
16233
+ console.error("[AUTH] Error in signUpPractitionerWithGoogle:", error);
16234
+ if (error instanceof AuthError) {
16235
+ throw error;
16236
+ }
16237
+ throw handleFirebaseError(error);
16238
+ }
16239
+ }
15762
16240
  /**
15763
16241
  * Links a Google account to the currently signed-in user using an ID token.
15764
16242
  * This is used to upgrade an anonymous user or to allow an existing user
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.13.20",
4
+ "version": "1.14.0",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -78,10 +78,21 @@ export const practitionerInvitationTemplate = `
78
78
 
79
79
  <p>This token will expire on <strong>{{expirationDate}}</strong>.</p>
80
80
 
81
- <p>To create your account:</p>
81
+ <p><strong>You have two options to create your account:</strong></p>
82
+
83
+ <p><strong>Option 1: Sign in with Google (Recommended)</strong></p>
84
+ <ol>
85
+ <li>Open the MetaEsthetics Doctor App</li>
86
+ <li>Click "Sign in with Google" on the login screen</li>
87
+ <li>Select your Google account (use the email address: {{practitionerEmail}})</li>
88
+ <li>You'll see an invitation to join {{clinicName}} - simply select it and join!</li>
89
+ </ol>
90
+
91
+ <p><strong>Option 2: Use Email/Password with Token</strong></p>
82
92
  <ol>
83
93
  <li>Visit {{registrationUrl}}</li>
84
- <li>Enter your email and create a password</li>
94
+ <li>Click "Claim Existing Profile with Token"</li>
95
+ <li>Enter your email ({{practitionerEmail}}) and create a password</li>
85
96
  <li>When prompted, enter the token above</li>
86
97
  </ol>
87
98