@blackcode_sa/metaestetics-api 1.13.19 → 1.13.21

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.js CHANGED
@@ -4606,7 +4606,7 @@ async function updateAppointmentUtil(db, appointmentId, data) {
4606
4606
  updateData.confirmationTime = import_firestore6.Timestamp.now();
4607
4607
  }
4608
4608
  if (currentAppointment.calendarEventId) {
4609
- await updateCalendarEventStatus(db, currentAppointment.calendarEventId, data.status);
4609
+ await updateCalendarEventStatus(db, currentAppointment, data.status);
4610
4610
  }
4611
4611
  }
4612
4612
  await (0, import_firestore6.updateDoc)(appointmentRef, updateData);
@@ -4620,12 +4620,11 @@ async function updateAppointmentUtil(db, appointmentId, data) {
4620
4620
  throw error;
4621
4621
  }
4622
4622
  }
4623
- async function updateCalendarEventStatus(db, calendarEventId, appointmentStatus) {
4623
+ async function updateCalendarEventStatus(db, appointment, appointmentStatus) {
4624
4624
  try {
4625
- const calendarEventRef = (0, import_firestore6.doc)(db, CALENDAR_COLLECTION, calendarEventId);
4626
- const calendarEventDoc = await (0, import_firestore6.getDoc)(calendarEventRef);
4627
- if (!calendarEventDoc.exists()) {
4628
- console.warn(`Calendar event with ID ${calendarEventId} not found`);
4625
+ const calendarEventId = appointment.calendarEventId;
4626
+ if (!calendarEventId) {
4627
+ console.warn(`Appointment ${appointment.id} has no calendarEventId, skipping calendar event update`);
4629
4628
  return;
4630
4629
  }
4631
4630
  let calendarStatus;
@@ -4660,12 +4659,48 @@ async function updateCalendarEventStatus(db, calendarEventId, appointmentStatus)
4660
4659
  console.warn(`Unknown appointment status: ${appointmentStatus}, not updating calendar event`);
4661
4660
  return;
4662
4661
  }
4663
- await (0, import_firestore6.updateDoc)(calendarEventRef, {
4662
+ const updateData = {
4664
4663
  status: calendarStatus,
4665
4664
  updatedAt: (0, import_firestore6.serverTimestamp)()
4666
- });
4665
+ };
4666
+ const updatePromises = [];
4667
+ if (appointment.practitionerId) {
4668
+ const practitionerEventRef = (0, import_firestore6.doc)(
4669
+ db,
4670
+ `${PRACTITIONERS_COLLECTION}/${appointment.practitionerId}/${CALENDAR_COLLECTION}/${calendarEventId}`
4671
+ );
4672
+ updatePromises.push(
4673
+ (0, import_firestore6.updateDoc)(practitionerEventRef, updateData).catch((error) => {
4674
+ console.error(`Error updating practitioner calendar event ${calendarEventId}:`, error);
4675
+ })
4676
+ );
4677
+ }
4678
+ if (appointment.patientId) {
4679
+ const patientEventRef = (0, import_firestore6.doc)(
4680
+ db,
4681
+ `${PATIENTS_COLLECTION}/${appointment.patientId}/${CALENDAR_COLLECTION}/${calendarEventId}`
4682
+ );
4683
+ updatePromises.push(
4684
+ (0, import_firestore6.updateDoc)(patientEventRef, updateData).catch((error) => {
4685
+ console.error(`Error updating patient calendar event ${calendarEventId}:`, error);
4686
+ })
4687
+ );
4688
+ }
4689
+ if (appointment.clinicBranchId) {
4690
+ const clinicEventRef = (0, import_firestore6.doc)(
4691
+ db,
4692
+ `${CLINICS_COLLECTION}/${appointment.clinicBranchId}/${CALENDAR_COLLECTION}/${calendarEventId}`
4693
+ );
4694
+ updatePromises.push(
4695
+ (0, import_firestore6.updateDoc)(clinicEventRef, updateData).catch((error) => {
4696
+ console.error(`Error updating clinic calendar event ${calendarEventId}:`, error);
4697
+ })
4698
+ );
4699
+ }
4700
+ await Promise.all(updatePromises);
4701
+ console.log(`Successfully updated calendar event ${calendarEventId} status to ${calendarStatus} across all collections`);
4667
4702
  } catch (error) {
4668
- console.error(`Error updating calendar event ${calendarEventId}:`, error);
4703
+ console.error(`Error updating calendar events for appointment ${appointment.id}:`, error);
4669
4704
  }
4670
4705
  }
4671
4706
  async function getAppointmentByIdUtil(db, appointmentId) {
@@ -7803,7 +7838,7 @@ var contraindicationSchema = import_zod6.z.object({
7803
7838
  notes: import_zod6.z.string().optional().nullable(),
7804
7839
  isActive: import_zod6.z.boolean()
7805
7840
  });
7806
- var medicationSchema = import_zod6.z.object({
7841
+ var baseMedicationSchema = import_zod6.z.object({
7807
7842
  name: import_zod6.z.string().min(1),
7808
7843
  dosage: import_zod6.z.string().min(1),
7809
7844
  frequency: import_zod6.z.string().min(1),
@@ -7811,6 +7846,24 @@ var medicationSchema = import_zod6.z.object({
7811
7846
  endDate: timestampSchema.optional().nullable(),
7812
7847
  prescribedBy: import_zod6.z.string().optional().nullable()
7813
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
+ );
7814
7867
  var patientMedicalInfoSchema = import_zod6.z.object({
7815
7868
  patientId: import_zod6.z.string(),
7816
7869
  vitalStats: vitalStatsSchema,
@@ -7846,9 +7899,26 @@ var updateContraindicationSchema = contraindicationSchema.partial().extend({
7846
7899
  contraindicationIndex: import_zod6.z.number().min(0)
7847
7900
  });
7848
7901
  var addMedicationSchema = medicationSchema;
7849
- var updateMedicationSchema = medicationSchema.partial().extend({
7902
+ var updateMedicationSchema = baseMedicationSchema.partial().extend({
7850
7903
  medicationIndex: import_zod6.z.number().min(0)
7851
- });
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
+ );
7852
7922
 
7853
7923
  // src/validations/patient.schema.ts
7854
7924
  var locationDataSchema = import_zod7.z.object({
@@ -12280,6 +12350,9 @@ var PractitionerService = class extends BaseService {
12280
12350
  */
12281
12351
  async EnableFreeConsultation(practitionerId, clinicId) {
12282
12352
  try {
12353
+ console.log(
12354
+ `[EnableFreeConsultation] Starting for practitioner ${practitionerId} in clinic ${clinicId}`
12355
+ );
12283
12356
  await this.ensureFreeConsultationInfrastructure();
12284
12357
  const practitioner = await this.getPractitioner(practitionerId);
12285
12358
  if (!practitioner) {
@@ -12295,32 +12368,83 @@ var PractitionerService = class extends BaseService {
12295
12368
  );
12296
12369
  }
12297
12370
  const [activeProcedures, inactiveProcedures] = await Promise.all([
12298
- this.getProcedureService().getProceduresByPractitioner(practitionerId),
12371
+ this.getProcedureService().getProceduresByPractitioner(
12372
+ practitionerId,
12373
+ void 0,
12374
+ // clinicBranchId
12375
+ false
12376
+ // excludeDraftPractitioners - allow draft practitioners
12377
+ ),
12299
12378
  this.getProcedureService().getInactiveProceduresByPractitioner(
12300
12379
  practitionerId
12301
12380
  )
12302
12381
  ]);
12303
12382
  const allProcedures = [...activeProcedures, ...inactiveProcedures];
12304
- const existingConsultation = allProcedures.find(
12383
+ const existingConsultations = allProcedures.filter(
12305
12384
  (procedure) => procedure.technology.id === "free-consultation-tech" && procedure.clinicBranchId === clinicId
12306
12385
  );
12386
+ console.log(
12387
+ `[EnableFreeConsultation] Found ${existingConsultations.length} existing free consultation(s)`
12388
+ );
12389
+ if (existingConsultations.length > 1) {
12390
+ console.warn(
12391
+ `[EnableFreeConsultation] WARNING: Found ${existingConsultations.length} duplicate free consultations for practitioner ${practitionerId} in clinic ${clinicId}`
12392
+ );
12393
+ for (let i = 1; i < existingConsultations.length; i++) {
12394
+ console.log(
12395
+ `[EnableFreeConsultation] Deactivating duplicate consultation ${existingConsultations[i].id}`
12396
+ );
12397
+ await this.getProcedureService().deactivateProcedure(
12398
+ existingConsultations[i].id
12399
+ );
12400
+ }
12401
+ }
12402
+ const existingConsultation = existingConsultations[0];
12307
12403
  if (existingConsultation) {
12308
12404
  if (existingConsultation.isActive) {
12309
12405
  console.log(
12310
- `Free consultation already active for practitioner ${practitionerId} in clinic ${clinicId}`
12406
+ `[EnableFreeConsultation] Free consultation already active for practitioner ${practitionerId} in clinic ${clinicId}`
12311
12407
  );
12312
12408
  return;
12313
12409
  } else {
12410
+ console.log(
12411
+ `[EnableFreeConsultation] Reactivating existing consultation ${existingConsultation.id}`
12412
+ );
12314
12413
  await this.getProcedureService().updateProcedure(
12315
12414
  existingConsultation.id,
12316
12415
  { isActive: true }
12317
12416
  );
12318
12417
  console.log(
12319
- `Reactivated existing free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
12418
+ `[EnableFreeConsultation] Reactivated existing free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
12320
12419
  );
12321
12420
  return;
12322
12421
  }
12323
12422
  }
12423
+ console.log(
12424
+ `[EnableFreeConsultation] Final race condition check before creating new procedure`
12425
+ );
12426
+ const finalCheckProcedures = await this.getProcedureService().getProceduresByPractitioner(
12427
+ practitionerId,
12428
+ void 0,
12429
+ // clinicBranchId
12430
+ false
12431
+ // excludeDraftPractitioners - allow draft practitioners
12432
+ );
12433
+ const raceConditionCheck = finalCheckProcedures.find(
12434
+ (procedure) => procedure.technology.id === "free-consultation-tech" && procedure.clinicBranchId === clinicId
12435
+ );
12436
+ if (raceConditionCheck) {
12437
+ console.log(
12438
+ `[EnableFreeConsultation] Race condition detected! Procedure was created by another request. Using existing procedure ${raceConditionCheck.id}`
12439
+ );
12440
+ if (!raceConditionCheck.isActive) {
12441
+ await this.getProcedureService().updateProcedure(
12442
+ raceConditionCheck.id,
12443
+ { isActive: true }
12444
+ );
12445
+ }
12446
+ return;
12447
+ }
12324
12448
  const consultationData = {
12325
12449
  name: "Free Consultation",
12326
12450
  nameLower: "free consultation",
@@ -12340,15 +12464,18 @@ var PractitionerService = class extends BaseService {
12340
12464
  photos: []
12341
12465
  // No photos for consultation
12342
12466
  };
12467
+ console.log(
12468
+ `[EnableFreeConsultation] Creating new free consultation procedure`
12469
+ );
12343
12470
  await this.getProcedureService().createConsultationProcedure(
12344
12471
  consultationData
12345
12472
  );
12346
12473
  console.log(
12347
- `Free consultation enabled for practitioner ${practitionerId} in clinic ${clinicId}`
12474
+ `[EnableFreeConsultation] Successfully created free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
12348
12475
  );
12349
12476
  } catch (error) {
12350
12477
  console.error(
12351
- `Error enabling free consultation for practitioner ${practitionerId} in clinic ${clinicId}:`,
12478
+ `[EnableFreeConsultation] Error enabling free consultation for practitioner ${practitionerId} in clinic ${clinicId}:`,
12352
12479
  error
12353
12480
  );
12354
12481
  throw error;
@@ -12444,17 +12571,40 @@ var PractitionerService = class extends BaseService {
12444
12571
  );
12445
12572
  }
12446
12573
  const existingProcedures = await this.getProcedureService().getProceduresByPractitioner(
12447
- practitionerId
12574
+ practitionerId,
12575
+ void 0,
12576
+ // clinicBranchId (optional)
12577
+ false
12578
+ // excludeDraftPractitioners - must be false to find procedures for draft practitioners
12579
+ );
12580
+ console.log(
12581
+ `[DisableFreeConsultation] Found ${existingProcedures.length} procedures for practitioner ${practitionerId}`
12448
12582
  );
12449
12583
  const freeConsultation = existingProcedures.find(
12450
12584
  (procedure) => procedure.technology.id === "free-consultation-tech" && procedure.clinicBranchId === clinicId && procedure.isActive
12451
12585
  );
12452
12586
  if (!freeConsultation) {
12453
12587
  console.log(
12454
- `No active free consultation found for practitioner ${practitionerId} in clinic ${clinicId}`
12588
+ `[DisableFreeConsultation] No active free consultation found for practitioner ${practitionerId} in clinic ${clinicId}`
12589
+ );
12590
+ console.log(
12591
+ `[DisableFreeConsultation] Existing procedures:`,
12592
+ existingProcedures.map((p) => {
12593
+ var _a;
12594
+ return {
12595
+ id: p.id,
12596
+ name: p.name,
12597
+ technologyId: (_a = p.technology) == null ? void 0 : _a.id,
12598
+ clinicBranchId: p.clinicBranchId,
12599
+ isActive: p.isActive
12600
+ };
12601
+ })
12455
12602
  );
12456
12603
  return;
12457
12604
  }
12605
+ console.log(
12606
+ `[DisableFreeConsultation] Found free consultation procedure ${freeConsultation.id}, deactivating...`
12607
+ );
12458
12608
  await this.getProcedureService().deactivateProcedure(freeConsultation.id);
12459
12609
  console.log(
12460
12610
  `Free consultation disabled for practitioner ${practitionerId} in clinic ${clinicId}`
package/dist/index.mjs CHANGED
@@ -4493,7 +4493,7 @@ async function updateAppointmentUtil(db, appointmentId, data) {
4493
4493
  updateData.confirmationTime = Timestamp5.now();
4494
4494
  }
4495
4495
  if (currentAppointment.calendarEventId) {
4496
- await updateCalendarEventStatus(db, currentAppointment.calendarEventId, data.status);
4496
+ await updateCalendarEventStatus(db, currentAppointment, data.status);
4497
4497
  }
4498
4498
  }
4499
4499
  await updateDoc2(appointmentRef, updateData);
@@ -4507,12 +4507,11 @@ async function updateAppointmentUtil(db, appointmentId, data) {
4507
4507
  throw error;
4508
4508
  }
4509
4509
  }
4510
- async function updateCalendarEventStatus(db, calendarEventId, appointmentStatus) {
4510
+ async function updateCalendarEventStatus(db, appointment, appointmentStatus) {
4511
4511
  try {
4512
- const calendarEventRef = doc4(db, CALENDAR_COLLECTION, calendarEventId);
4513
- const calendarEventDoc = await getDoc4(calendarEventRef);
4514
- if (!calendarEventDoc.exists()) {
4515
- console.warn(`Calendar event with ID ${calendarEventId} not found`);
4512
+ const calendarEventId = appointment.calendarEventId;
4513
+ if (!calendarEventId) {
4514
+ console.warn(`Appointment ${appointment.id} has no calendarEventId, skipping calendar event update`);
4516
4515
  return;
4517
4516
  }
4518
4517
  let calendarStatus;
@@ -4547,12 +4546,48 @@ async function updateCalendarEventStatus(db, calendarEventId, appointmentStatus)
4547
4546
  console.warn(`Unknown appointment status: ${appointmentStatus}, not updating calendar event`);
4548
4547
  return;
4549
4548
  }
4550
- await updateDoc2(calendarEventRef, {
4549
+ const updateData = {
4551
4550
  status: calendarStatus,
4552
4551
  updatedAt: serverTimestamp()
4553
- });
4552
+ };
4553
+ const updatePromises = [];
4554
+ if (appointment.practitionerId) {
4555
+ const practitionerEventRef = doc4(
4556
+ db,
4557
+ `${PRACTITIONERS_COLLECTION}/${appointment.practitionerId}/${CALENDAR_COLLECTION}/${calendarEventId}`
4558
+ );
4559
+ updatePromises.push(
4560
+ updateDoc2(practitionerEventRef, updateData).catch((error) => {
4561
+ console.error(`Error updating practitioner calendar event ${calendarEventId}:`, error);
4562
+ })
4563
+ );
4564
+ }
4565
+ if (appointment.patientId) {
4566
+ const patientEventRef = doc4(
4567
+ db,
4568
+ `${PATIENTS_COLLECTION}/${appointment.patientId}/${CALENDAR_COLLECTION}/${calendarEventId}`
4569
+ );
4570
+ updatePromises.push(
4571
+ updateDoc2(patientEventRef, updateData).catch((error) => {
4572
+ console.error(`Error updating patient calendar event ${calendarEventId}:`, error);
4573
+ })
4574
+ );
4575
+ }
4576
+ if (appointment.clinicBranchId) {
4577
+ const clinicEventRef = doc4(
4578
+ db,
4579
+ `${CLINICS_COLLECTION}/${appointment.clinicBranchId}/${CALENDAR_COLLECTION}/${calendarEventId}`
4580
+ );
4581
+ updatePromises.push(
4582
+ updateDoc2(clinicEventRef, updateData).catch((error) => {
4583
+ console.error(`Error updating clinic calendar event ${calendarEventId}:`, error);
4584
+ })
4585
+ );
4586
+ }
4587
+ await Promise.all(updatePromises);
4588
+ console.log(`Successfully updated calendar event ${calendarEventId} status to ${calendarStatus} across all collections`);
4554
4589
  } catch (error) {
4555
- console.error(`Error updating calendar event ${calendarEventId}:`, error);
4590
+ console.error(`Error updating calendar events for appointment ${appointment.id}:`, error);
4556
4591
  }
4557
4592
  }
4558
4593
  async function getAppointmentByIdUtil(db, appointmentId) {
@@ -7743,7 +7778,7 @@ var contraindicationSchema = z6.object({
7743
7778
  notes: z6.string().optional().nullable(),
7744
7779
  isActive: z6.boolean()
7745
7780
  });
7746
- var medicationSchema = z6.object({
7781
+ var baseMedicationSchema = z6.object({
7747
7782
  name: z6.string().min(1),
7748
7783
  dosage: z6.string().min(1),
7749
7784
  frequency: z6.string().min(1),
@@ -7751,6 +7786,24 @@ var medicationSchema = z6.object({
7751
7786
  endDate: timestampSchema.optional().nullable(),
7752
7787
  prescribedBy: z6.string().optional().nullable()
7753
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
+ );
7754
7807
  var patientMedicalInfoSchema = z6.object({
7755
7808
  patientId: z6.string(),
7756
7809
  vitalStats: vitalStatsSchema,
@@ -7786,9 +7839,26 @@ var updateContraindicationSchema = contraindicationSchema.partial().extend({
7786
7839
  contraindicationIndex: z6.number().min(0)
7787
7840
  });
7788
7841
  var addMedicationSchema = medicationSchema;
7789
- var updateMedicationSchema = medicationSchema.partial().extend({
7842
+ var updateMedicationSchema = baseMedicationSchema.partial().extend({
7790
7843
  medicationIndex: z6.number().min(0)
7791
- });
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
+ );
7792
7862
 
7793
7863
  // src/validations/patient.schema.ts
7794
7864
  var locationDataSchema = z7.object({
@@ -12303,6 +12373,9 @@ var PractitionerService = class extends BaseService {
12303
12373
  */
12304
12374
  async EnableFreeConsultation(practitionerId, clinicId) {
12305
12375
  try {
12376
+ console.log(
12377
+ `[EnableFreeConsultation] Starting for practitioner ${practitionerId} in clinic ${clinicId}`
12378
+ );
12306
12379
  await this.ensureFreeConsultationInfrastructure();
12307
12380
  const practitioner = await this.getPractitioner(practitionerId);
12308
12381
  if (!practitioner) {
@@ -12318,32 +12391,83 @@ var PractitionerService = class extends BaseService {
12318
12391
  );
12319
12392
  }
12320
12393
  const [activeProcedures, inactiveProcedures] = await Promise.all([
12321
- this.getProcedureService().getProceduresByPractitioner(practitionerId),
12394
+ this.getProcedureService().getProceduresByPractitioner(
12395
+ practitionerId,
12396
+ void 0,
12397
+ // clinicBranchId
12398
+ false
12399
+ // excludeDraftPractitioners - allow draft practitioners
12400
+ ),
12322
12401
  this.getProcedureService().getInactiveProceduresByPractitioner(
12323
12402
  practitionerId
12324
12403
  )
12325
12404
  ]);
12326
12405
  const allProcedures = [...activeProcedures, ...inactiveProcedures];
12327
- const existingConsultation = allProcedures.find(
12406
+ const existingConsultations = allProcedures.filter(
12328
12407
  (procedure) => procedure.technology.id === "free-consultation-tech" && procedure.clinicBranchId === clinicId
12329
12408
  );
12409
+ console.log(
12410
+ `[EnableFreeConsultation] Found ${existingConsultations.length} existing free consultation(s)`
12411
+ );
12412
+ if (existingConsultations.length > 1) {
12413
+ console.warn(
12414
+ `[EnableFreeConsultation] WARNING: Found ${existingConsultations.length} duplicate free consultations for practitioner ${practitionerId} in clinic ${clinicId}`
12415
+ );
12416
+ for (let i = 1; i < existingConsultations.length; i++) {
12417
+ console.log(
12418
+ `[EnableFreeConsultation] Deactivating duplicate consultation ${existingConsultations[i].id}`
12419
+ );
12420
+ await this.getProcedureService().deactivateProcedure(
12421
+ existingConsultations[i].id
12422
+ );
12423
+ }
12424
+ }
12425
+ const existingConsultation = existingConsultations[0];
12330
12426
  if (existingConsultation) {
12331
12427
  if (existingConsultation.isActive) {
12332
12428
  console.log(
12333
- `Free consultation already active for practitioner ${practitionerId} in clinic ${clinicId}`
12429
+ `[EnableFreeConsultation] Free consultation already active for practitioner ${practitionerId} in clinic ${clinicId}`
12334
12430
  );
12335
12431
  return;
12336
12432
  } else {
12433
+ console.log(
12434
+ `[EnableFreeConsultation] Reactivating existing consultation ${existingConsultation.id}`
12435
+ );
12337
12436
  await this.getProcedureService().updateProcedure(
12338
12437
  existingConsultation.id,
12339
12438
  { isActive: true }
12340
12439
  );
12341
12440
  console.log(
12342
- `Reactivated existing free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
12441
+ `[EnableFreeConsultation] Reactivated existing free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
12343
12442
  );
12344
12443
  return;
12345
12444
  }
12346
12445
  }
12446
+ console.log(
12447
+ `[EnableFreeConsultation] Final race condition check before creating new procedure`
12448
+ );
12449
+ const finalCheckProcedures = await this.getProcedureService().getProceduresByPractitioner(
12450
+ practitionerId,
12451
+ void 0,
12452
+ // clinicBranchId
12453
+ false
12454
+ // excludeDraftPractitioners - allow draft practitioners
12455
+ );
12456
+ const raceConditionCheck = finalCheckProcedures.find(
12457
+ (procedure) => procedure.technology.id === "free-consultation-tech" && procedure.clinicBranchId === clinicId
12458
+ );
12459
+ if (raceConditionCheck) {
12460
+ console.log(
12461
+ `[EnableFreeConsultation] Race condition detected! Procedure was created by another request. Using existing procedure ${raceConditionCheck.id}`
12462
+ );
12463
+ if (!raceConditionCheck.isActive) {
12464
+ await this.getProcedureService().updateProcedure(
12465
+ raceConditionCheck.id,
12466
+ { isActive: true }
12467
+ );
12468
+ }
12469
+ return;
12470
+ }
12347
12471
  const consultationData = {
12348
12472
  name: "Free Consultation",
12349
12473
  nameLower: "free consultation",
@@ -12363,15 +12487,18 @@ var PractitionerService = class extends BaseService {
12363
12487
  photos: []
12364
12488
  // No photos for consultation
12365
12489
  };
12490
+ console.log(
12491
+ `[EnableFreeConsultation] Creating new free consultation procedure`
12492
+ );
12366
12493
  await this.getProcedureService().createConsultationProcedure(
12367
12494
  consultationData
12368
12495
  );
12369
12496
  console.log(
12370
- `Free consultation enabled for practitioner ${practitionerId} in clinic ${clinicId}`
12497
+ `[EnableFreeConsultation] Successfully created free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
12371
12498
  );
12372
12499
  } catch (error) {
12373
12500
  console.error(
12374
- `Error enabling free consultation for practitioner ${practitionerId} in clinic ${clinicId}:`,
12501
+ `[EnableFreeConsultation] Error enabling free consultation for practitioner ${practitionerId} in clinic ${clinicId}:`,
12375
12502
  error
12376
12503
  );
12377
12504
  throw error;
@@ -12467,17 +12594,40 @@ var PractitionerService = class extends BaseService {
12467
12594
  );
12468
12595
  }
12469
12596
  const existingProcedures = await this.getProcedureService().getProceduresByPractitioner(
12470
- practitionerId
12597
+ practitionerId,
12598
+ void 0,
12599
+ // clinicBranchId (optional)
12600
+ false
12601
+ // excludeDraftPractitioners - must be false to find procedures for draft practitioners
12602
+ );
12603
+ console.log(
12604
+ `[DisableFreeConsultation] Found ${existingProcedures.length} procedures for practitioner ${practitionerId}`
12471
12605
  );
12472
12606
  const freeConsultation = existingProcedures.find(
12473
12607
  (procedure) => procedure.technology.id === "free-consultation-tech" && procedure.clinicBranchId === clinicId && procedure.isActive
12474
12608
  );
12475
12609
  if (!freeConsultation) {
12476
12610
  console.log(
12477
- `No active free consultation found for practitioner ${practitionerId} in clinic ${clinicId}`
12611
+ `[DisableFreeConsultation] No active free consultation found for practitioner ${practitionerId} in clinic ${clinicId}`
12612
+ );
12613
+ console.log(
12614
+ `[DisableFreeConsultation] Existing procedures:`,
12615
+ existingProcedures.map((p) => {
12616
+ var _a;
12617
+ return {
12618
+ id: p.id,
12619
+ name: p.name,
12620
+ technologyId: (_a = p.technology) == null ? void 0 : _a.id,
12621
+ clinicBranchId: p.clinicBranchId,
12622
+ isActive: p.isActive
12623
+ };
12624
+ })
12478
12625
  );
12479
12626
  return;
12480
12627
  }
12628
+ console.log(
12629
+ `[DisableFreeConsultation] Found free consultation procedure ${freeConsultation.id}, deactivating...`
12630
+ );
12481
12631
  await this.getProcedureService().deactivateProcedure(freeConsultation.id);
12482
12632
  console.log(
12483
12633
  `Free consultation disabled for practitioner ${practitionerId} in clinic ${clinicId}`
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.19",
4
+ "version": "1.13.21",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -28,6 +28,9 @@ import {
28
28
  import { CalendarEvent, CALENDAR_COLLECTION } from '../../../types/calendar';
29
29
  import { ProcedureSummaryInfo } from '../../../types/procedure';
30
30
  import { ClinicInfo, PatientProfileInfo, PractitionerProfileInfo } from '../../../types/profile';
31
+ import { PRACTITIONERS_COLLECTION } from '../../../types/practitioner';
32
+ import { PATIENTS_COLLECTION } from '../../../types/patient';
33
+ import { CLINICS_COLLECTION } from '../../../types/clinic';
31
34
  import { BlockingCondition } from '../../../backoffice/types/static/blocking-condition.types';
32
35
  import { Requirement } from '../../../backoffice/types/requirement.types';
33
36
  import { PRACTITIONERS_COLLECTION } from '../../../types/practitioner';
@@ -379,7 +382,7 @@ export async function updateAppointmentUtil(
379
382
 
380
383
  // Update the related calendar event status if needed
381
384
  if (currentAppointment.calendarEventId) {
382
- await updateCalendarEventStatus(db, currentAppointment.calendarEventId, data.status);
385
+ await updateCalendarEventStatus(db, currentAppointment, data.status);
383
386
  }
384
387
  }
385
388
 
@@ -400,28 +403,27 @@ export async function updateAppointmentUtil(
400
403
  }
401
404
 
402
405
  /**
403
- * Updates the status of a calendar event based on appointment status changes.
406
+ * Updates the status of calendar events across all collections (practitioner, patient, clinic)
407
+ * based on appointment status changes.
404
408
  *
405
409
  * @param db Firestore instance
406
- * @param calendarEventId ID of the calendar event
410
+ * @param appointment The appointment object containing calendar event references
407
411
  * @param appointmentStatus New appointment status
408
412
  */
409
413
  async function updateCalendarEventStatus(
410
414
  db: Firestore,
411
- calendarEventId: string,
415
+ appointment: Appointment,
412
416
  appointmentStatus: AppointmentStatus,
413
417
  ): Promise<void> {
414
418
  try {
415
- const calendarEventRef = doc(db, CALENDAR_COLLECTION, calendarEventId);
416
- const calendarEventDoc = await getDoc(calendarEventRef);
417
-
418
- if (!calendarEventDoc.exists()) {
419
- console.warn(`Calendar event with ID ${calendarEventId} not found`);
419
+ const calendarEventId = appointment.calendarEventId;
420
+ if (!calendarEventId) {
421
+ console.warn(`Appointment ${appointment.id} has no calendarEventId, skipping calendar event update`);
420
422
  return;
421
423
  }
422
424
 
423
425
  // Map appointment status to calendar event status
424
- let calendarStatus;
426
+ let calendarStatus: string;
425
427
  switch (appointmentStatus) {
426
428
  case AppointmentStatus.PENDING:
427
429
  calendarStatus = 'pending';
@@ -455,12 +457,58 @@ async function updateCalendarEventStatus(
455
457
  return;
456
458
  }
457
459
 
458
- await updateDoc(calendarEventRef, {
460
+ const updateData = {
459
461
  status: calendarStatus,
460
462
  updatedAt: serverTimestamp(),
461
- });
463
+ };
464
+
465
+ // Update all three calendar event collections in parallel
466
+ const updatePromises: Promise<void>[] = [];
467
+
468
+ // Update practitioner calendar event
469
+ if (appointment.practitionerId) {
470
+ const practitionerEventRef = doc(
471
+ db,
472
+ `${PRACTITIONERS_COLLECTION}/${appointment.practitionerId}/${CALENDAR_COLLECTION}/${calendarEventId}`
473
+ );
474
+ updatePromises.push(
475
+ updateDoc(practitionerEventRef, updateData).catch(error => {
476
+ console.error(`Error updating practitioner calendar event ${calendarEventId}:`, error);
477
+ })
478
+ );
479
+ }
480
+
481
+ // Update patient calendar event
482
+ if (appointment.patientId) {
483
+ const patientEventRef = doc(
484
+ db,
485
+ `${PATIENTS_COLLECTION}/${appointment.patientId}/${CALENDAR_COLLECTION}/${calendarEventId}`
486
+ );
487
+ updatePromises.push(
488
+ updateDoc(patientEventRef, updateData).catch(error => {
489
+ console.error(`Error updating patient calendar event ${calendarEventId}:`, error);
490
+ })
491
+ );
492
+ }
493
+
494
+ // Update clinic calendar event
495
+ if (appointment.clinicBranchId) {
496
+ const clinicEventRef = doc(
497
+ db,
498
+ `${CLINICS_COLLECTION}/${appointment.clinicBranchId}/${CALENDAR_COLLECTION}/${calendarEventId}`
499
+ );
500
+ updatePromises.push(
501
+ updateDoc(clinicEventRef, updateData).catch(error => {
502
+ console.error(`Error updating clinic calendar event ${calendarEventId}:`, error);
503
+ })
504
+ );
505
+ }
506
+
507
+ // Wait for all updates to complete
508
+ await Promise.all(updatePromises);
509
+ console.log(`Successfully updated calendar event ${calendarEventId} status to ${calendarStatus} across all collections`);
462
510
  } catch (error) {
463
- console.error(`Error updating calendar event ${calendarEventId}:`, error);
511
+ console.error(`Error updating calendar events for appointment ${appointment.id}:`, error);
464
512
  // Don't throw error to avoid failing the appointment update
465
513
  }
466
514
  }
@@ -1546,6 +1546,10 @@ export class PractitionerService extends BaseService {
1546
1546
  clinicId: string
1547
1547
  ): Promise<void> {
1548
1548
  try {
1549
+ console.log(
1550
+ `[EnableFreeConsultation] Starting for practitioner ${practitionerId} in clinic ${clinicId}`
1551
+ );
1552
+
1549
1553
  // First, ensure the free consultation infrastructure exists
1550
1554
  await this.ensureFreeConsultationInfrastructure();
1551
1555
 
@@ -1573,9 +1577,15 @@ export class PractitionerService extends BaseService {
1573
1577
  );
1574
1578
  }
1575
1579
 
1576
- // Get all procedures for this practitioner (including inactive ones)
1580
+ // CRITICAL: Double-check for existing procedures to prevent race conditions
1581
+ // Fetch procedures again right before creation/update
1582
+ // IMPORTANT: Pass false for excludeDraftPractitioners to work with draft practitioners
1577
1583
  const [activeProcedures, inactiveProcedures] = await Promise.all([
1578
- this.getProcedureService().getProceduresByPractitioner(practitionerId),
1584
+ this.getProcedureService().getProceduresByPractitioner(
1585
+ practitionerId,
1586
+ undefined, // clinicBranchId
1587
+ false // excludeDraftPractitioners - allow draft practitioners
1588
+ ),
1579
1589
  this.getProcedureService().getInactiveProceduresByPractitioner(
1580
1590
  practitionerId
1581
1591
  ),
@@ -1585,31 +1595,86 @@ export class PractitionerService extends BaseService {
1585
1595
  const allProcedures = [...activeProcedures, ...inactiveProcedures];
1586
1596
 
1587
1597
  // Check if free consultation already exists (active or inactive)
1588
- const existingConsultation = allProcedures.find(
1598
+ const existingConsultations = allProcedures.filter(
1589
1599
  (procedure) =>
1590
1600
  procedure.technology.id === "free-consultation-tech" &&
1591
1601
  procedure.clinicBranchId === clinicId
1592
1602
  );
1593
1603
 
1604
+ console.log(
1605
+ `[EnableFreeConsultation] Found ${existingConsultations.length} existing free consultation(s)`
1606
+ );
1607
+
1608
+ // If multiple consultations exist, log a warning and clean up duplicates
1609
+ if (existingConsultations.length > 1) {
1610
+ console.warn(
1611
+ `[EnableFreeConsultation] WARNING: Found ${existingConsultations.length} duplicate free consultations for practitioner ${practitionerId} in clinic ${clinicId}`
1612
+ );
1613
+ // Keep the first one, deactivate the rest
1614
+ for (let i = 1; i < existingConsultations.length; i++) {
1615
+ console.log(
1616
+ `[EnableFreeConsultation] Deactivating duplicate consultation ${existingConsultations[i].id}`
1617
+ );
1618
+ await this.getProcedureService().deactivateProcedure(
1619
+ existingConsultations[i].id
1620
+ );
1621
+ }
1622
+ }
1623
+
1624
+ const existingConsultation = existingConsultations[0];
1625
+
1594
1626
  if (existingConsultation) {
1595
1627
  if (existingConsultation.isActive) {
1596
1628
  console.log(
1597
- `Free consultation already active for practitioner ${practitionerId} in clinic ${clinicId}`
1629
+ `[EnableFreeConsultation] Free consultation already active for practitioner ${practitionerId} in clinic ${clinicId}`
1598
1630
  );
1599
1631
  return;
1600
1632
  } else {
1601
1633
  // Reactivate the existing disabled consultation
1634
+ console.log(
1635
+ `[EnableFreeConsultation] Reactivating existing consultation ${existingConsultation.id}`
1636
+ );
1602
1637
  await this.getProcedureService().updateProcedure(
1603
1638
  existingConsultation.id,
1604
1639
  { isActive: true }
1605
1640
  );
1606
1641
  console.log(
1607
- `Reactivated existing free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
1642
+ `[EnableFreeConsultation] Reactivated existing free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
1608
1643
  );
1609
1644
  return;
1610
1645
  }
1611
1646
  }
1612
1647
 
1648
+ // Final check before creating - race condition guard
1649
+ // Fetch one more time to ensure no procedure was created in parallel
1650
+ console.log(
1651
+ `[EnableFreeConsultation] Final race condition check before creating new procedure`
1652
+ );
1653
+ const finalCheckProcedures =
1654
+ await this.getProcedureService().getProceduresByPractitioner(
1655
+ practitionerId,
1656
+ undefined, // clinicBranchId
1657
+ false // excludeDraftPractitioners - allow draft practitioners
1658
+ );
1659
+ const raceConditionCheck = finalCheckProcedures.find(
1660
+ (procedure) =>
1661
+ procedure.technology.id === "free-consultation-tech" &&
1662
+ procedure.clinicBranchId === clinicId
1663
+ );
1664
+
1665
+ if (raceConditionCheck) {
1666
+ console.log(
1667
+ `[EnableFreeConsultation] Race condition detected! Procedure was created by another request. Using existing procedure ${raceConditionCheck.id}`
1668
+ );
1669
+ if (!raceConditionCheck.isActive) {
1670
+ await this.getProcedureService().updateProcedure(
1671
+ raceConditionCheck.id,
1672
+ { isActive: true }
1673
+ );
1674
+ }
1675
+ return;
1676
+ }
1677
+
1613
1678
  // Create procedure data for free consultation (without productId or productsMetadata)
1614
1679
  const consultationData: Omit<CreateProcedureData, "productId"> = {
1615
1680
  name: "Free Consultation",
@@ -1631,16 +1696,19 @@ export class PractitionerService extends BaseService {
1631
1696
  };
1632
1697
 
1633
1698
  // Create the consultation procedure using the special method
1699
+ console.log(
1700
+ `[EnableFreeConsultation] Creating new free consultation procedure`
1701
+ );
1634
1702
  await this.getProcedureService().createConsultationProcedure(
1635
1703
  consultationData
1636
1704
  );
1637
1705
 
1638
1706
  console.log(
1639
- `Free consultation enabled for practitioner ${practitionerId} in clinic ${clinicId}`
1707
+ `[EnableFreeConsultation] Successfully created free consultation for practitioner ${practitionerId} in clinic ${clinicId}`
1640
1708
  );
1641
1709
  } catch (error) {
1642
1710
  console.error(
1643
- `Error enabling free consultation for practitioner ${practitionerId} in clinic ${clinicId}:`,
1711
+ `[EnableFreeConsultation] Error enabling free consultation for practitioner ${practitionerId} in clinic ${clinicId}:`,
1644
1712
  error
1645
1713
  );
1646
1714
  throw error;
@@ -1764,10 +1832,18 @@ export class PractitionerService extends BaseService {
1764
1832
 
1765
1833
  // Find the free consultation procedure for this practitioner in this clinic
1766
1834
  // Use the more specific search by technology ID instead of name
1835
+ // IMPORTANT: Pass false for excludeDraftPractitioners to allow disabling for draft practitioners
1767
1836
  const existingProcedures =
1768
1837
  await this.getProcedureService().getProceduresByPractitioner(
1769
- practitionerId
1838
+ practitionerId,
1839
+ undefined, // clinicBranchId (optional)
1840
+ false // excludeDraftPractitioners - must be false to find procedures for draft practitioners
1770
1841
  );
1842
+
1843
+ console.log(
1844
+ `[DisableFreeConsultation] Found ${existingProcedures.length} procedures for practitioner ${practitionerId}`
1845
+ );
1846
+
1771
1847
  const freeConsultation = existingProcedures.find(
1772
1848
  (procedure) =>
1773
1849
  procedure.technology.id === "free-consultation-tech" &&
@@ -1777,10 +1853,24 @@ export class PractitionerService extends BaseService {
1777
1853
 
1778
1854
  if (!freeConsultation) {
1779
1855
  console.log(
1780
- `No active free consultation found for practitioner ${practitionerId} in clinic ${clinicId}`
1856
+ `[DisableFreeConsultation] No active free consultation found for practitioner ${practitionerId} in clinic ${clinicId}`
1857
+ );
1858
+ console.log(
1859
+ `[DisableFreeConsultation] Existing procedures:`,
1860
+ existingProcedures.map(p => ({
1861
+ id: p.id,
1862
+ name: p.name,
1863
+ technologyId: p.technology?.id,
1864
+ clinicBranchId: p.clinicBranchId,
1865
+ isActive: p.isActive
1866
+ }))
1781
1867
  );
1782
1868
  return;
1783
1869
  }
1870
+
1871
+ console.log(
1872
+ `[DisableFreeConsultation] Found free consultation procedure ${freeConsultation.id}, deactivating...`
1873
+ );
1784
1874
 
1785
1875
  // Deactivate the consultation procedure
1786
1876
  await this.getProcedureService().deactivateProcedure(freeConsultation.id);
@@ -63,7 +63,8 @@ export const contraindicationSchema = z.object({
63
63
  isActive: z.boolean(),
64
64
  });
65
65
 
66
- export const medicationSchema = z.object({
66
+ // Base medication schema without refinement (for update operations that need .partial())
67
+ const baseMedicationSchema = z.object({
67
68
  name: z.string().min(1),
68
69
  dosage: z.string().min(1),
69
70
  frequency: z.string().min(1),
@@ -72,6 +73,33 @@ export const medicationSchema = z.object({
72
73
  prescribedBy: z.string().optional().nullable(),
73
74
  });
74
75
 
76
+ // Medication schema with date validation refinement
77
+ export const medicationSchema = baseMedicationSchema.refine(
78
+ (data) => {
79
+ // If either date is not provided, skip validation (both are optional)
80
+ // However, if endDate is provided, startDate must also be provided
81
+ if (!data.endDate) {
82
+ return true;
83
+ }
84
+
85
+ // If endDate exists but startDate doesn't, this is invalid
86
+ if (!data.startDate) {
87
+ return false;
88
+ }
89
+
90
+ // Both dates must be Timestamp objects with toDate method
91
+ const startDate = data.startDate.toDate();
92
+ const endDate = data.endDate.toDate();
93
+
94
+ // End date must be >= start date
95
+ return endDate >= startDate;
96
+ },
97
+ {
98
+ message: "End date requires a start date and must be equal to or after start date",
99
+ path: ["endDate"], // This will attach the error to the endDate field
100
+ }
101
+ );
102
+
75
103
  export const patientMedicalInfoSchema = z.object({
76
104
  patientId: z.string(),
77
105
  vitalStats: vitalStatsSchema,
@@ -120,6 +148,30 @@ export const updateContraindicationSchema = contraindicationSchema
120
148
  });
121
149
 
122
150
  export const addMedicationSchema = medicationSchema;
123
- export const updateMedicationSchema = medicationSchema.partial().extend({
151
+ export const updateMedicationSchema = baseMedicationSchema.partial().extend({
124
152
  medicationIndex: z.number().min(0),
125
- });
153
+ }).refine(
154
+ (data) => {
155
+ // If either date is not provided, skip validation (both are optional)
156
+ // However, if endDate is provided, startDate must also be provided
157
+ if (!data.endDate) {
158
+ return true;
159
+ }
160
+
161
+ // If endDate exists but startDate doesn't, this is invalid
162
+ if (!data.startDate) {
163
+ return false;
164
+ }
165
+
166
+ // Both dates must be Timestamp objects with toDate method
167
+ const startDate = data.startDate.toDate();
168
+ const endDate = data.endDate.toDate();
169
+
170
+ // End date must be >= start date
171
+ return endDate >= startDate;
172
+ },
173
+ {
174
+ message: "End date requires a start date and must be equal to or after start date",
175
+ path: ["endDate"], // This will attach the error to the endDate field
176
+ }
177
+ );