@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.js
CHANGED
|
@@ -7838,7 +7838,7 @@ var contraindicationSchema = import_zod6.z.object({
|
|
|
7838
7838
|
notes: import_zod6.z.string().optional().nullable(),
|
|
7839
7839
|
isActive: import_zod6.z.boolean()
|
|
7840
7840
|
});
|
|
7841
|
-
var
|
|
7841
|
+
var baseMedicationSchema = import_zod6.z.object({
|
|
7842
7842
|
name: import_zod6.z.string().min(1),
|
|
7843
7843
|
dosage: import_zod6.z.string().min(1),
|
|
7844
7844
|
frequency: import_zod6.z.string().min(1),
|
|
@@ -7846,6 +7846,24 @@ var medicationSchema = import_zod6.z.object({
|
|
|
7846
7846
|
endDate: timestampSchema.optional().nullable(),
|
|
7847
7847
|
prescribedBy: import_zod6.z.string().optional().nullable()
|
|
7848
7848
|
});
|
|
7849
|
+
var medicationSchema = baseMedicationSchema.refine(
|
|
7850
|
+
(data) => {
|
|
7851
|
+
if (!data.endDate) {
|
|
7852
|
+
return true;
|
|
7853
|
+
}
|
|
7854
|
+
if (!data.startDate) {
|
|
7855
|
+
return false;
|
|
7856
|
+
}
|
|
7857
|
+
const startDate = data.startDate.toDate();
|
|
7858
|
+
const endDate = data.endDate.toDate();
|
|
7859
|
+
return endDate >= startDate;
|
|
7860
|
+
},
|
|
7861
|
+
{
|
|
7862
|
+
message: "End date requires a start date and must be equal to or after start date",
|
|
7863
|
+
path: ["endDate"]
|
|
7864
|
+
// This will attach the error to the endDate field
|
|
7865
|
+
}
|
|
7866
|
+
);
|
|
7849
7867
|
var patientMedicalInfoSchema = import_zod6.z.object({
|
|
7850
7868
|
patientId: import_zod6.z.string(),
|
|
7851
7869
|
vitalStats: vitalStatsSchema,
|
|
@@ -7881,9 +7899,26 @@ var updateContraindicationSchema = contraindicationSchema.partial().extend({
|
|
|
7881
7899
|
contraindicationIndex: import_zod6.z.number().min(0)
|
|
7882
7900
|
});
|
|
7883
7901
|
var addMedicationSchema = medicationSchema;
|
|
7884
|
-
var updateMedicationSchema =
|
|
7902
|
+
var updateMedicationSchema = baseMedicationSchema.partial().extend({
|
|
7885
7903
|
medicationIndex: import_zod6.z.number().min(0)
|
|
7886
|
-
})
|
|
7904
|
+
}).refine(
|
|
7905
|
+
(data) => {
|
|
7906
|
+
if (!data.endDate) {
|
|
7907
|
+
return true;
|
|
7908
|
+
}
|
|
7909
|
+
if (!data.startDate) {
|
|
7910
|
+
return false;
|
|
7911
|
+
}
|
|
7912
|
+
const startDate = data.startDate.toDate();
|
|
7913
|
+
const endDate = data.endDate.toDate();
|
|
7914
|
+
return endDate >= startDate;
|
|
7915
|
+
},
|
|
7916
|
+
{
|
|
7917
|
+
message: "End date requires a start date and must be equal to or after start date",
|
|
7918
|
+
path: ["endDate"]
|
|
7919
|
+
// This will attach the error to the endDate field
|
|
7920
|
+
}
|
|
7921
|
+
);
|
|
7887
7922
|
|
|
7888
7923
|
// src/validations/patient.schema.ts
|
|
7889
7924
|
var locationDataSchema = import_zod7.z.object({
|
|
@@ -11680,6 +11715,240 @@ var PractitionerService = class extends BaseService {
|
|
|
11680
11715
|
return null;
|
|
11681
11716
|
}
|
|
11682
11717
|
}
|
|
11718
|
+
/**
|
|
11719
|
+
* Finds all draft practitioner profiles by email address
|
|
11720
|
+
* Used when a doctor signs in with Google to show all clinic invitations
|
|
11721
|
+
*
|
|
11722
|
+
* @param email - Email address to search for
|
|
11723
|
+
* @returns Array of draft practitioner profiles with clinic information
|
|
11724
|
+
*
|
|
11725
|
+
* @remarks
|
|
11726
|
+
* Requires Firestore composite index on:
|
|
11727
|
+
* - Collection: practitioners
|
|
11728
|
+
* - Fields: basicInfo.email (Ascending), status (Ascending), userRef (Ascending)
|
|
11729
|
+
*/
|
|
11730
|
+
async getDraftProfilesByEmail(email) {
|
|
11731
|
+
try {
|
|
11732
|
+
const normalizedEmail = email.toLowerCase().trim();
|
|
11733
|
+
console.log("[PRACTITIONER] Searching for all draft practitioners by email", {
|
|
11734
|
+
email: normalizedEmail
|
|
11735
|
+
});
|
|
11736
|
+
const q = (0, import_firestore32.query)(
|
|
11737
|
+
(0, import_firestore32.collection)(this.db, PRACTITIONERS_COLLECTION),
|
|
11738
|
+
(0, import_firestore32.where)("basicInfo.email", "==", normalizedEmail),
|
|
11739
|
+
(0, import_firestore32.where)("status", "==", "draft" /* DRAFT */),
|
|
11740
|
+
(0, import_firestore32.where)("userRef", "==", "")
|
|
11741
|
+
);
|
|
11742
|
+
const querySnapshot = await (0, import_firestore32.getDocs)(q);
|
|
11743
|
+
if (querySnapshot.empty) {
|
|
11744
|
+
console.log("[PRACTITIONER] No draft practitioners found for email", {
|
|
11745
|
+
email: normalizedEmail
|
|
11746
|
+
});
|
|
11747
|
+
return [];
|
|
11748
|
+
}
|
|
11749
|
+
const draftPractitioners = querySnapshot.docs.map(
|
|
11750
|
+
(doc47) => doc47.data()
|
|
11751
|
+
);
|
|
11752
|
+
console.log("[PRACTITIONER] Found draft practitioners", {
|
|
11753
|
+
email: normalizedEmail,
|
|
11754
|
+
count: draftPractitioners.length,
|
|
11755
|
+
practitionerIds: draftPractitioners.map((p) => p.id)
|
|
11756
|
+
});
|
|
11757
|
+
return draftPractitioners;
|
|
11758
|
+
} catch (error) {
|
|
11759
|
+
console.error(
|
|
11760
|
+
"[PRACTITIONER] Error finding draft practitioners by email:",
|
|
11761
|
+
error
|
|
11762
|
+
);
|
|
11763
|
+
return [];
|
|
11764
|
+
}
|
|
11765
|
+
}
|
|
11766
|
+
/**
|
|
11767
|
+
* Claims a draft practitioner profile and links it to a user account
|
|
11768
|
+
* Used when a doctor selects which clinic(s) to join after Google Sign-In
|
|
11769
|
+
*
|
|
11770
|
+
* @param practitionerId - ID of the draft practitioner profile to claim
|
|
11771
|
+
* @param userId - ID of the user account to link the profile to
|
|
11772
|
+
* @returns The claimed practitioner profile
|
|
11773
|
+
*/
|
|
11774
|
+
async claimDraftProfileWithGoogle(practitionerId, userId) {
|
|
11775
|
+
try {
|
|
11776
|
+
console.log("[PRACTITIONER] Claiming draft profile with Google", {
|
|
11777
|
+
practitionerId,
|
|
11778
|
+
userId
|
|
11779
|
+
});
|
|
11780
|
+
const practitioner = await this.getPractitioner(practitionerId);
|
|
11781
|
+
if (!practitioner) {
|
|
11782
|
+
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
11783
|
+
}
|
|
11784
|
+
if (practitioner.status !== "draft" /* DRAFT */) {
|
|
11785
|
+
throw new Error("This practitioner profile has already been claimed");
|
|
11786
|
+
}
|
|
11787
|
+
const existingPractitioner = await this.getPractitionerByUserRef(userId);
|
|
11788
|
+
if (existingPractitioner) {
|
|
11789
|
+
console.log("[PRACTITIONER] User already has profile, merging clinics");
|
|
11790
|
+
const mergedClinics = Array.from(/* @__PURE__ */ new Set([
|
|
11791
|
+
...existingPractitioner.clinics,
|
|
11792
|
+
...practitioner.clinics
|
|
11793
|
+
]));
|
|
11794
|
+
const mergedWorkingHours = [...existingPractitioner.clinicWorkingHours];
|
|
11795
|
+
for (const workingHours of practitioner.clinicWorkingHours) {
|
|
11796
|
+
if (!mergedWorkingHours.find((wh) => wh.clinicId === workingHours.clinicId)) {
|
|
11797
|
+
mergedWorkingHours.push(workingHours);
|
|
11798
|
+
}
|
|
11799
|
+
}
|
|
11800
|
+
const mergedClinicsInfo = [...existingPractitioner.clinicsInfo];
|
|
11801
|
+
for (const clinicInfo of practitioner.clinicsInfo) {
|
|
11802
|
+
if (!mergedClinicsInfo.find((ci) => ci.id === clinicInfo.id)) {
|
|
11803
|
+
mergedClinicsInfo.push(clinicInfo);
|
|
11804
|
+
}
|
|
11805
|
+
}
|
|
11806
|
+
const updatedPractitioner2 = await this.updatePractitioner(existingPractitioner.id, {
|
|
11807
|
+
clinics: mergedClinics,
|
|
11808
|
+
clinicWorkingHours: mergedWorkingHours,
|
|
11809
|
+
clinicsInfo: mergedClinicsInfo
|
|
11810
|
+
});
|
|
11811
|
+
await (0, import_firestore32.deleteDoc)((0, import_firestore32.doc)(this.db, PRACTITIONERS_COLLECTION, practitionerId));
|
|
11812
|
+
const activeTokens2 = await this.getPractitionerActiveTokens(practitionerId);
|
|
11813
|
+
for (const token of activeTokens2) {
|
|
11814
|
+
await this.markTokenAsUsed(token.id, practitionerId, userId);
|
|
11815
|
+
}
|
|
11816
|
+
return updatedPractitioner2;
|
|
11817
|
+
}
|
|
11818
|
+
const updatedPractitioner = await this.updatePractitioner(practitioner.id, {
|
|
11819
|
+
userRef: userId,
|
|
11820
|
+
status: "active" /* ACTIVE */
|
|
11821
|
+
});
|
|
11822
|
+
const activeTokens = await this.getPractitionerActiveTokens(practitionerId);
|
|
11823
|
+
for (const token of activeTokens) {
|
|
11824
|
+
await this.markTokenAsUsed(token.id, practitionerId, userId);
|
|
11825
|
+
}
|
|
11826
|
+
console.log("[PRACTITIONER] Draft profile claimed successfully", {
|
|
11827
|
+
practitionerId: updatedPractitioner.id,
|
|
11828
|
+
userId
|
|
11829
|
+
});
|
|
11830
|
+
return updatedPractitioner;
|
|
11831
|
+
} catch (error) {
|
|
11832
|
+
console.error(
|
|
11833
|
+
"[PRACTITIONER] Error claiming draft profile with Google:",
|
|
11834
|
+
error
|
|
11835
|
+
);
|
|
11836
|
+
throw error;
|
|
11837
|
+
}
|
|
11838
|
+
}
|
|
11839
|
+
/**
|
|
11840
|
+
* Claims multiple draft practitioner profiles and merges them into one profile
|
|
11841
|
+
* Used when a doctor selects multiple clinics to join after Google Sign-In
|
|
11842
|
+
*
|
|
11843
|
+
* @param practitionerIds - Array of draft practitioner profile IDs to claim
|
|
11844
|
+
* @param userId - ID of the user account to link the profiles to
|
|
11845
|
+
* @returns The claimed practitioner profile (first one becomes main, others merged)
|
|
11846
|
+
*/
|
|
11847
|
+
async claimMultipleDraftProfilesWithGoogle(practitionerIds, userId) {
|
|
11848
|
+
try {
|
|
11849
|
+
if (practitionerIds.length === 0) {
|
|
11850
|
+
throw new Error("No practitioner IDs provided");
|
|
11851
|
+
}
|
|
11852
|
+
console.log("[PRACTITIONER] Claiming multiple draft profiles with Google", {
|
|
11853
|
+
practitionerIds,
|
|
11854
|
+
userId,
|
|
11855
|
+
count: practitionerIds.length
|
|
11856
|
+
});
|
|
11857
|
+
const draftProfiles = await Promise.all(
|
|
11858
|
+
practitionerIds.map((id) => this.getPractitioner(id))
|
|
11859
|
+
);
|
|
11860
|
+
const validDrafts = draftProfiles.filter((p) => {
|
|
11861
|
+
if (!p) return false;
|
|
11862
|
+
if (p.status !== "draft" /* DRAFT */) {
|
|
11863
|
+
throw new Error(`Practitioner ${p.id} has already been claimed`);
|
|
11864
|
+
}
|
|
11865
|
+
return true;
|
|
11866
|
+
});
|
|
11867
|
+
if (validDrafts.length === 0) {
|
|
11868
|
+
throw new Error("No valid draft profiles found");
|
|
11869
|
+
}
|
|
11870
|
+
const existingPractitioner = await this.getPractitionerByUserRef(userId);
|
|
11871
|
+
if (existingPractitioner) {
|
|
11872
|
+
let mergedClinics2 = new Set(existingPractitioner.clinics);
|
|
11873
|
+
let mergedWorkingHours2 = [...existingPractitioner.clinicWorkingHours];
|
|
11874
|
+
let mergedClinicsInfo2 = [...existingPractitioner.clinicsInfo];
|
|
11875
|
+
for (const draft of validDrafts) {
|
|
11876
|
+
draft.clinics.forEach((clinicId) => mergedClinics2.add(clinicId));
|
|
11877
|
+
for (const workingHours of draft.clinicWorkingHours) {
|
|
11878
|
+
if (!mergedWorkingHours2.find((wh) => wh.clinicId === workingHours.clinicId)) {
|
|
11879
|
+
mergedWorkingHours2.push(workingHours);
|
|
11880
|
+
}
|
|
11881
|
+
}
|
|
11882
|
+
for (const clinicInfo of draft.clinicsInfo) {
|
|
11883
|
+
if (!mergedClinicsInfo2.find((ci) => ci.id === clinicInfo.id)) {
|
|
11884
|
+
mergedClinicsInfo2.push(clinicInfo);
|
|
11885
|
+
}
|
|
11886
|
+
}
|
|
11887
|
+
}
|
|
11888
|
+
const updatedPractitioner2 = await this.updatePractitioner(existingPractitioner.id, {
|
|
11889
|
+
clinics: Array.from(mergedClinics2),
|
|
11890
|
+
clinicWorkingHours: mergedWorkingHours2,
|
|
11891
|
+
clinicsInfo: mergedClinicsInfo2
|
|
11892
|
+
});
|
|
11893
|
+
for (const draft of validDrafts) {
|
|
11894
|
+
await (0, import_firestore32.deleteDoc)((0, import_firestore32.doc)(this.db, PRACTITIONERS_COLLECTION, draft.id));
|
|
11895
|
+
const activeTokens = await this.getPractitionerActiveTokens(draft.id);
|
|
11896
|
+
for (const token of activeTokens) {
|
|
11897
|
+
await this.markTokenAsUsed(token.id, draft.id, userId);
|
|
11898
|
+
}
|
|
11899
|
+
}
|
|
11900
|
+
return updatedPractitioner2;
|
|
11901
|
+
}
|
|
11902
|
+
const mainDraft = validDrafts[0];
|
|
11903
|
+
const otherDrafts = validDrafts.slice(1);
|
|
11904
|
+
let mergedClinics = new Set(mainDraft.clinics);
|
|
11905
|
+
let mergedWorkingHours = [...mainDraft.clinicWorkingHours];
|
|
11906
|
+
let mergedClinicsInfo = [...mainDraft.clinicsInfo];
|
|
11907
|
+
for (const draft of otherDrafts) {
|
|
11908
|
+
draft.clinics.forEach((clinicId) => mergedClinics.add(clinicId));
|
|
11909
|
+
for (const workingHours of draft.clinicWorkingHours) {
|
|
11910
|
+
if (!mergedWorkingHours.find((wh) => wh.clinicId === workingHours.clinicId)) {
|
|
11911
|
+
mergedWorkingHours.push(workingHours);
|
|
11912
|
+
}
|
|
11913
|
+
}
|
|
11914
|
+
for (const clinicInfo of draft.clinicsInfo) {
|
|
11915
|
+
if (!mergedClinicsInfo.find((ci) => ci.id === clinicInfo.id)) {
|
|
11916
|
+
mergedClinicsInfo.push(clinicInfo);
|
|
11917
|
+
}
|
|
11918
|
+
}
|
|
11919
|
+
}
|
|
11920
|
+
const updatedPractitioner = await this.updatePractitioner(mainDraft.id, {
|
|
11921
|
+
userRef: userId,
|
|
11922
|
+
status: "active" /* ACTIVE */,
|
|
11923
|
+
clinics: Array.from(mergedClinics),
|
|
11924
|
+
clinicWorkingHours: mergedWorkingHours,
|
|
11925
|
+
clinicsInfo: mergedClinicsInfo
|
|
11926
|
+
});
|
|
11927
|
+
const mainActiveTokens = await this.getPractitionerActiveTokens(mainDraft.id);
|
|
11928
|
+
for (const token of mainActiveTokens) {
|
|
11929
|
+
await this.markTokenAsUsed(token.id, mainDraft.id, userId);
|
|
11930
|
+
}
|
|
11931
|
+
for (const draft of otherDrafts) {
|
|
11932
|
+
await (0, import_firestore32.deleteDoc)((0, import_firestore32.doc)(this.db, PRACTITIONERS_COLLECTION, draft.id));
|
|
11933
|
+
const activeTokens = await this.getPractitionerActiveTokens(draft.id);
|
|
11934
|
+
for (const token of activeTokens) {
|
|
11935
|
+
await this.markTokenAsUsed(token.id, draft.id, userId);
|
|
11936
|
+
}
|
|
11937
|
+
}
|
|
11938
|
+
console.log("[PRACTITIONER] Multiple draft profiles claimed successfully", {
|
|
11939
|
+
practitionerId: updatedPractitioner.id,
|
|
11940
|
+
userId,
|
|
11941
|
+
mergedCount: validDrafts.length
|
|
11942
|
+
});
|
|
11943
|
+
return updatedPractitioner;
|
|
11944
|
+
} catch (error) {
|
|
11945
|
+
console.error(
|
|
11946
|
+
"[PRACTITIONER] Error claiming multiple draft profiles with Google:",
|
|
11947
|
+
error
|
|
11948
|
+
);
|
|
11949
|
+
throw error;
|
|
11950
|
+
}
|
|
11951
|
+
}
|
|
11683
11952
|
/**
|
|
11684
11953
|
* Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
|
|
11685
11954
|
*/
|
|
@@ -12315,6 +12584,9 @@ var PractitionerService = class extends BaseService {
|
|
|
12315
12584
|
*/
|
|
12316
12585
|
async EnableFreeConsultation(practitionerId, clinicId) {
|
|
12317
12586
|
try {
|
|
12587
|
+
console.log(
|
|
12588
|
+
`[EnableFreeConsultation] Starting for practitioner ${practitionerId} in clinic ${clinicId}`
|
|
12589
|
+
);
|
|
12318
12590
|
await this.ensureFreeConsultationInfrastructure();
|
|
12319
12591
|
const practitioner = await this.getPractitioner(practitionerId);
|
|
12320
12592
|
if (!practitioner) {
|
|
@@ -12330,32 +12602,83 @@ var PractitionerService = class extends BaseService {
|
|
|
12330
12602
|
);
|
|
12331
12603
|
}
|
|
12332
12604
|
const [activeProcedures, inactiveProcedures] = await Promise.all([
|
|
12333
|
-
this.getProcedureService().getProceduresByPractitioner(
|
|
12605
|
+
this.getProcedureService().getProceduresByPractitioner(
|
|
12606
|
+
practitionerId,
|
|
12607
|
+
void 0,
|
|
12608
|
+
// clinicBranchId
|
|
12609
|
+
false
|
|
12610
|
+
// excludeDraftPractitioners - allow draft practitioners
|
|
12611
|
+
),
|
|
12334
12612
|
this.getProcedureService().getInactiveProceduresByPractitioner(
|
|
12335
12613
|
practitionerId
|
|
12336
12614
|
)
|
|
12337
12615
|
]);
|
|
12338
12616
|
const allProcedures = [...activeProcedures, ...inactiveProcedures];
|
|
12339
|
-
const
|
|
12617
|
+
const existingConsultations = allProcedures.filter(
|
|
12340
12618
|
(procedure) => procedure.technology.id === "free-consultation-tech" && procedure.clinicBranchId === clinicId
|
|
12341
12619
|
);
|
|
12620
|
+
console.log(
|
|
12621
|
+
`[EnableFreeConsultation] Found ${existingConsultations.length} existing free consultation(s)`
|
|
12622
|
+
);
|
|
12623
|
+
if (existingConsultations.length > 1) {
|
|
12624
|
+
console.warn(
|
|
12625
|
+
`[EnableFreeConsultation] WARNING: Found ${existingConsultations.length} duplicate free consultations for practitioner ${practitionerId} in clinic ${clinicId}`
|
|
12626
|
+
);
|
|
12627
|
+
for (let i = 1; i < existingConsultations.length; i++) {
|
|
12628
|
+
console.log(
|
|
12629
|
+
`[EnableFreeConsultation] Deactivating duplicate consultation ${existingConsultations[i].id}`
|
|
12630
|
+
);
|
|
12631
|
+
await this.getProcedureService().deactivateProcedure(
|
|
12632
|
+
existingConsultations[i].id
|
|
12633
|
+
);
|
|
12634
|
+
}
|
|
12635
|
+
}
|
|
12636
|
+
const existingConsultation = existingConsultations[0];
|
|
12342
12637
|
if (existingConsultation) {
|
|
12343
12638
|
if (existingConsultation.isActive) {
|
|
12344
12639
|
console.log(
|
|
12345
|
-
`Free consultation already active for practitioner ${practitionerId} in clinic ${clinicId}`
|
|
12640
|
+
`[EnableFreeConsultation] Free consultation already active for practitioner ${practitionerId} in clinic ${clinicId}`
|
|
12346
12641
|
);
|
|
12347
12642
|
return;
|
|
12348
12643
|
} else {
|
|
12644
|
+
console.log(
|
|
12645
|
+
`[EnableFreeConsultation] Reactivating existing consultation ${existingConsultation.id}`
|
|
12646
|
+
);
|
|
12349
12647
|
await this.getProcedureService().updateProcedure(
|
|
12350
12648
|
existingConsultation.id,
|
|
12351
12649
|
{ isActive: true }
|
|
12352
12650
|
);
|
|
12353
12651
|
console.log(
|
|
12354
|
-
`Reactivated existing free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
|
|
12652
|
+
`[EnableFreeConsultation] Reactivated existing free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
|
|
12355
12653
|
);
|
|
12356
12654
|
return;
|
|
12357
12655
|
}
|
|
12358
12656
|
}
|
|
12657
|
+
console.log(
|
|
12658
|
+
`[EnableFreeConsultation] Final race condition check before creating new procedure`
|
|
12659
|
+
);
|
|
12660
|
+
const finalCheckProcedures = await this.getProcedureService().getProceduresByPractitioner(
|
|
12661
|
+
practitionerId,
|
|
12662
|
+
void 0,
|
|
12663
|
+
// clinicBranchId
|
|
12664
|
+
false
|
|
12665
|
+
// excludeDraftPractitioners - allow draft practitioners
|
|
12666
|
+
);
|
|
12667
|
+
const raceConditionCheck = finalCheckProcedures.find(
|
|
12668
|
+
(procedure) => procedure.technology.id === "free-consultation-tech" && procedure.clinicBranchId === clinicId
|
|
12669
|
+
);
|
|
12670
|
+
if (raceConditionCheck) {
|
|
12671
|
+
console.log(
|
|
12672
|
+
`[EnableFreeConsultation] Race condition detected! Procedure was created by another request. Using existing procedure ${raceConditionCheck.id}`
|
|
12673
|
+
);
|
|
12674
|
+
if (!raceConditionCheck.isActive) {
|
|
12675
|
+
await this.getProcedureService().updateProcedure(
|
|
12676
|
+
raceConditionCheck.id,
|
|
12677
|
+
{ isActive: true }
|
|
12678
|
+
);
|
|
12679
|
+
}
|
|
12680
|
+
return;
|
|
12681
|
+
}
|
|
12359
12682
|
const consultationData = {
|
|
12360
12683
|
name: "Free Consultation",
|
|
12361
12684
|
nameLower: "free consultation",
|
|
@@ -12375,15 +12698,18 @@ var PractitionerService = class extends BaseService {
|
|
|
12375
12698
|
photos: []
|
|
12376
12699
|
// No photos for consultation
|
|
12377
12700
|
};
|
|
12701
|
+
console.log(
|
|
12702
|
+
`[EnableFreeConsultation] Creating new free consultation procedure`
|
|
12703
|
+
);
|
|
12378
12704
|
await this.getProcedureService().createConsultationProcedure(
|
|
12379
12705
|
consultationData
|
|
12380
12706
|
);
|
|
12381
12707
|
console.log(
|
|
12382
|
-
`
|
|
12708
|
+
`[EnableFreeConsultation] Successfully created free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
|
|
12383
12709
|
);
|
|
12384
12710
|
} catch (error) {
|
|
12385
12711
|
console.error(
|
|
12386
|
-
`Error enabling free consultation for practitioner ${practitionerId} in clinic ${clinicId}:`,
|
|
12712
|
+
`[EnableFreeConsultation] Error enabling free consultation for practitioner ${practitionerId} in clinic ${clinicId}:`,
|
|
12387
12713
|
error
|
|
12388
12714
|
);
|
|
12389
12715
|
throw error;
|
|
@@ -12479,17 +12805,40 @@ var PractitionerService = class extends BaseService {
|
|
|
12479
12805
|
);
|
|
12480
12806
|
}
|
|
12481
12807
|
const existingProcedures = await this.getProcedureService().getProceduresByPractitioner(
|
|
12482
|
-
practitionerId
|
|
12808
|
+
practitionerId,
|
|
12809
|
+
void 0,
|
|
12810
|
+
// clinicBranchId (optional)
|
|
12811
|
+
false
|
|
12812
|
+
// excludeDraftPractitioners - must be false to find procedures for draft practitioners
|
|
12813
|
+
);
|
|
12814
|
+
console.log(
|
|
12815
|
+
`[DisableFreeConsultation] Found ${existingProcedures.length} procedures for practitioner ${practitionerId}`
|
|
12483
12816
|
);
|
|
12484
12817
|
const freeConsultation = existingProcedures.find(
|
|
12485
12818
|
(procedure) => procedure.technology.id === "free-consultation-tech" && procedure.clinicBranchId === clinicId && procedure.isActive
|
|
12486
12819
|
);
|
|
12487
12820
|
if (!freeConsultation) {
|
|
12488
12821
|
console.log(
|
|
12489
|
-
`No active free consultation found for practitioner ${practitionerId} in clinic ${clinicId}`
|
|
12822
|
+
`[DisableFreeConsultation] No active free consultation found for practitioner ${practitionerId} in clinic ${clinicId}`
|
|
12823
|
+
);
|
|
12824
|
+
console.log(
|
|
12825
|
+
`[DisableFreeConsultation] Existing procedures:`,
|
|
12826
|
+
existingProcedures.map((p) => {
|
|
12827
|
+
var _a;
|
|
12828
|
+
return {
|
|
12829
|
+
id: p.id,
|
|
12830
|
+
name: p.name,
|
|
12831
|
+
technologyId: (_a = p.technology) == null ? void 0 : _a.id,
|
|
12832
|
+
clinicBranchId: p.clinicBranchId,
|
|
12833
|
+
isActive: p.isActive
|
|
12834
|
+
};
|
|
12835
|
+
})
|
|
12490
12836
|
);
|
|
12491
12837
|
return;
|
|
12492
12838
|
}
|
|
12839
|
+
console.log(
|
|
12840
|
+
`[DisableFreeConsultation] Found free consultation procedure ${freeConsultation.id}, deactivating...`
|
|
12841
|
+
);
|
|
12493
12842
|
await this.getProcedureService().deactivateProcedure(freeConsultation.id);
|
|
12494
12843
|
console.log(
|
|
12495
12844
|
`Free consultation disabled for practitioner ${practitionerId} in clinic ${clinicId}`
|
|
@@ -15672,6 +16021,135 @@ var AuthService = class extends BaseService {
|
|
|
15672
16021
|
throw handleFirebaseError(error);
|
|
15673
16022
|
}
|
|
15674
16023
|
}
|
|
16024
|
+
/**
|
|
16025
|
+
* Signs up or signs in a practitioner with Google authentication.
|
|
16026
|
+
* Checks for existing practitioner account or draft profiles.
|
|
16027
|
+
*
|
|
16028
|
+
* @param idToken - The Google ID token obtained from the mobile app
|
|
16029
|
+
* @returns Object containing user, practitioner (if exists), and draft profiles (if any)
|
|
16030
|
+
*/
|
|
16031
|
+
async signUpPractitionerWithGoogle(idToken) {
|
|
16032
|
+
try {
|
|
16033
|
+
console.log("[AUTH] Starting practitioner Google Sign-In/Sign-Up");
|
|
16034
|
+
let email;
|
|
16035
|
+
try {
|
|
16036
|
+
const payloadBase64 = idToken.split(".")[1];
|
|
16037
|
+
const payloadJson = globalThis.atob ? globalThis.atob(payloadBase64) : Buffer.from(payloadBase64, "base64").toString("utf8");
|
|
16038
|
+
const payload = JSON.parse(payloadJson);
|
|
16039
|
+
email = payload.email;
|
|
16040
|
+
} catch (decodeError) {
|
|
16041
|
+
console.error("[AUTH] Failed to decode email from Google ID token:", decodeError);
|
|
16042
|
+
throw new AuthError(
|
|
16043
|
+
"Unable to read email from Google token. Please try again.",
|
|
16044
|
+
"AUTH/INVALID_GOOGLE_TOKEN",
|
|
16045
|
+
400
|
|
16046
|
+
);
|
|
16047
|
+
}
|
|
16048
|
+
if (!email) {
|
|
16049
|
+
throw new AuthError(
|
|
16050
|
+
"Unable to read email from Google token. Please try again.",
|
|
16051
|
+
"AUTH/INVALID_GOOGLE_TOKEN",
|
|
16052
|
+
400
|
|
16053
|
+
);
|
|
16054
|
+
}
|
|
16055
|
+
const normalizedEmail = email.toLowerCase().trim();
|
|
16056
|
+
console.log("[AUTH] Extracted email from Google token:", normalizedEmail);
|
|
16057
|
+
const methods = await (0, import_auth8.fetchSignInMethodsForEmail)(this.auth, normalizedEmail);
|
|
16058
|
+
const hasGoogleMethod = methods.includes(import_auth8.GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD);
|
|
16059
|
+
const hasEmailMethod = methods.includes(import_auth8.EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD);
|
|
16060
|
+
const practitionerService = new PractitionerService(this.db, this.auth, this.app);
|
|
16061
|
+
if (hasGoogleMethod) {
|
|
16062
|
+
console.log("[AUTH] User exists with Google provider, signing in");
|
|
16063
|
+
const credential2 = import_auth8.GoogleAuthProvider.credential(idToken);
|
|
16064
|
+
const { user: firebaseUser2 } = await (0, import_auth8.signInWithCredential)(this.auth, credential2);
|
|
16065
|
+
const existingUser2 = await this.userService.getUserById(firebaseUser2.uid);
|
|
16066
|
+
if (!existingUser2) {
|
|
16067
|
+
await (0, import_auth8.signOut)(this.auth);
|
|
16068
|
+
throw new AuthError(
|
|
16069
|
+
"No account found. Please contact support.",
|
|
16070
|
+
"AUTH/USER_NOT_FOUND",
|
|
16071
|
+
404
|
|
16072
|
+
);
|
|
16073
|
+
}
|
|
16074
|
+
let practitioner2 = null;
|
|
16075
|
+
if (existingUser2.practitionerProfile) {
|
|
16076
|
+
practitioner2 = await practitionerService.getPractitioner(existingUser2.practitionerProfile);
|
|
16077
|
+
}
|
|
16078
|
+
const draftProfiles2 = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
|
|
16079
|
+
return {
|
|
16080
|
+
user: existingUser2,
|
|
16081
|
+
practitioner: practitioner2,
|
|
16082
|
+
draftProfiles: draftProfiles2
|
|
16083
|
+
};
|
|
16084
|
+
}
|
|
16085
|
+
if (hasEmailMethod && !hasGoogleMethod) {
|
|
16086
|
+
console.log("[AUTH] User exists with email/password only");
|
|
16087
|
+
throw new AuthError(
|
|
16088
|
+
"An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.",
|
|
16089
|
+
"AUTH/EMAIL_ALREADY_EXISTS",
|
|
16090
|
+
409
|
|
16091
|
+
);
|
|
16092
|
+
}
|
|
16093
|
+
console.log("[AUTH] Signing in with Google credential");
|
|
16094
|
+
const credential = import_auth8.GoogleAuthProvider.credential(idToken);
|
|
16095
|
+
let firebaseUser;
|
|
16096
|
+
try {
|
|
16097
|
+
const result = await (0, import_auth8.signInWithCredential)(this.auth, credential);
|
|
16098
|
+
firebaseUser = result.user;
|
|
16099
|
+
} catch (error) {
|
|
16100
|
+
if (error.code === "auth/account-exists-with-different-credential") {
|
|
16101
|
+
throw new AuthError(
|
|
16102
|
+
"An account with this email already exists. Please sign in with your email and password, then link your Google account in settings.",
|
|
16103
|
+
"AUTH/EMAIL_ALREADY_EXISTS",
|
|
16104
|
+
409
|
|
16105
|
+
);
|
|
16106
|
+
}
|
|
16107
|
+
throw error;
|
|
16108
|
+
}
|
|
16109
|
+
let existingUser = null;
|
|
16110
|
+
try {
|
|
16111
|
+
const existingUserDoc = await this.userService.getUserById(firebaseUser.uid);
|
|
16112
|
+
if (existingUserDoc) {
|
|
16113
|
+
existingUser = existingUserDoc;
|
|
16114
|
+
console.log("[AUTH] Found existing User document");
|
|
16115
|
+
}
|
|
16116
|
+
} catch (error) {
|
|
16117
|
+
console.error("[AUTH] Error checking for existing user:", error);
|
|
16118
|
+
}
|
|
16119
|
+
console.log("[AUTH] Checking for draft profiles");
|
|
16120
|
+
const draftProfiles = await practitionerService.getDraftProfilesByEmail(normalizedEmail);
|
|
16121
|
+
let user;
|
|
16122
|
+
if (existingUser) {
|
|
16123
|
+
user = existingUser;
|
|
16124
|
+
console.log("[AUTH] Using existing user account");
|
|
16125
|
+
} else {
|
|
16126
|
+
user = await this.userService.createUser(firebaseUser, ["practitioner" /* PRACTITIONER */], {
|
|
16127
|
+
skipProfileCreation: true
|
|
16128
|
+
});
|
|
16129
|
+
console.log("[AUTH] Created new user account");
|
|
16130
|
+
}
|
|
16131
|
+
let practitioner = null;
|
|
16132
|
+
if (user.practitionerProfile) {
|
|
16133
|
+
practitioner = await practitionerService.getPractitioner(user.practitionerProfile);
|
|
16134
|
+
}
|
|
16135
|
+
console.log("[AUTH] Google Sign-In complete", {
|
|
16136
|
+
userId: user.uid,
|
|
16137
|
+
hasPractitioner: !!practitioner,
|
|
16138
|
+
draftProfilesCount: draftProfiles.length
|
|
16139
|
+
});
|
|
16140
|
+
return {
|
|
16141
|
+
user,
|
|
16142
|
+
practitioner,
|
|
16143
|
+
draftProfiles
|
|
16144
|
+
};
|
|
16145
|
+
} catch (error) {
|
|
16146
|
+
console.error("[AUTH] Error in signUpPractitionerWithGoogle:", error);
|
|
16147
|
+
if (error instanceof AuthError) {
|
|
16148
|
+
throw error;
|
|
16149
|
+
}
|
|
16150
|
+
throw handleFirebaseError(error);
|
|
16151
|
+
}
|
|
16152
|
+
}
|
|
15675
16153
|
/**
|
|
15676
16154
|
* Links a Google account to the currently signed-in user using an ID token.
|
|
15677
16155
|
* This is used to upgrade an anonymous user or to allow an existing user
|