@blackcode_sa/metaestetics-api 1.13.6 → 1.13.10

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.d.mts CHANGED
@@ -6693,7 +6693,23 @@ declare class ProcedureService extends BaseService {
6693
6693
  private technologyService;
6694
6694
  private productService;
6695
6695
  private mediaService;
6696
+ private practitionerService?;
6696
6697
  constructor(db: Firestore, auth: Auth, app: FirebaseApp, categoryService: CategoryService, subcategoryService: SubcategoryService, technologyService: TechnologyService, productService: ProductService, mediaService: MediaService);
6698
+ setPractitionerService(practitionerService: PractitionerService): void;
6699
+ /**
6700
+ * Filters out procedures that should not be visible to patients:
6701
+ * 1. Procedures with no practitioner (missing practitionerId)
6702
+ * 2. Procedures where practitioner doesn't exist
6703
+ * 3. Procedures from draft practitioners
6704
+ * 4. Procedures from inactive practitioners (isActive === false)
6705
+ *
6706
+ * Note: Each procedure has ONE practitionerId. If that practitioner is inactive/draft/missing,
6707
+ * the procedure is filtered out.
6708
+ *
6709
+ * @param procedures - Array of procedures to filter
6710
+ * @returns Filtered array of procedures (excluding invalid/inactive/draft practitioners)
6711
+ */
6712
+ private filterDraftPractitionerProcedures;
6697
6713
  /**
6698
6714
  * Process media resource (string URL or File object)
6699
6715
  * @param media String URL or File object
@@ -6782,9 +6798,10 @@ declare class ProcedureService extends BaseService {
6782
6798
  * Gets all procedures for a practitioner
6783
6799
  * @param practitionerId - The ID of the practitioner
6784
6800
  * @param clinicBranchId - Optional clinic branch ID to filter by
6801
+ * @param excludeDraftPractitioners - Whether to exclude procedures if the practitioner is in DRAFT status
6785
6802
  * @returns List of procedures
6786
6803
  */
6787
- getProceduresByPractitioner(practitionerId: string, clinicBranchId?: string): Promise<Procedure[]>;
6804
+ getProceduresByPractitioner(practitionerId: string, clinicBranchId?: string, excludeDraftPractitioners?: boolean): Promise<Procedure[]>;
6788
6805
  /**
6789
6806
  * Gets all inactive procedures for a practitioner
6790
6807
  * @param practitionerId - The ID of the practitioner
@@ -6825,9 +6842,10 @@ declare class ProcedureService extends BaseService {
6825
6842
  *
6826
6843
  * @param pagination - Optional number of procedures per page (0 or undefined returns all)
6827
6844
  * @param lastDoc - Optional last document for pagination (if continuing from a previous page)
6845
+ * @param excludeDraftPractitioners - Whether to exclude procedures from draft practitioners (default: true)
6828
6846
  * @returns Object containing procedures array and the last document for pagination
6829
6847
  */
6830
- getAllProcedures(pagination?: number, lastDoc?: any): Promise<{
6848
+ getAllProcedures(pagination?: number, lastDoc?: any, excludeDraftPractitioners?: boolean): Promise<{
6831
6849
  procedures: Procedure[];
6832
6850
  lastDoc: any;
6833
6851
  }>;
@@ -6924,7 +6942,7 @@ declare class PractitionerService extends BaseService {
6924
6942
  private mediaService;
6925
6943
  private procedureService?;
6926
6944
  constructor(db: Firestore, auth: Auth, app: FirebaseApp, clinicService?: ClinicService, procedureService?: ProcedureService);
6927
- private getClinicService;
6945
+ getClinicService(): ClinicService;
6928
6946
  private getProcedureService;
6929
6947
  setClinicService(clinicService: ClinicService): void;
6930
6948
  setProcedureService(procedureService: ProcedureService): void;
@@ -6992,6 +7010,19 @@ declare class PractitionerService extends BaseService {
6992
7010
  * Dohvata zdravstvenog radnika po User ID-u
6993
7011
  */
6994
7012
  getPractitionerByUserRef(userRef: string): Promise<Practitioner | null>;
7013
+ /**
7014
+ * Finds a draft practitioner profile by email address
7015
+ * Used to detect if a draft profile exists when a doctor registers without a token
7016
+ *
7017
+ * @param email - Email address to search for
7018
+ * @returns Draft practitioner profile if found, null otherwise
7019
+ *
7020
+ * @remarks
7021
+ * Requires Firestore composite index on:
7022
+ * - Collection: practitioners
7023
+ * - Fields: basicInfo.email (Ascending), status (Ascending), userRef (Ascending)
7024
+ */
7025
+ findDraftPractitionerByEmail(email: string): Promise<Practitioner | null>;
6995
7026
  /**
6996
7027
  * Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
6997
7028
  */
package/dist/index.d.ts CHANGED
@@ -6693,7 +6693,23 @@ declare class ProcedureService extends BaseService {
6693
6693
  private technologyService;
6694
6694
  private productService;
6695
6695
  private mediaService;
6696
+ private practitionerService?;
6696
6697
  constructor(db: Firestore, auth: Auth, app: FirebaseApp, categoryService: CategoryService, subcategoryService: SubcategoryService, technologyService: TechnologyService, productService: ProductService, mediaService: MediaService);
6698
+ setPractitionerService(practitionerService: PractitionerService): void;
6699
+ /**
6700
+ * Filters out procedures that should not be visible to patients:
6701
+ * 1. Procedures with no practitioner (missing practitionerId)
6702
+ * 2. Procedures where practitioner doesn't exist
6703
+ * 3. Procedures from draft practitioners
6704
+ * 4. Procedures from inactive practitioners (isActive === false)
6705
+ *
6706
+ * Note: Each procedure has ONE practitionerId. If that practitioner is inactive/draft/missing,
6707
+ * the procedure is filtered out.
6708
+ *
6709
+ * @param procedures - Array of procedures to filter
6710
+ * @returns Filtered array of procedures (excluding invalid/inactive/draft practitioners)
6711
+ */
6712
+ private filterDraftPractitionerProcedures;
6697
6713
  /**
6698
6714
  * Process media resource (string URL or File object)
6699
6715
  * @param media String URL or File object
@@ -6782,9 +6798,10 @@ declare class ProcedureService extends BaseService {
6782
6798
  * Gets all procedures for a practitioner
6783
6799
  * @param practitionerId - The ID of the practitioner
6784
6800
  * @param clinicBranchId - Optional clinic branch ID to filter by
6801
+ * @param excludeDraftPractitioners - Whether to exclude procedures if the practitioner is in DRAFT status
6785
6802
  * @returns List of procedures
6786
6803
  */
6787
- getProceduresByPractitioner(practitionerId: string, clinicBranchId?: string): Promise<Procedure[]>;
6804
+ getProceduresByPractitioner(practitionerId: string, clinicBranchId?: string, excludeDraftPractitioners?: boolean): Promise<Procedure[]>;
6788
6805
  /**
6789
6806
  * Gets all inactive procedures for a practitioner
6790
6807
  * @param practitionerId - The ID of the practitioner
@@ -6825,9 +6842,10 @@ declare class ProcedureService extends BaseService {
6825
6842
  *
6826
6843
  * @param pagination - Optional number of procedures per page (0 or undefined returns all)
6827
6844
  * @param lastDoc - Optional last document for pagination (if continuing from a previous page)
6845
+ * @param excludeDraftPractitioners - Whether to exclude procedures from draft practitioners (default: true)
6828
6846
  * @returns Object containing procedures array and the last document for pagination
6829
6847
  */
6830
- getAllProcedures(pagination?: number, lastDoc?: any): Promise<{
6848
+ getAllProcedures(pagination?: number, lastDoc?: any, excludeDraftPractitioners?: boolean): Promise<{
6831
6849
  procedures: Procedure[];
6832
6850
  lastDoc: any;
6833
6851
  }>;
@@ -6924,7 +6942,7 @@ declare class PractitionerService extends BaseService {
6924
6942
  private mediaService;
6925
6943
  private procedureService?;
6926
6944
  constructor(db: Firestore, auth: Auth, app: FirebaseApp, clinicService?: ClinicService, procedureService?: ProcedureService);
6927
- private getClinicService;
6945
+ getClinicService(): ClinicService;
6928
6946
  private getProcedureService;
6929
6947
  setClinicService(clinicService: ClinicService): void;
6930
6948
  setProcedureService(procedureService: ProcedureService): void;
@@ -6992,6 +7010,19 @@ declare class PractitionerService extends BaseService {
6992
7010
  * Dohvata zdravstvenog radnika po User ID-u
6993
7011
  */
6994
7012
  getPractitionerByUserRef(userRef: string): Promise<Practitioner | null>;
7013
+ /**
7014
+ * Finds a draft practitioner profile by email address
7015
+ * Used to detect if a draft profile exists when a doctor registers without a token
7016
+ *
7017
+ * @param email - Email address to search for
7018
+ * @returns Draft practitioner profile if found, null otherwise
7019
+ *
7020
+ * @remarks
7021
+ * Requires Firestore composite index on:
7022
+ * - Collection: practitioners
7023
+ * - Fields: basicInfo.email (Ascending), status (Ascending), userRef (Ascending)
7024
+ */
7025
+ findDraftPractitionerByEmail(email: string): Promise<Practitioner | null>;
6995
7026
  /**
6996
7027
  * Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
6997
7028
  */
package/dist/index.js CHANGED
@@ -7319,11 +7319,12 @@ var userSchema = import_zod4.z.object({
7319
7319
 
7320
7320
  // src/errors/auth.errors.ts
7321
7321
  var AuthError = class extends Error {
7322
- constructor(message, code, status = 400) {
7322
+ constructor(message, code, status = 400, metadata) {
7323
7323
  super(message);
7324
7324
  this.code = code;
7325
7325
  this.status = status;
7326
7326
  this.name = "AuthError";
7327
+ this.metadata = metadata;
7327
7328
  }
7328
7329
  };
7329
7330
  var AUTH_ERRORS = {
@@ -7502,6 +7503,12 @@ var AUTH_ERRORS = {
7502
7503
  "Lozinka je previ\u0161e slaba. Molimo koristite ja\u010Du lozinku.",
7503
7504
  "AUTH/WEAK_PASSWORD",
7504
7505
  400
7506
+ ),
7507
+ // Draft profile exists error
7508
+ DRAFT_PROFILE_EXISTS: new AuthError(
7509
+ "A draft practitioner profile exists for this email. Please use your invitation code to claim it, or contact support if you don't have one.",
7510
+ "AUTH/DRAFT_PROFILE_EXISTS",
7511
+ 409
7505
7512
  )
7506
7513
  };
7507
7514
 
@@ -11546,6 +11553,52 @@ var PractitionerService = class extends BaseService {
11546
11553
  }
11547
11554
  return querySnapshot.docs[0].data();
11548
11555
  }
11556
+ /**
11557
+ * Finds a draft practitioner profile by email address
11558
+ * Used to detect if a draft profile exists when a doctor registers without a token
11559
+ *
11560
+ * @param email - Email address to search for
11561
+ * @returns Draft practitioner profile if found, null otherwise
11562
+ *
11563
+ * @remarks
11564
+ * Requires Firestore composite index on:
11565
+ * - Collection: practitioners
11566
+ * - Fields: basicInfo.email (Ascending), status (Ascending), userRef (Ascending)
11567
+ */
11568
+ async findDraftPractitionerByEmail(email) {
11569
+ try {
11570
+ const normalizedEmail = email.toLowerCase().trim();
11571
+ console.log("[PRACTITIONER] Searching for draft practitioner by email", {
11572
+ email: normalizedEmail
11573
+ });
11574
+ const q = (0, import_firestore32.query)(
11575
+ (0, import_firestore32.collection)(this.db, PRACTITIONERS_COLLECTION),
11576
+ (0, import_firestore32.where)("basicInfo.email", "==", normalizedEmail),
11577
+ (0, import_firestore32.where)("status", "==", "draft" /* DRAFT */),
11578
+ (0, import_firestore32.where)("userRef", "==", ""),
11579
+ (0, import_firestore32.limit)(1)
11580
+ );
11581
+ const querySnapshot = await (0, import_firestore32.getDocs)(q);
11582
+ if (querySnapshot.empty) {
11583
+ console.log("[PRACTITIONER] No draft practitioner found for email", {
11584
+ email: normalizedEmail
11585
+ });
11586
+ return null;
11587
+ }
11588
+ const draftPractitioner = querySnapshot.docs[0].data();
11589
+ console.log("[PRACTITIONER] Draft practitioner found", {
11590
+ email: normalizedEmail,
11591
+ practitionerId: draftPractitioner.id
11592
+ });
11593
+ return draftPractitioner;
11594
+ } catch (error) {
11595
+ console.error(
11596
+ "[PRACTITIONER] Error finding draft practitioner by email:",
11597
+ error
11598
+ );
11599
+ return null;
11600
+ }
11601
+ }
11549
11602
  /**
11550
11603
  * Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
11551
11604
  */
@@ -15290,7 +15343,49 @@ var AuthService = class extends BaseService {
15290
15343
  }
15291
15344
  practitioner = claimedPractitioner;
15292
15345
  } else {
15293
- console.log("[AUTH] Creating new practitioner profile");
15346
+ console.log("[AUTH] Checking for existing draft practitioner profile", {
15347
+ email: data.email
15348
+ });
15349
+ const draftPractitioner = await practitionerService.findDraftPractitionerByEmail(
15350
+ data.email
15351
+ );
15352
+ if (draftPractitioner) {
15353
+ console.log("[AUTH] Draft practitioner profile found", {
15354
+ practitionerId: draftPractitioner.id,
15355
+ email: data.email,
15356
+ clinics: draftPractitioner.clinics
15357
+ });
15358
+ let clinicNames = [];
15359
+ if (draftPractitioner.clinicsInfo && draftPractitioner.clinicsInfo.length > 0) {
15360
+ clinicNames = draftPractitioner.clinicsInfo.map((clinic) => clinic.name).filter((name) => !!name);
15361
+ } else if (draftPractitioner.clinics && draftPractitioner.clinics.length > 0) {
15362
+ console.log("[AUTH] clinicsInfo missing, fetching clinic names from clinic IDs");
15363
+ const clinicService = practitionerService.getClinicService();
15364
+ const clinicNamePromises = draftPractitioner.clinics.map(async (clinicId) => {
15365
+ try {
15366
+ const clinic = await clinicService.getClinic(clinicId);
15367
+ return (clinic == null ? void 0 : clinic.name) || null;
15368
+ } catch (error) {
15369
+ console.error(`[AUTH] Error fetching clinic ${clinicId}:`, error);
15370
+ return null;
15371
+ }
15372
+ });
15373
+ const names = await Promise.all(clinicNamePromises);
15374
+ clinicNames = names.filter((name) => !!name);
15375
+ }
15376
+ await cleanupFirebaseUser(firebaseUser);
15377
+ throw new AuthError(
15378
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.message,
15379
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.code,
15380
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.status,
15381
+ {
15382
+ clinicNames,
15383
+ clinics: draftPractitioner.clinics,
15384
+ clinicsInfo: draftPractitioner.clinicsInfo
15385
+ }
15386
+ );
15387
+ }
15388
+ console.log("[AUTH] No draft profile found, creating new practitioner profile");
15294
15389
  const practitionerData = buildPractitionerData(data, firebaseUser.uid);
15295
15390
  practitioner = await practitionerService.createPractitioner(practitionerData);
15296
15391
  }
@@ -19923,6 +20018,91 @@ var ProcedureService = class extends BaseService {
19923
20018
  this.productService = productService;
19924
20019
  this.mediaService = mediaService;
19925
20020
  }
20021
+ setPractitionerService(practitionerService) {
20022
+ this.practitionerService = practitionerService;
20023
+ }
20024
+ /**
20025
+ * Filters out procedures that should not be visible to patients:
20026
+ * 1. Procedures with no practitioner (missing practitionerId)
20027
+ * 2. Procedures where practitioner doesn't exist
20028
+ * 3. Procedures from draft practitioners
20029
+ * 4. Procedures from inactive practitioners (isActive === false)
20030
+ *
20031
+ * Note: Each procedure has ONE practitionerId. If that practitioner is inactive/draft/missing,
20032
+ * the procedure is filtered out.
20033
+ *
20034
+ * @param procedures - Array of procedures to filter
20035
+ * @returns Filtered array of procedures (excluding invalid/inactive/draft practitioners)
20036
+ */
20037
+ async filterDraftPractitionerProcedures(procedures) {
20038
+ if (!this.practitionerService || procedures.length === 0) {
20039
+ return procedures;
20040
+ }
20041
+ try {
20042
+ const proceduresWithPractitioner = procedures.filter(
20043
+ (p) => p.practitionerId && p.practitionerId.trim() !== ""
20044
+ );
20045
+ if (proceduresWithPractitioner.length === 0) {
20046
+ console.log(
20047
+ `[ProcedureService] All ${procedures.length} procedures have no practitionerId - filtering out`
20048
+ );
20049
+ return [];
20050
+ }
20051
+ const practitionerIds = Array.from(
20052
+ new Set(proceduresWithPractitioner.map((p) => p.practitionerId).filter(Boolean))
20053
+ );
20054
+ if (practitionerIds.length === 0) {
20055
+ return [];
20056
+ }
20057
+ const practitionerPromises = practitionerIds.map(
20058
+ (id) => this.practitionerService.getPractitioner(id).catch(() => null)
20059
+ );
20060
+ const practitioners = await Promise.all(practitionerPromises);
20061
+ const practitionerMap = /* @__PURE__ */ new Map();
20062
+ practitioners.forEach((practitioner, index) => {
20063
+ if (practitioner) {
20064
+ practitionerMap.set(practitionerIds[index], practitioner);
20065
+ }
20066
+ });
20067
+ const filteredProcedures = proceduresWithPractitioner.filter((procedure) => {
20068
+ const practitioner = practitionerMap.get(procedure.practitionerId);
20069
+ if (!practitioner) {
20070
+ console.log(
20071
+ `[ProcedureService] Filtering out procedure ${procedure.id} - practitioner ${procedure.practitionerId} not found`
20072
+ );
20073
+ return false;
20074
+ }
20075
+ if (practitioner.status === "draft" /* DRAFT */) {
20076
+ console.log(
20077
+ `[ProcedureService] Filtering out procedure ${procedure.id} - practitioner ${procedure.practitionerId} is DRAFT`
20078
+ );
20079
+ return false;
20080
+ }
20081
+ if (!practitioner.isActive) {
20082
+ console.log(
20083
+ `[ProcedureService] Filtering out procedure ${procedure.id} - practitioner ${procedure.practitionerId} is not active`
20084
+ );
20085
+ return false;
20086
+ }
20087
+ return true;
20088
+ });
20089
+ const filteredCount = procedures.length - filteredProcedures.length;
20090
+ if (filteredCount > 0) {
20091
+ const noPractitionerCount = procedures.length - proceduresWithPractitioner.length;
20092
+ const invalidPractitionerCount = proceduresWithPractitioner.length - filteredProcedures.length;
20093
+ console.log(
20094
+ `[ProcedureService] Filtered out ${filteredCount} procedures: ${noPractitionerCount} with no practitionerId, ${invalidPractitionerCount} with missing/draft/inactive practitioners`
20095
+ );
20096
+ }
20097
+ return filteredProcedures;
20098
+ } catch (error) {
20099
+ console.error(
20100
+ "[ProcedureService] Error filtering practitioner procedures:",
20101
+ error
20102
+ );
20103
+ return procedures;
20104
+ }
20105
+ }
19926
20106
  /**
19927
20107
  * Process media resource (string URL or File object)
19928
20108
  * @param media String URL or File object
@@ -20627,15 +20807,17 @@ var ProcedureService = class extends BaseService {
20627
20807
  (0, import_firestore58.where)("isActive", "==", true)
20628
20808
  );
20629
20809
  const snapshot = await (0, import_firestore58.getDocs)(q);
20630
- return snapshot.docs.map((doc47) => doc47.data());
20810
+ const procedures = snapshot.docs.map((doc47) => doc47.data());
20811
+ return await this.filterDraftPractitionerProcedures(procedures);
20631
20812
  }
20632
20813
  /**
20633
20814
  * Gets all procedures for a practitioner
20634
20815
  * @param practitionerId - The ID of the practitioner
20635
20816
  * @param clinicBranchId - Optional clinic branch ID to filter by
20817
+ * @param excludeDraftPractitioners - Whether to exclude procedures if the practitioner is in DRAFT status
20636
20818
  * @returns List of procedures
20637
20819
  */
20638
- async getProceduresByPractitioner(practitionerId, clinicBranchId) {
20820
+ async getProceduresByPractitioner(practitionerId, clinicBranchId, excludeDraftPractitioners = true) {
20639
20821
  const constraints = [
20640
20822
  (0, import_firestore58.where)("practitionerId", "==", practitionerId),
20641
20823
  (0, import_firestore58.where)("isActive", "==", true)
@@ -20648,7 +20830,19 @@ var ProcedureService = class extends BaseService {
20648
20830
  ...constraints
20649
20831
  );
20650
20832
  const snapshot = await (0, import_firestore58.getDocs)(q);
20651
- return snapshot.docs.map((doc47) => doc47.data());
20833
+ const procedures = snapshot.docs.map((doc47) => doc47.data());
20834
+ if (excludeDraftPractitioners && this.practitionerService) {
20835
+ try {
20836
+ const practitioner = await this.practitionerService.getPractitioner(practitionerId);
20837
+ if (practitioner && practitioner.status === "draft" /* DRAFT */) {
20838
+ console.log(`[ProcedureService] Excluding procedures for draft practitioner ${practitionerId}`);
20839
+ return [];
20840
+ }
20841
+ } catch (error) {
20842
+ console.error(`[ProcedureService] Error checking practitioner status for ${practitionerId}:`, error);
20843
+ }
20844
+ }
20845
+ return procedures;
20652
20846
  }
20653
20847
  /**
20654
20848
  * Gets all inactive procedures for a practitioner
@@ -20857,9 +21051,10 @@ var ProcedureService = class extends BaseService {
20857
21051
  *
20858
21052
  * @param pagination - Optional number of procedures per page (0 or undefined returns all)
20859
21053
  * @param lastDoc - Optional last document for pagination (if continuing from a previous page)
21054
+ * @param excludeDraftPractitioners - Whether to exclude procedures from draft practitioners (default: true)
20860
21055
  * @returns Object containing procedures array and the last document for pagination
20861
21056
  */
20862
- async getAllProcedures(pagination, lastDoc) {
21057
+ async getAllProcedures(pagination, lastDoc, excludeDraftPractitioners = true) {
20863
21058
  try {
20864
21059
  const proceduresCollection = (0, import_firestore58.collection)(this.db, PROCEDURES_COLLECTION);
20865
21060
  let proceduresQuery = (0, import_firestore58.query)(proceduresCollection);
@@ -20881,7 +21076,7 @@ var ProcedureService = class extends BaseService {
20881
21076
  }
20882
21077
  const proceduresSnapshot = await (0, import_firestore58.getDocs)(proceduresQuery);
20883
21078
  const lastVisible = proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
20884
- const procedures = proceduresSnapshot.docs.map((doc47) => {
21079
+ let procedures = proceduresSnapshot.docs.map((doc47) => {
20885
21080
  const data = doc47.data();
20886
21081
  return {
20887
21082
  ...data,
@@ -20889,6 +21084,9 @@ var ProcedureService = class extends BaseService {
20889
21084
  // Ensure ID is present
20890
21085
  };
20891
21086
  });
21087
+ if (excludeDraftPractitioners) {
21088
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
21089
+ }
20892
21090
  return {
20893
21091
  procedures,
20894
21092
  lastDoc: lastVisible
@@ -21013,6 +21211,7 @@ var ProcedureService = class extends BaseService {
21013
21211
  if (hasNestedFilters) {
21014
21212
  procedures = this.applyInMemoryFilters(procedures, filters);
21015
21213
  }
21214
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
21016
21215
  const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
21017
21216
  console.log(`[PROCEDURE_SERVICE] Strategy 1 success: ${procedures.length} procedures`);
21018
21217
  if (procedures.length < (filters.pagination || 10)) {
@@ -21053,6 +21252,7 @@ var ProcedureService = class extends BaseService {
21053
21252
  if (hasNestedFilters) {
21054
21253
  procedures = this.applyInMemoryFilters(procedures, filters);
21055
21254
  }
21255
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
21056
21256
  const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
21057
21257
  console.log(`[PROCEDURE_SERVICE] Strategy 2 success: ${procedures.length} procedures`);
21058
21258
  if (procedures.length < (filters.pagination || 10)) {
@@ -21133,6 +21333,7 @@ var ProcedureService = class extends BaseService {
21133
21333
  }
21134
21334
  });
21135
21335
  procedures = this.applyInMemoryFilters(procedures, filters);
21336
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
21136
21337
  console.log("[PROCEDURE_SERVICE] After applyInMemoryFilters (Strategy 3):", {
21137
21338
  procedureCount: procedures.length
21138
21339
  });
@@ -21164,6 +21365,7 @@ var ProcedureService = class extends BaseService {
21164
21365
  (doc47) => ({ ...doc47.data(), id: doc47.id })
21165
21366
  );
21166
21367
  procedures = this.applyInMemoryFilters(procedures, filters);
21368
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
21167
21369
  const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
21168
21370
  console.log(`[PROCEDURE_SERVICE] Strategy 4 success: ${procedures.length} procedures`);
21169
21371
  if (procedures.length < (filters.pagination || 10)) {
package/dist/index.mjs CHANGED
@@ -7225,11 +7225,12 @@ var userSchema = z4.object({
7225
7225
 
7226
7226
  // src/errors/auth.errors.ts
7227
7227
  var AuthError = class extends Error {
7228
- constructor(message, code, status = 400) {
7228
+ constructor(message, code, status = 400, metadata) {
7229
7229
  super(message);
7230
7230
  this.code = code;
7231
7231
  this.status = status;
7232
7232
  this.name = "AuthError";
7233
+ this.metadata = metadata;
7233
7234
  }
7234
7235
  };
7235
7236
  var AUTH_ERRORS = {
@@ -7408,6 +7409,12 @@ var AUTH_ERRORS = {
7408
7409
  "Lozinka je previ\u0161e slaba. Molimo koristite ja\u010Du lozinku.",
7409
7410
  "AUTH/WEAK_PASSWORD",
7410
7411
  400
7412
+ ),
7413
+ // Draft profile exists error
7414
+ DRAFT_PROFILE_EXISTS: new AuthError(
7415
+ "A draft practitioner profile exists for this email. Please use your invitation code to claim it, or contact support if you don't have one.",
7416
+ "AUTH/DRAFT_PROFILE_EXISTS",
7417
+ 409
7411
7418
  )
7412
7419
  };
7413
7420
 
@@ -11569,6 +11576,52 @@ var PractitionerService = class extends BaseService {
11569
11576
  }
11570
11577
  return querySnapshot.docs[0].data();
11571
11578
  }
11579
+ /**
11580
+ * Finds a draft practitioner profile by email address
11581
+ * Used to detect if a draft profile exists when a doctor registers without a token
11582
+ *
11583
+ * @param email - Email address to search for
11584
+ * @returns Draft practitioner profile if found, null otherwise
11585
+ *
11586
+ * @remarks
11587
+ * Requires Firestore composite index on:
11588
+ * - Collection: practitioners
11589
+ * - Fields: basicInfo.email (Ascending), status (Ascending), userRef (Ascending)
11590
+ */
11591
+ async findDraftPractitionerByEmail(email) {
11592
+ try {
11593
+ const normalizedEmail = email.toLowerCase().trim();
11594
+ console.log("[PRACTITIONER] Searching for draft practitioner by email", {
11595
+ email: normalizedEmail
11596
+ });
11597
+ const q = query13(
11598
+ collection13(this.db, PRACTITIONERS_COLLECTION),
11599
+ where13("basicInfo.email", "==", normalizedEmail),
11600
+ where13("status", "==", "draft" /* DRAFT */),
11601
+ where13("userRef", "==", ""),
11602
+ limit7(1)
11603
+ );
11604
+ const querySnapshot = await getDocs13(q);
11605
+ if (querySnapshot.empty) {
11606
+ console.log("[PRACTITIONER] No draft practitioner found for email", {
11607
+ email: normalizedEmail
11608
+ });
11609
+ return null;
11610
+ }
11611
+ const draftPractitioner = querySnapshot.docs[0].data();
11612
+ console.log("[PRACTITIONER] Draft practitioner found", {
11613
+ email: normalizedEmail,
11614
+ practitionerId: draftPractitioner.id
11615
+ });
11616
+ return draftPractitioner;
11617
+ } catch (error) {
11618
+ console.error(
11619
+ "[PRACTITIONER] Error finding draft practitioner by email:",
11620
+ error
11621
+ );
11622
+ return null;
11623
+ }
11624
+ }
11572
11625
  /**
11573
11626
  * Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
11574
11627
  */
@@ -15377,7 +15430,49 @@ var AuthService = class extends BaseService {
15377
15430
  }
15378
15431
  practitioner = claimedPractitioner;
15379
15432
  } else {
15380
- console.log("[AUTH] Creating new practitioner profile");
15433
+ console.log("[AUTH] Checking for existing draft practitioner profile", {
15434
+ email: data.email
15435
+ });
15436
+ const draftPractitioner = await practitionerService.findDraftPractitionerByEmail(
15437
+ data.email
15438
+ );
15439
+ if (draftPractitioner) {
15440
+ console.log("[AUTH] Draft practitioner profile found", {
15441
+ practitionerId: draftPractitioner.id,
15442
+ email: data.email,
15443
+ clinics: draftPractitioner.clinics
15444
+ });
15445
+ let clinicNames = [];
15446
+ if (draftPractitioner.clinicsInfo && draftPractitioner.clinicsInfo.length > 0) {
15447
+ clinicNames = draftPractitioner.clinicsInfo.map((clinic) => clinic.name).filter((name) => !!name);
15448
+ } else if (draftPractitioner.clinics && draftPractitioner.clinics.length > 0) {
15449
+ console.log("[AUTH] clinicsInfo missing, fetching clinic names from clinic IDs");
15450
+ const clinicService = practitionerService.getClinicService();
15451
+ const clinicNamePromises = draftPractitioner.clinics.map(async (clinicId) => {
15452
+ try {
15453
+ const clinic = await clinicService.getClinic(clinicId);
15454
+ return (clinic == null ? void 0 : clinic.name) || null;
15455
+ } catch (error) {
15456
+ console.error(`[AUTH] Error fetching clinic ${clinicId}:`, error);
15457
+ return null;
15458
+ }
15459
+ });
15460
+ const names = await Promise.all(clinicNamePromises);
15461
+ clinicNames = names.filter((name) => !!name);
15462
+ }
15463
+ await cleanupFirebaseUser(firebaseUser);
15464
+ throw new AuthError(
15465
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.message,
15466
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.code,
15467
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.status,
15468
+ {
15469
+ clinicNames,
15470
+ clinics: draftPractitioner.clinics,
15471
+ clinicsInfo: draftPractitioner.clinicsInfo
15472
+ }
15473
+ );
15474
+ }
15475
+ console.log("[AUTH] No draft profile found, creating new practitioner profile");
15381
15476
  const practitionerData = buildPractitionerData(data, firebaseUser.uid);
15382
15477
  practitioner = await practitionerService.createPractitioner(practitionerData);
15383
15478
  }
@@ -20159,6 +20254,91 @@ var ProcedureService = class extends BaseService {
20159
20254
  this.productService = productService;
20160
20255
  this.mediaService = mediaService;
20161
20256
  }
20257
+ setPractitionerService(practitionerService) {
20258
+ this.practitionerService = practitionerService;
20259
+ }
20260
+ /**
20261
+ * Filters out procedures that should not be visible to patients:
20262
+ * 1. Procedures with no practitioner (missing practitionerId)
20263
+ * 2. Procedures where practitioner doesn't exist
20264
+ * 3. Procedures from draft practitioners
20265
+ * 4. Procedures from inactive practitioners (isActive === false)
20266
+ *
20267
+ * Note: Each procedure has ONE practitionerId. If that practitioner is inactive/draft/missing,
20268
+ * the procedure is filtered out.
20269
+ *
20270
+ * @param procedures - Array of procedures to filter
20271
+ * @returns Filtered array of procedures (excluding invalid/inactive/draft practitioners)
20272
+ */
20273
+ async filterDraftPractitionerProcedures(procedures) {
20274
+ if (!this.practitionerService || procedures.length === 0) {
20275
+ return procedures;
20276
+ }
20277
+ try {
20278
+ const proceduresWithPractitioner = procedures.filter(
20279
+ (p) => p.practitionerId && p.practitionerId.trim() !== ""
20280
+ );
20281
+ if (proceduresWithPractitioner.length === 0) {
20282
+ console.log(
20283
+ `[ProcedureService] All ${procedures.length} procedures have no practitionerId - filtering out`
20284
+ );
20285
+ return [];
20286
+ }
20287
+ const practitionerIds = Array.from(
20288
+ new Set(proceduresWithPractitioner.map((p) => p.practitionerId).filter(Boolean))
20289
+ );
20290
+ if (practitionerIds.length === 0) {
20291
+ return [];
20292
+ }
20293
+ const practitionerPromises = practitionerIds.map(
20294
+ (id) => this.practitionerService.getPractitioner(id).catch(() => null)
20295
+ );
20296
+ const practitioners = await Promise.all(practitionerPromises);
20297
+ const practitionerMap = /* @__PURE__ */ new Map();
20298
+ practitioners.forEach((practitioner, index) => {
20299
+ if (practitioner) {
20300
+ practitionerMap.set(practitionerIds[index], practitioner);
20301
+ }
20302
+ });
20303
+ const filteredProcedures = proceduresWithPractitioner.filter((procedure) => {
20304
+ const practitioner = practitionerMap.get(procedure.practitionerId);
20305
+ if (!practitioner) {
20306
+ console.log(
20307
+ `[ProcedureService] Filtering out procedure ${procedure.id} - practitioner ${procedure.practitionerId} not found`
20308
+ );
20309
+ return false;
20310
+ }
20311
+ if (practitioner.status === "draft" /* DRAFT */) {
20312
+ console.log(
20313
+ `[ProcedureService] Filtering out procedure ${procedure.id} - practitioner ${procedure.practitionerId} is DRAFT`
20314
+ );
20315
+ return false;
20316
+ }
20317
+ if (!practitioner.isActive) {
20318
+ console.log(
20319
+ `[ProcedureService] Filtering out procedure ${procedure.id} - practitioner ${procedure.practitionerId} is not active`
20320
+ );
20321
+ return false;
20322
+ }
20323
+ return true;
20324
+ });
20325
+ const filteredCount = procedures.length - filteredProcedures.length;
20326
+ if (filteredCount > 0) {
20327
+ const noPractitionerCount = procedures.length - proceduresWithPractitioner.length;
20328
+ const invalidPractitionerCount = proceduresWithPractitioner.length - filteredProcedures.length;
20329
+ console.log(
20330
+ `[ProcedureService] Filtered out ${filteredCount} procedures: ${noPractitionerCount} with no practitionerId, ${invalidPractitionerCount} with missing/draft/inactive practitioners`
20331
+ );
20332
+ }
20333
+ return filteredProcedures;
20334
+ } catch (error) {
20335
+ console.error(
20336
+ "[ProcedureService] Error filtering practitioner procedures:",
20337
+ error
20338
+ );
20339
+ return procedures;
20340
+ }
20341
+ }
20162
20342
  /**
20163
20343
  * Process media resource (string URL or File object)
20164
20344
  * @param media String URL or File object
@@ -20863,15 +21043,17 @@ var ProcedureService = class extends BaseService {
20863
21043
  where33("isActive", "==", true)
20864
21044
  );
20865
21045
  const snapshot = await getDocs33(q);
20866
- return snapshot.docs.map((doc47) => doc47.data());
21046
+ const procedures = snapshot.docs.map((doc47) => doc47.data());
21047
+ return await this.filterDraftPractitionerProcedures(procedures);
20867
21048
  }
20868
21049
  /**
20869
21050
  * Gets all procedures for a practitioner
20870
21051
  * @param practitionerId - The ID of the practitioner
20871
21052
  * @param clinicBranchId - Optional clinic branch ID to filter by
21053
+ * @param excludeDraftPractitioners - Whether to exclude procedures if the practitioner is in DRAFT status
20872
21054
  * @returns List of procedures
20873
21055
  */
20874
- async getProceduresByPractitioner(practitionerId, clinicBranchId) {
21056
+ async getProceduresByPractitioner(practitionerId, clinicBranchId, excludeDraftPractitioners = true) {
20875
21057
  const constraints = [
20876
21058
  where33("practitionerId", "==", practitionerId),
20877
21059
  where33("isActive", "==", true)
@@ -20884,7 +21066,19 @@ var ProcedureService = class extends BaseService {
20884
21066
  ...constraints
20885
21067
  );
20886
21068
  const snapshot = await getDocs33(q);
20887
- return snapshot.docs.map((doc47) => doc47.data());
21069
+ const procedures = snapshot.docs.map((doc47) => doc47.data());
21070
+ if (excludeDraftPractitioners && this.practitionerService) {
21071
+ try {
21072
+ const practitioner = await this.practitionerService.getPractitioner(practitionerId);
21073
+ if (practitioner && practitioner.status === "draft" /* DRAFT */) {
21074
+ console.log(`[ProcedureService] Excluding procedures for draft practitioner ${practitionerId}`);
21075
+ return [];
21076
+ }
21077
+ } catch (error) {
21078
+ console.error(`[ProcedureService] Error checking practitioner status for ${practitionerId}:`, error);
21079
+ }
21080
+ }
21081
+ return procedures;
20888
21082
  }
20889
21083
  /**
20890
21084
  * Gets all inactive procedures for a practitioner
@@ -21093,9 +21287,10 @@ var ProcedureService = class extends BaseService {
21093
21287
  *
21094
21288
  * @param pagination - Optional number of procedures per page (0 or undefined returns all)
21095
21289
  * @param lastDoc - Optional last document for pagination (if continuing from a previous page)
21290
+ * @param excludeDraftPractitioners - Whether to exclude procedures from draft practitioners (default: true)
21096
21291
  * @returns Object containing procedures array and the last document for pagination
21097
21292
  */
21098
- async getAllProcedures(pagination, lastDoc) {
21293
+ async getAllProcedures(pagination, lastDoc, excludeDraftPractitioners = true) {
21099
21294
  try {
21100
21295
  const proceduresCollection = collection33(this.db, PROCEDURES_COLLECTION);
21101
21296
  let proceduresQuery = query33(proceduresCollection);
@@ -21117,7 +21312,7 @@ var ProcedureService = class extends BaseService {
21117
21312
  }
21118
21313
  const proceduresSnapshot = await getDocs33(proceduresQuery);
21119
21314
  const lastVisible = proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
21120
- const procedures = proceduresSnapshot.docs.map((doc47) => {
21315
+ let procedures = proceduresSnapshot.docs.map((doc47) => {
21121
21316
  const data = doc47.data();
21122
21317
  return {
21123
21318
  ...data,
@@ -21125,6 +21320,9 @@ var ProcedureService = class extends BaseService {
21125
21320
  // Ensure ID is present
21126
21321
  };
21127
21322
  });
21323
+ if (excludeDraftPractitioners) {
21324
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
21325
+ }
21128
21326
  return {
21129
21327
  procedures,
21130
21328
  lastDoc: lastVisible
@@ -21249,6 +21447,7 @@ var ProcedureService = class extends BaseService {
21249
21447
  if (hasNestedFilters) {
21250
21448
  procedures = this.applyInMemoryFilters(procedures, filters);
21251
21449
  }
21450
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
21252
21451
  const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
21253
21452
  console.log(`[PROCEDURE_SERVICE] Strategy 1 success: ${procedures.length} procedures`);
21254
21453
  if (procedures.length < (filters.pagination || 10)) {
@@ -21289,6 +21488,7 @@ var ProcedureService = class extends BaseService {
21289
21488
  if (hasNestedFilters) {
21290
21489
  procedures = this.applyInMemoryFilters(procedures, filters);
21291
21490
  }
21491
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
21292
21492
  const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
21293
21493
  console.log(`[PROCEDURE_SERVICE] Strategy 2 success: ${procedures.length} procedures`);
21294
21494
  if (procedures.length < (filters.pagination || 10)) {
@@ -21369,6 +21569,7 @@ var ProcedureService = class extends BaseService {
21369
21569
  }
21370
21570
  });
21371
21571
  procedures = this.applyInMemoryFilters(procedures, filters);
21572
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
21372
21573
  console.log("[PROCEDURE_SERVICE] After applyInMemoryFilters (Strategy 3):", {
21373
21574
  procedureCount: procedures.length
21374
21575
  });
@@ -21400,6 +21601,7 @@ var ProcedureService = class extends BaseService {
21400
21601
  (doc47) => ({ ...doc47.data(), id: doc47.id })
21401
21602
  );
21402
21603
  procedures = this.applyInMemoryFilters(procedures, filters);
21604
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
21403
21605
  const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
21404
21606
  console.log(`[PROCEDURE_SERVICE] Strategy 4 success: ${procedures.length} procedures`);
21405
21607
  if (procedures.length < (filters.pagination || 10)) {
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.6",
4
+ "version": "1.13.10",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -1,11 +1,15 @@
1
1
  export class AuthError extends Error {
2
+ public metadata?: Record<string, any>;
3
+
2
4
  constructor(
3
5
  message: string,
4
6
  public code: string,
5
- public status: number = 400
7
+ public status: number = 400,
8
+ metadata?: Record<string, any>
6
9
  ) {
7
10
  super(message);
8
11
  this.name = "AuthError";
12
+ this.metadata = metadata;
9
13
  }
10
14
  }
11
15
 
@@ -197,4 +201,11 @@ export const AUTH_ERRORS = {
197
201
  "AUTH/WEAK_PASSWORD",
198
202
  400
199
203
  ),
204
+
205
+ // Draft profile exists error
206
+ DRAFT_PROFILE_EXISTS: new AuthError(
207
+ "A draft practitioner profile exists for this email. Please use your invitation code to claim it, or contact support if you don't have one.",
208
+ "AUTH/DRAFT_PROFILE_EXISTS",
209
+ 409
210
+ ),
200
211
  } as const;
@@ -724,7 +724,61 @@ export class AuthService extends BaseService {
724
724
  }
725
725
  practitioner = claimedPractitioner;
726
726
  } else {
727
- console.log('[AUTH] Creating new practitioner profile');
727
+ // Check if a draft profile exists for this email
728
+ console.log('[AUTH] Checking for existing draft practitioner profile', {
729
+ email: data.email,
730
+ });
731
+ const draftPractitioner = await practitionerService.findDraftPractitionerByEmail(
732
+ data.email
733
+ );
734
+
735
+ if (draftPractitioner) {
736
+ console.log('[AUTH] Draft practitioner profile found', {
737
+ practitionerId: draftPractitioner.id,
738
+ email: data.email,
739
+ clinics: draftPractitioner.clinics,
740
+ });
741
+
742
+ // Extract clinic names from clinicsInfo (should be populated when draft is created)
743
+ let clinicNames: string[] = [];
744
+ if (draftPractitioner.clinicsInfo && draftPractitioner.clinicsInfo.length > 0) {
745
+ clinicNames = draftPractitioner.clinicsInfo
746
+ .map((clinic) => clinic.name)
747
+ .filter((name): name is string => !!name);
748
+ } else if (draftPractitioner.clinics && draftPractitioner.clinics.length > 0) {
749
+ // Fallback: fetch clinic names if clinicsInfo is missing
750
+ console.log('[AUTH] clinicsInfo missing, fetching clinic names from clinic IDs');
751
+ const clinicService = practitionerService.getClinicService();
752
+ const clinicNamePromises = draftPractitioner.clinics.map(async (clinicId) => {
753
+ try {
754
+ const clinic = await clinicService.getClinic(clinicId);
755
+ return clinic?.name || null;
756
+ } catch (error) {
757
+ console.error(`[AUTH] Error fetching clinic ${clinicId}:`, error);
758
+ return null;
759
+ }
760
+ });
761
+ const names = await Promise.all(clinicNamePromises);
762
+ clinicNames = names.filter((name): name is string => !!name);
763
+ }
764
+
765
+ // Cleanup Firebase user since we're not creating a profile
766
+ await cleanupFirebaseUser(firebaseUser);
767
+
768
+ // Throw error with clinic information
769
+ throw new AuthError(
770
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.message,
771
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.code,
772
+ AUTH_ERRORS.DRAFT_PROFILE_EXISTS.status,
773
+ {
774
+ clinicNames,
775
+ clinics: draftPractitioner.clinics,
776
+ clinicsInfo: draftPractitioner.clinicsInfo,
777
+ }
778
+ );
779
+ }
780
+
781
+ console.log('[AUTH] No draft profile found, creating new practitioner profile');
728
782
  const practitionerData = buildPractitionerData(data, firebaseUser.uid);
729
783
  practitioner = await practitionerService.createPractitioner(practitionerData);
730
784
  }
@@ -81,7 +81,7 @@ export class PractitionerService extends BaseService {
81
81
  this.mediaService = new MediaService(db, auth, app);
82
82
  }
83
83
 
84
- private getClinicService(): ClinicService {
84
+ public getClinicService(): ClinicService {
85
85
  if (!this.clinicService) {
86
86
  throw new Error("Clinic service not initialized!");
87
87
  }
@@ -657,6 +657,63 @@ export class PractitionerService extends BaseService {
657
657
  return querySnapshot.docs[0].data() as Practitioner;
658
658
  }
659
659
 
660
+ /**
661
+ * Finds a draft practitioner profile by email address
662
+ * Used to detect if a draft profile exists when a doctor registers without a token
663
+ *
664
+ * @param email - Email address to search for
665
+ * @returns Draft practitioner profile if found, null otherwise
666
+ *
667
+ * @remarks
668
+ * Requires Firestore composite index on:
669
+ * - Collection: practitioners
670
+ * - Fields: basicInfo.email (Ascending), status (Ascending), userRef (Ascending)
671
+ */
672
+ async findDraftPractitionerByEmail(
673
+ email: string
674
+ ): Promise<Practitioner | null> {
675
+ try {
676
+ const normalizedEmail = email.toLowerCase().trim();
677
+
678
+ console.log("[PRACTITIONER] Searching for draft practitioner by email", {
679
+ email: normalizedEmail,
680
+ });
681
+
682
+ const q = query(
683
+ collection(this.db, PRACTITIONERS_COLLECTION),
684
+ where("basicInfo.email", "==", normalizedEmail),
685
+ where("status", "==", PractitionerStatus.DRAFT),
686
+ where("userRef", "==", ""),
687
+ limit(1)
688
+ );
689
+
690
+ const querySnapshot = await getDocs(q);
691
+
692
+ if (querySnapshot.empty) {
693
+ console.log("[PRACTITIONER] No draft practitioner found for email", {
694
+ email: normalizedEmail,
695
+ });
696
+ return null;
697
+ }
698
+
699
+ const draftPractitioner = querySnapshot.docs[0].data() as Practitioner;
700
+ console.log("[PRACTITIONER] Draft practitioner found", {
701
+ email: normalizedEmail,
702
+ practitionerId: draftPractitioner.id,
703
+ });
704
+
705
+ return draftPractitioner;
706
+ } catch (error) {
707
+ console.error(
708
+ "[PRACTITIONER] Error finding draft practitioner by email:",
709
+ error
710
+ );
711
+ // If query fails (e.g., index not created), return null to allow registration
712
+ // This prevents blocking registration if index is missing
713
+ return null;
714
+ }
715
+ }
716
+
660
717
  /**
661
718
  * Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
662
719
  */
@@ -56,12 +56,16 @@ import { distanceBetween, geohashQueryBounds } from 'geofire-common';
56
56
  import { MediaService, MediaAccessLevel } from '../media/media.service';
57
57
  import type { ProcedureProduct } from '../../backoffice/types/procedure-product.types';
58
58
 
59
+ import { PractitionerService } from '../practitioner/practitioner.service';
60
+ import { PractitionerStatus } from '../../types/practitioner';
61
+
59
62
  export class ProcedureService extends BaseService {
60
63
  private categoryService: CategoryService;
61
64
  private subcategoryService: SubcategoryService;
62
65
  private technologyService: TechnologyService;
63
66
  private productService: ProductService;
64
67
  private mediaService: MediaService;
68
+ private practitionerService?: PractitionerService;
65
69
 
66
70
  constructor(
67
71
  db: Firestore,
@@ -81,6 +85,123 @@ export class ProcedureService extends BaseService {
81
85
  this.mediaService = mediaService;
82
86
  }
83
87
 
88
+ setPractitionerService(practitionerService: PractitionerService) {
89
+ this.practitionerService = practitionerService;
90
+ }
91
+
92
+ /**
93
+ * Filters out procedures that should not be visible to patients:
94
+ * 1. Procedures with no practitioner (missing practitionerId)
95
+ * 2. Procedures where practitioner doesn't exist
96
+ * 3. Procedures from draft practitioners
97
+ * 4. Procedures from inactive practitioners (isActive === false)
98
+ *
99
+ * Note: Each procedure has ONE practitionerId. If that practitioner is inactive/draft/missing,
100
+ * the procedure is filtered out.
101
+ *
102
+ * @param procedures - Array of procedures to filter
103
+ * @returns Filtered array of procedures (excluding invalid/inactive/draft practitioners)
104
+ */
105
+ private async filterDraftPractitionerProcedures(
106
+ procedures: Procedure[]
107
+ ): Promise<Procedure[]> {
108
+ if (!this.practitionerService || procedures.length === 0) {
109
+ return procedures;
110
+ }
111
+
112
+ try {
113
+ // First, filter out procedures with no practitionerId
114
+ const proceduresWithPractitioner = procedures.filter((p) =>
115
+ p.practitionerId && p.practitionerId.trim() !== ''
116
+ );
117
+
118
+ if (proceduresWithPractitioner.length === 0) {
119
+ console.log(
120
+ `[ProcedureService] All ${procedures.length} procedures have no practitionerId - filtering out`
121
+ );
122
+ return [];
123
+ }
124
+
125
+ // Get unique practitioner IDs from procedures
126
+ const practitionerIds = Array.from(
127
+ new Set(proceduresWithPractitioner.map((p) => p.practitionerId).filter(Boolean))
128
+ );
129
+
130
+ if (practitionerIds.length === 0) {
131
+ return [];
132
+ }
133
+
134
+ // Fetch all practitioners in parallel
135
+ const practitionerPromises = practitionerIds.map((id) =>
136
+ this.practitionerService!.getPractitioner(id).catch(() => null)
137
+ );
138
+ const practitioners = await Promise.all(practitionerPromises);
139
+
140
+ // Create a map of practitioner ID to practitioner data
141
+ const practitionerMap = new Map<string, Practitioner>();
142
+ practitioners.forEach((practitioner, index) => {
143
+ if (practitioner) {
144
+ practitionerMap.set(practitionerIds[index], practitioner);
145
+ }
146
+ });
147
+
148
+ // Filter out procedures that:
149
+ // 1. Have no practitionerId (already filtered above, but double-check)
150
+ // 2. Have a practitioner that doesn't exist
151
+ // 3. Have a practitioner with DRAFT status
152
+ // 4. Have a practitioner that is not active (isActive === false)
153
+ const filteredProcedures = proceduresWithPractitioner.filter((procedure) => {
154
+ // Check if practitioner exists
155
+ const practitioner = practitionerMap.get(procedure.practitionerId);
156
+
157
+ if (!practitioner) {
158
+ console.log(
159
+ `[ProcedureService] Filtering out procedure ${procedure.id} - practitioner ${procedure.practitionerId} not found`
160
+ );
161
+ return false;
162
+ }
163
+
164
+ // Check if practitioner is DRAFT
165
+ if (practitioner.status === PractitionerStatus.DRAFT) {
166
+ console.log(
167
+ `[ProcedureService] Filtering out procedure ${procedure.id} - practitioner ${procedure.practitionerId} is DRAFT`
168
+ );
169
+ return false;
170
+ }
171
+
172
+ // Check if practitioner is active (must be true to show procedure)
173
+ if (!practitioner.isActive) {
174
+ console.log(
175
+ `[ProcedureService] Filtering out procedure ${procedure.id} - practitioner ${procedure.practitionerId} is not active`
176
+ );
177
+ return false;
178
+ }
179
+
180
+ return true;
181
+ });
182
+
183
+ const filteredCount = procedures.length - filteredProcedures.length;
184
+ if (filteredCount > 0) {
185
+ const noPractitionerCount = procedures.length - proceduresWithPractitioner.length;
186
+ const invalidPractitionerCount = proceduresWithPractitioner.length - filteredProcedures.length;
187
+ console.log(
188
+ `[ProcedureService] Filtered out ${filteredCount} procedures: ` +
189
+ `${noPractitionerCount} with no practitionerId, ` +
190
+ `${invalidPractitionerCount} with missing/draft/inactive practitioners`
191
+ );
192
+ }
193
+
194
+ return filteredProcedures;
195
+ } catch (error) {
196
+ console.error(
197
+ '[ProcedureService] Error filtering practitioner procedures:',
198
+ error
199
+ );
200
+ // On error, return original procedures to avoid breaking functionality
201
+ return procedures;
202
+ }
203
+ }
204
+
84
205
  /**
85
206
  * Process media resource (string URL or File object)
86
207
  * @param media String URL or File object
@@ -993,16 +1114,24 @@ export class ProcedureService extends BaseService {
993
1114
  where('isActive', '==', true),
994
1115
  );
995
1116
  const snapshot = await getDocs(q);
996
- return snapshot.docs.map(doc => doc.data() as Procedure);
1117
+ const procedures = snapshot.docs.map(doc => doc.data() as Procedure);
1118
+
1119
+ // Filter out procedures from draft practitioners
1120
+ return await this.filterDraftPractitionerProcedures(procedures);
997
1121
  }
998
1122
 
999
1123
  /**
1000
1124
  * Gets all procedures for a practitioner
1001
1125
  * @param practitionerId - The ID of the practitioner
1002
1126
  * @param clinicBranchId - Optional clinic branch ID to filter by
1127
+ * @param excludeDraftPractitioners - Whether to exclude procedures if the practitioner is in DRAFT status
1003
1128
  * @returns List of procedures
1004
1129
  */
1005
- async getProceduresByPractitioner(practitionerId: string, clinicBranchId?: string): Promise<Procedure[]> {
1130
+ async getProceduresByPractitioner(
1131
+ practitionerId: string,
1132
+ clinicBranchId?: string,
1133
+ excludeDraftPractitioners: boolean = true
1134
+ ): Promise<Procedure[]> {
1006
1135
  const constraints: QueryConstraint[] = [
1007
1136
  where('practitionerId', '==', practitionerId),
1008
1137
  where('isActive', '==', true),
@@ -1017,7 +1146,23 @@ export class ProcedureService extends BaseService {
1017
1146
  ...constraints
1018
1147
  );
1019
1148
  const snapshot = await getDocs(q);
1020
- return snapshot.docs.map(doc => doc.data() as Procedure);
1149
+ const procedures = snapshot.docs.map(doc => doc.data() as Procedure);
1150
+
1151
+ // If we need to exclude draft practitioners and have the service available
1152
+ if (excludeDraftPractitioners && this.practitionerService) {
1153
+ try {
1154
+ const practitioner = await this.practitionerService.getPractitioner(practitionerId);
1155
+ if (practitioner && practitioner.status === PractitionerStatus.DRAFT) {
1156
+ console.log(`[ProcedureService] Excluding procedures for draft practitioner ${practitionerId}`);
1157
+ return [];
1158
+ }
1159
+ } catch (error) {
1160
+ console.error(`[ProcedureService] Error checking practitioner status for ${practitionerId}:`, error);
1161
+ // On error, default to returning procedures to avoid breaking UI
1162
+ }
1163
+ }
1164
+
1165
+ return procedures;
1021
1166
  }
1022
1167
 
1023
1168
  /**
@@ -1285,11 +1430,13 @@ export class ProcedureService extends BaseService {
1285
1430
  *
1286
1431
  * @param pagination - Optional number of procedures per page (0 or undefined returns all)
1287
1432
  * @param lastDoc - Optional last document for pagination (if continuing from a previous page)
1433
+ * @param excludeDraftPractitioners - Whether to exclude procedures from draft practitioners (default: true)
1288
1434
  * @returns Object containing procedures array and the last document for pagination
1289
1435
  */
1290
1436
  async getAllProcedures(
1291
1437
  pagination?: number,
1292
1438
  lastDoc?: any,
1439
+ excludeDraftPractitioners: boolean = true,
1293
1440
  ): Promise<{ procedures: Procedure[]; lastDoc: any }> {
1294
1441
  try {
1295
1442
  const proceduresCollection = collection(this.db, PROCEDURES_COLLECTION);
@@ -1316,7 +1463,7 @@ export class ProcedureService extends BaseService {
1316
1463
  const proceduresSnapshot = await getDocs(proceduresQuery);
1317
1464
  const lastVisible = proceduresSnapshot.docs[proceduresSnapshot.docs.length - 1];
1318
1465
 
1319
- const procedures = proceduresSnapshot.docs.map(doc => {
1466
+ let procedures = proceduresSnapshot.docs.map(doc => {
1320
1467
  const data = doc.data() as Procedure;
1321
1468
  return {
1322
1469
  ...data,
@@ -1324,6 +1471,11 @@ export class ProcedureService extends BaseService {
1324
1471
  };
1325
1472
  });
1326
1473
 
1474
+ // Filter out procedures from draft practitioners if requested
1475
+ if (excludeDraftPractitioners) {
1476
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
1477
+ }
1478
+
1327
1479
  return {
1328
1480
  procedures,
1329
1481
  lastDoc: lastVisible,
@@ -1494,6 +1646,9 @@ export class ProcedureService extends BaseService {
1494
1646
  procedures = this.applyInMemoryFilters(procedures, filters);
1495
1647
  }
1496
1648
 
1649
+ // Filter out procedures from draft practitioners
1650
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
1651
+
1497
1652
  const lastDoc =
1498
1653
  querySnapshot.docs.length > 0
1499
1654
  ? querySnapshot.docs[querySnapshot.docs.length - 1]
@@ -1551,6 +1706,9 @@ export class ProcedureService extends BaseService {
1551
1706
  procedures = this.applyInMemoryFilters(procedures, filters);
1552
1707
  }
1553
1708
 
1709
+ // Filter out procedures from draft practitioners
1710
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
1711
+
1554
1712
  const lastDoc =
1555
1713
  querySnapshot.docs.length > 0
1556
1714
  ? querySnapshot.docs[querySnapshot.docs.length - 1]
@@ -1660,6 +1818,10 @@ export class ProcedureService extends BaseService {
1660
1818
  },
1661
1819
  });
1662
1820
  procedures = this.applyInMemoryFilters(procedures, filters);
1821
+
1822
+ // Filter out procedures from draft practitioners
1823
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
1824
+
1663
1825
  console.log('[PROCEDURE_SERVICE] After applyInMemoryFilters (Strategy 3):', {
1664
1826
  procedureCount: procedures.length,
1665
1827
  });
@@ -1700,6 +1862,9 @@ export class ProcedureService extends BaseService {
1700
1862
 
1701
1863
  // Apply all client-side filters using centralized function
1702
1864
  procedures = this.applyInMemoryFilters(procedures, filters);
1865
+
1866
+ // Filter out procedures from draft practitioners
1867
+ procedures = await this.filterDraftPractitionerProcedures(procedures);
1703
1868
 
1704
1869
  const lastDoc =
1705
1870
  querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;