@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/admin/index.js +13 -2
- package/dist/admin/index.mjs +13 -2
- package/dist/index.d.mts +43 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +489 -11
- package/dist/index.mjs +489 -11
- package/package.json +1 -1
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +13 -2
- package/src/services/auth/auth.service.ts +173 -0
- package/src/services/practitioner/practitioner.service.ts +417 -9
- package/src/validations/patient/medical-info.schema.ts +55 -3
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
`
|
|
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
|
@@ -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>
|
|
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>
|
|
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
|
|