@blackcode_sa/metaestetics-api 1.12.26 → 1.12.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2905,12 +2905,7 @@ import {
2905
2905
  } from "firebase/firestore";
2906
2906
 
2907
2907
  // src/services/patient/utils/sensitive.utils.ts
2908
- import {
2909
- getDoc as getDoc6,
2910
- updateDoc as updateDoc4,
2911
- setDoc as setDoc4,
2912
- serverTimestamp as serverTimestamp4
2913
- } from "firebase/firestore";
2908
+ import { getDoc as getDoc6, updateDoc as updateDoc4, setDoc as setDoc4, serverTimestamp as serverTimestamp4 } from "firebase/firestore";
2914
2909
 
2915
2910
  // src/validations/patient.schema.ts
2916
2911
  import { z as z7 } from "zod";
@@ -4211,10 +4206,7 @@ var checkSensitiveAccessUtil = async (db, patientId, requesterId, requesterRoles
4211
4206
  return;
4212
4207
  }
4213
4208
  if (requesterRoles.includes("practitioner" /* PRACTITIONER */)) {
4214
- const practitionerProfile = await getPractitionerProfileByUserRef(
4215
- db,
4216
- requesterId
4217
- );
4209
+ const practitionerProfile = await getPractitionerProfileByUserRef(db, requesterId);
4218
4210
  if (practitionerProfile && ((_a = patientData.doctorIds) == null ? void 0 : _a.includes(practitionerProfile.id))) {
4219
4211
  return;
4220
4212
  }
@@ -4262,16 +4254,9 @@ var handlePhotoUrlUpload = async (photoUrl, patientId, mediaService) => {
4262
4254
  };
4263
4255
  var createSensitiveInfoUtil = async (db, data, requesterId, requesterRoles, mediaService) => {
4264
4256
  try {
4265
- await checkSensitiveAccessUtil(
4266
- db,
4267
- data.patientId,
4268
- requesterId,
4269
- requesterRoles
4270
- );
4257
+ await checkSensitiveAccessUtil(db, data.patientId, requesterId, requesterRoles);
4271
4258
  const validatedData = createPatientSensitiveInfoSchema.parse(data);
4272
- const sensitiveDoc = await getDoc6(
4273
- getSensitiveInfoDocRef(db, data.patientId)
4274
- );
4259
+ const sensitiveDoc = await getDoc6(getSensitiveInfoDocRef(db, data.patientId));
4275
4260
  if (sensitiveDoc.exists()) {
4276
4261
  throw new Error("Sensitive information already exists for this patient");
4277
4262
  }
@@ -4316,11 +4301,7 @@ var updateSensitiveInfoUtil = async (db, patientId, data, requesterId, requester
4316
4301
  let processedPhotoUrl = void 0;
4317
4302
  if (data.photoUrl !== void 0) {
4318
4303
  if (mediaService) {
4319
- processedPhotoUrl = await handlePhotoUrlUpload(
4320
- data.photoUrl,
4321
- patientId,
4322
- mediaService
4323
- );
4304
+ processedPhotoUrl = await handlePhotoUrlUpload(data.photoUrl, patientId, mediaService);
4324
4305
  } else if (typeof data.photoUrl === "string" || data.photoUrl === null) {
4325
4306
  processedPhotoUrl = data.photoUrl;
4326
4307
  } else {
@@ -4339,6 +4320,36 @@ var updateSensitiveInfoUtil = async (db, patientId, data, requesterId, requester
4339
4320
  }
4340
4321
  return updatedDoc.data();
4341
4322
  };
4323
+ var claimPatientSensitiveInfoUtil = async (db, patientId, userId) => {
4324
+ const patientDoc = await getDoc6(getPatientDocRef(db, patientId));
4325
+ if (!patientDoc.exists()) {
4326
+ throw new Error("Patient profile not found");
4327
+ }
4328
+ const patientData = patientDoc.data();
4329
+ if (!patientData.isManual) {
4330
+ throw new Error("Only manually created patient profiles can be claimed");
4331
+ }
4332
+ if (patientData.userRef) {
4333
+ throw new Error("Patient profile has already been claimed");
4334
+ }
4335
+ const sensitiveDoc = await getDoc6(getSensitiveInfoDocRef(db, patientId));
4336
+ if (!sensitiveDoc.exists()) {
4337
+ throw new Error("Patient sensitive information not found");
4338
+ }
4339
+ const sensitiveData = sensitiveDoc.data();
4340
+ if (sensitiveData.userRef) {
4341
+ throw new Error("Patient sensitive information has already been claimed");
4342
+ }
4343
+ await updateDoc4(getSensitiveInfoDocRef(db, patientId), {
4344
+ userRef: userId,
4345
+ updatedAt: serverTimestamp4()
4346
+ });
4347
+ const updatedDoc = await getDoc6(getSensitiveInfoDocRef(db, patientId));
4348
+ if (!updatedDoc.exists()) {
4349
+ throw new Error("Failed to retrieve updated sensitive information");
4350
+ }
4351
+ return updatedDoc.data();
4352
+ };
4342
4353
 
4343
4354
  // src/services/patient/utils/docs.utils.ts
4344
4355
  var getPatientDocRef = (db, patientId) => {
@@ -5524,12 +5535,7 @@ var PatientService = class extends BaseService {
5524
5535
  const currentUser = await this.getCurrentUser();
5525
5536
  if (currentUser.uid !== requesterUserId) {
5526
5537
  }
5527
- return getSensitiveInfoUtil(
5528
- this.db,
5529
- patientId,
5530
- requesterUserId,
5531
- currentUser.roles
5532
- );
5538
+ return getSensitiveInfoUtil(this.db, patientId, requesterUserId, currentUser.roles);
5533
5539
  }
5534
5540
  async getSensitiveInfoByUserRef(userRef, requesterUserId) {
5535
5541
  const profile = await this.getPatientProfileByUserRef(userRef);
@@ -5550,25 +5556,17 @@ var PatientService = class extends BaseService {
5550
5556
  this.mediaService
5551
5557
  );
5552
5558
  }
5559
+ async claimPatientSensitiveInfo(patientId, userId) {
5560
+ return claimPatientSensitiveInfoUtil(this.db, patientId, userId);
5561
+ }
5553
5562
  // Metode za rad sa medicinskim informacijama
5554
5563
  async createMedicalInfo(patientId, data) {
5555
5564
  const currentUser = await this.getCurrentUser();
5556
- await createMedicalInfoUtil(
5557
- this.db,
5558
- patientId,
5559
- data,
5560
- currentUser.uid,
5561
- currentUser.roles
5562
- );
5565
+ await createMedicalInfoUtil(this.db, patientId, data, currentUser.uid, currentUser.roles);
5563
5566
  }
5564
5567
  async getMedicalInfo(patientId) {
5565
5568
  const currentUser = await this.getCurrentUser();
5566
- return getMedicalInfoUtil(
5567
- this.db,
5568
- patientId,
5569
- currentUser.uid,
5570
- currentUser.roles
5571
- );
5569
+ return getMedicalInfoUtil(this.db, patientId, currentUser.uid, currentUser.roles);
5572
5570
  }
5573
5571
  async getMedicalInfoByUserRef(userRef) {
5574
5572
  const profile = await this.getPatientProfileByUserRef(userRef);
@@ -5578,65 +5576,29 @@ var PatientService = class extends BaseService {
5578
5576
  // Metode za rad sa vitalnim statistikama
5579
5577
  async updateVitalStats(patientId, data) {
5580
5578
  const currentUser = await this.getCurrentUser();
5581
- await updateVitalStatsUtil(
5582
- this.db,
5583
- patientId,
5584
- data,
5585
- currentUser.uid,
5586
- currentUser.roles
5587
- );
5579
+ await updateVitalStatsUtil(this.db, patientId, data, currentUser.uid, currentUser.roles);
5588
5580
  }
5589
5581
  // Metode za rad sa alergijama
5590
5582
  async addAllergy(patientId, data) {
5591
5583
  const currentUser = await this.getCurrentUser();
5592
- await addAllergyUtil(
5593
- this.db,
5594
- patientId,
5595
- data,
5596
- currentUser.uid,
5597
- currentUser.roles
5598
- );
5584
+ await addAllergyUtil(this.db, patientId, data, currentUser.uid, currentUser.roles);
5599
5585
  }
5600
5586
  async updateAllergy(patientId, data) {
5601
5587
  const currentUser = await this.getCurrentUser();
5602
- await updateAllergyUtil(
5603
- this.db,
5604
- patientId,
5605
- data,
5606
- currentUser.uid,
5607
- currentUser.roles
5608
- );
5588
+ await updateAllergyUtil(this.db, patientId, data, currentUser.uid, currentUser.roles);
5609
5589
  }
5610
5590
  async removeAllergy(patientId, allergyIndex) {
5611
5591
  const currentUser = await this.getCurrentUser();
5612
- await removeAllergyUtil(
5613
- this.db,
5614
- patientId,
5615
- allergyIndex,
5616
- currentUser.uid,
5617
- currentUser.roles
5618
- );
5592
+ await removeAllergyUtil(this.db, patientId, allergyIndex, currentUser.uid, currentUser.roles);
5619
5593
  }
5620
5594
  // Metode za rad sa blocking conditions
5621
5595
  async addBlockingCondition(patientId, data) {
5622
5596
  const currentUser = await this.getCurrentUser();
5623
- await addBlockingConditionUtil(
5624
- this.db,
5625
- patientId,
5626
- data,
5627
- currentUser.uid,
5628
- currentUser.roles
5629
- );
5597
+ await addBlockingConditionUtil(this.db, patientId, data, currentUser.uid, currentUser.roles);
5630
5598
  }
5631
5599
  async updateBlockingCondition(patientId, data) {
5632
5600
  const currentUser = await this.getCurrentUser();
5633
- await updateBlockingConditionUtil(
5634
- this.db,
5635
- patientId,
5636
- data,
5637
- currentUser.uid,
5638
- currentUser.roles
5639
- );
5601
+ await updateBlockingConditionUtil(this.db, patientId, data, currentUser.uid, currentUser.roles);
5640
5602
  }
5641
5603
  async removeBlockingCondition(patientId, conditionIndex) {
5642
5604
  const currentUser = await this.getCurrentUser();
@@ -5651,23 +5613,11 @@ var PatientService = class extends BaseService {
5651
5613
  // Metode za rad sa kontraindikacijama
5652
5614
  async addContraindication(patientId, data) {
5653
5615
  const currentUser = await this.getCurrentUser();
5654
- await addContraindicationUtil(
5655
- this.db,
5656
- patientId,
5657
- data,
5658
- currentUser.uid,
5659
- currentUser.roles
5660
- );
5616
+ await addContraindicationUtil(this.db, patientId, data, currentUser.uid, currentUser.roles);
5661
5617
  }
5662
5618
  async updateContraindication(patientId, data) {
5663
5619
  const currentUser = await this.getCurrentUser();
5664
- await updateContraindicationUtil(
5665
- this.db,
5666
- patientId,
5667
- data,
5668
- currentUser.uid,
5669
- currentUser.roles
5670
- );
5620
+ await updateContraindicationUtil(this.db, patientId, data, currentUser.uid, currentUser.roles);
5671
5621
  }
5672
5622
  async removeContraindication(patientId, contraindicationIndex) {
5673
5623
  const currentUser = await this.getCurrentUser();
@@ -5682,23 +5632,11 @@ var PatientService = class extends BaseService {
5682
5632
  // Metode za rad sa medikacijama
5683
5633
  async addMedication(patientId, data) {
5684
5634
  const currentUser = await this.getCurrentUser();
5685
- await addMedicationUtil(
5686
- this.db,
5687
- patientId,
5688
- data,
5689
- currentUser.uid,
5690
- currentUser.roles
5691
- );
5635
+ await addMedicationUtil(this.db, patientId, data, currentUser.uid, currentUser.roles);
5692
5636
  }
5693
5637
  async updateMedication(patientId, data) {
5694
5638
  const currentUser = await this.getCurrentUser();
5695
- await updateMedicationUtil(
5696
- this.db,
5697
- patientId,
5698
- data,
5699
- currentUser.uid,
5700
- currentUser.roles
5701
- );
5639
+ await updateMedicationUtil(this.db, patientId, data, currentUser.uid, currentUser.roles);
5702
5640
  }
5703
5641
  async removeMedication(patientId, medicationIndex) {
5704
5642
  const currentUser = await this.getCurrentUser();
@@ -5739,9 +5677,7 @@ var PatientService = class extends BaseService {
5739
5677
  if (!this.auth.currentUser) {
5740
5678
  throw new Error("No authenticated user");
5741
5679
  }
5742
- const userDoc = await getDoc13(
5743
- doc10(this.db, "users", this.auth.currentUser.uid)
5744
- );
5680
+ const userDoc = await getDoc13(doc10(this.db, "users", this.auth.currentUser.uid));
5745
5681
  if (!userDoc.exists()) {
5746
5682
  throw new Error("User not found");
5747
5683
  }
@@ -5782,9 +5718,7 @@ var PatientService = class extends BaseService {
5782
5718
  * @returns URL of the uploaded photo
5783
5719
  */
5784
5720
  async uploadProfilePhoto(patientId, file) {
5785
- console.log(
5786
- `[PatientService] Uploading profile photo for patient ${patientId}`
5787
- );
5721
+ console.log(`[PatientService] Uploading profile photo for patient ${patientId}`);
5788
5722
  const mediaMetadata = await this.mediaService.uploadMedia(
5789
5723
  file,
5790
5724
  patientId,
@@ -5807,14 +5741,9 @@ var PatientService = class extends BaseService {
5807
5741
  * @returns URL of the new uploaded photo
5808
5742
  */
5809
5743
  async updateProfilePhoto(patientId, file) {
5810
- console.log(
5811
- `[PatientService] Updating profile photo for patient ${patientId}`
5812
- );
5744
+ console.log(`[PatientService] Updating profile photo for patient ${patientId}`);
5813
5745
  const currentUser = await this.getCurrentUser();
5814
- const currentSensitiveInfo = await this.getSensitiveInfo(
5815
- patientId,
5816
- currentUser.uid
5817
- );
5746
+ const currentSensitiveInfo = await this.getSensitiveInfo(patientId, currentUser.uid);
5818
5747
  if ((currentSensitiveInfo == null ? void 0 : currentSensitiveInfo.photoUrl) && typeof currentSensitiveInfo.photoUrl === "string") {
5819
5748
  try {
5820
5749
  const existingMediaMetadata = await this.mediaService.getMediaMetadataByUrl(
@@ -5837,14 +5766,9 @@ var PatientService = class extends BaseService {
5837
5766
  * @param patientId - ID of the patient
5838
5767
  */
5839
5768
  async deleteProfilePhoto(patientId) {
5840
- console.log(
5841
- `[PatientService] Deleting profile photo for patient ${patientId}`
5842
- );
5769
+ console.log(`[PatientService] Deleting profile photo for patient ${patientId}`);
5843
5770
  const currentUser = await this.getCurrentUser();
5844
- const currentSensitiveInfo = await this.getSensitiveInfo(
5845
- patientId,
5846
- currentUser.uid
5847
- );
5771
+ const currentSensitiveInfo = await this.getSensitiveInfo(patientId, currentUser.uid);
5848
5772
  if ((currentSensitiveInfo == null ? void 0 : currentSensitiveInfo.photoUrl) && typeof currentSensitiveInfo.photoUrl === "string") {
5849
5773
  try {
5850
5774
  const existingMediaMetadata = await this.mediaService.getMediaMetadataByUrl(
@@ -5916,10 +5840,7 @@ var PatientService = class extends BaseService {
5916
5840
  * @returns {Promise<PatientProfile[]>} A promise resolving to an array of all patient profiles.
5917
5841
  */
5918
5842
  async getAllPatients(options) {
5919
- console.log(
5920
- `[PatientService.getAllPatients] Fetching patients with options:`,
5921
- options
5922
- );
5843
+ console.log(`[PatientService.getAllPatients] Fetching patients with options:`, options);
5923
5844
  return getAllPatientsUtil(this.db, options);
5924
5845
  }
5925
5846
  /**
@@ -5950,11 +5871,7 @@ var PatientService = class extends BaseService {
5950
5871
  console.log(
5951
5872
  `[PatientService.getPatientsByPractitionerWithDetails] Fetching detailed patient profiles for practitioner: ${practitionerId}`
5952
5873
  );
5953
- return getPatientsByPractitionerWithDetailsUtil(
5954
- this.db,
5955
- practitionerId,
5956
- options
5957
- );
5874
+ return getPatientsByPractitionerWithDetailsUtil(this.db, practitionerId, options);
5958
5875
  }
5959
5876
  /**
5960
5877
  * Gets all patients associated with a specific clinic.
@@ -5966,9 +5883,7 @@ var PatientService = class extends BaseService {
5966
5883
  * @returns {Promise<PatientProfile[]>} A promise resolving to an array of patient profiles
5967
5884
  */
5968
5885
  async getPatientsByClinic(clinicId, options) {
5969
- console.log(
5970
- `[PatientService.getPatientsByClinic] Fetching patients for clinic: ${clinicId}`
5971
- );
5886
+ console.log(`[PatientService.getPatientsByClinic] Fetching patients for clinic: ${clinicId}`);
5972
5887
  return getPatientsByClinicUtil(this.db, clinicId, options);
5973
5888
  }
5974
5889
  /**
@@ -7680,11 +7595,15 @@ var UserService = class extends BaseService {
7680
7595
  if ((await this.getUserById(userId)).patientProfile || patientProfile2.userRef) {
7681
7596
  throw new Error("User already has a patient profile.");
7682
7597
  }
7683
- const sensitiveInfo = await patientService.getSensitiveInfo(patientProfile2.id, userId);
7598
+ const sensitiveInfo = await patientService.claimPatientSensitiveInfo(
7599
+ patientProfile2.id,
7600
+ userId
7601
+ );
7684
7602
  const fullDisplayName = sensitiveInfo ? `${sensitiveInfo.firstName} ${sensitiveInfo.lastName}` : patientProfile2.displayName;
7685
7603
  await patientService.updatePatientProfile(patientProfile2.id, {
7686
7604
  userRef: userId,
7687
7605
  isManual: false,
7606
+ isVerified: true,
7688
7607
  displayName: fullDisplayName
7689
7608
  });
7690
7609
  await patientService.markPatientTokenAsUsed(token.id, token.patientId, userId);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.12.26",
4
+ "version": "1.12.28",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -339,22 +339,33 @@ export class BookingAdmin {
339
339
  endTime: end.toDate().toISOString(),
340
340
  });
341
341
 
342
+ const MAX_EVENT_DURATION_MS = 24 * 60 * 60 * 1000;
343
+ const queryStart = admin.firestore.Timestamp.fromMillis(
344
+ start.toMillis() - MAX_EVENT_DURATION_MS
345
+ );
346
+
342
347
  const eventsRef = this.db
343
348
  .collection(`clinics/${clinicId}/calendar`)
344
- .where("eventTime.start", ">=", start)
345
- .where("eventTime.start", "<=", end);
349
+ .where("eventTime.start", ">=", queryStart)
350
+ .where("eventTime.start", "<", end)
351
+ .orderBy("eventTime.start");
346
352
 
347
353
  const snapshot = await eventsRef.get();
348
354
 
349
- const events = snapshot.docs.map((doc) => ({
350
- ...doc.data(),
351
- id: doc.id,
352
- }));
355
+ const events = snapshot.docs
356
+ .map((doc) => ({
357
+ ...doc.data(),
358
+ id: doc.id,
359
+ }))
360
+ .filter((event: any) => {
361
+ return event.eventTime.end.toMillis() > start.toMillis();
362
+ });
353
363
 
354
364
  Logger.debug("[BookingAdmin] Retrieved clinic calendar events", {
355
365
  clinicId,
356
366
  eventsCount: events.length,
357
367
  eventsTypes: this.summarizeEventTypes(events),
368
+ queryStartTime: queryStart.toDate().toISOString(),
358
369
  });
359
370
 
360
371
  return events;
@@ -392,22 +403,33 @@ export class BookingAdmin {
392
403
  endTime: end.toDate().toISOString(),
393
404
  });
394
405
 
406
+ const MAX_EVENT_DURATION_MS = 24 * 60 * 60 * 1000;
407
+ const queryStart = admin.firestore.Timestamp.fromMillis(
408
+ start.toMillis() - MAX_EVENT_DURATION_MS
409
+ );
410
+
395
411
  const eventsRef = this.db
396
412
  .collection(`practitioners/${practitionerId}/calendar`)
397
- .where("eventTime.start", ">=", start)
398
- .where("eventTime.start", "<=", end);
413
+ .where("eventTime.start", ">=", queryStart)
414
+ .where("eventTime.start", "<", end)
415
+ .orderBy("eventTime.start");
399
416
 
400
417
  const snapshot = await eventsRef.get();
401
418
 
402
- const events = snapshot.docs.map((doc) => ({
403
- ...doc.data(),
404
- id: doc.id,
405
- }));
419
+ const events = snapshot.docs
420
+ .map((doc) => ({
421
+ ...doc.data(),
422
+ id: doc.id,
423
+ }))
424
+ .filter((event: any) => {
425
+ return event.eventTime.end.toMillis() > start.toMillis();
426
+ });
406
427
 
407
428
  Logger.debug("[BookingAdmin] Retrieved practitioner calendar events", {
408
429
  practitionerId,
409
430
  eventsCount: events.length,
410
431
  eventsTypes: this.summarizeEventTypes(events),
432
+ queryStartTime: queryStart.toDate().toISOString(),
411
433
  });
412
434
 
413
435
  return events;
@@ -96,7 +96,8 @@ export class BookingAvailabilityCalculator {
96
96
  const availableSlots = this.generateAvailableSlots(
97
97
  availableIntervals,
98
98
  schedulingIntervalMinutes,
99
- procedureDurationMinutes
99
+ procedureDurationMinutes,
100
+ tz
100
101
  );
101
102
 
102
103
  return { availableSlots };
@@ -469,16 +470,18 @@ export class BookingAvailabilityCalculator {
469
470
  * @param intervals - Final available intervals
470
471
  * @param intervalMinutes - Scheduling interval in minutes
471
472
  * @param durationMinutes - Procedure duration in minutes
473
+ * @param tz - IANA timezone of the clinic
472
474
  * @returns Array of available booking slots
473
475
  */
474
476
  private static generateAvailableSlots(
475
477
  intervals: TimeInterval[],
476
478
  intervalMinutes: number,
477
- durationMinutes: number
479
+ durationMinutes: number,
480
+ tz: string
478
481
  ): AvailableSlot[] {
479
482
  const slots: AvailableSlot[] = [];
480
483
  console.log(
481
- `Generating slots with ${intervalMinutes}min intervals for ${durationMinutes}min procedure`
484
+ `Generating slots with ${intervalMinutes}min intervals for ${durationMinutes}min procedure in timezone ${tz}`
482
485
  );
483
486
 
484
487
  // Convert duration to milliseconds
@@ -492,8 +495,8 @@ export class BookingAvailabilityCalculator {
492
495
  const intervalStart = interval.start.toDate();
493
496
  const intervalEnd = interval.end.toDate();
494
497
 
495
- // Start at the beginning of the interval
496
- let slotStart = DateTime.fromJSDate(intervalStart);
498
+ // Start at the beginning of the interval IN CLINIC TIMEZONE
499
+ let slotStart = DateTime.fromMillis(intervalStart.getTime(), { zone: tz });
497
500
 
498
501
  // Adjust slotStart to the nearest interval boundary if needed
499
502
  const minutesIntoDay = slotStart.hour * 60 + slotStart.minute;
@@ -511,7 +514,7 @@ export class BookingAvailabilityCalculator {
511
514
  const slotEnd = slotStart.plus({ minutes: durationMinutes });
512
515
 
513
516
  // Check if this slot fits entirely within one of our available intervals
514
- if (this.isSlotFullyAvailable(slotStart, slotEnd, intervals)) {
517
+ if (this.isSlotFullyAvailable(slotStart, slotEnd, intervals, tz)) {
515
518
  slots.push({
516
519
  start: Timestamp.fromMillis(slotStart.toMillis()),
517
520
  });
@@ -532,17 +535,19 @@ export class BookingAvailabilityCalculator {
532
535
  * @param slotStart - Start time of the slot
533
536
  * @param slotEnd - End time of the slot
534
537
  * @param intervals - Available intervals
538
+ * @param tz - IANA timezone of the clinic
535
539
  * @returns True if the slot is fully contained within an available interval
536
540
  */
537
541
  private static isSlotFullyAvailable(
538
542
  slotStart: DateTime,
539
543
  slotEnd: DateTime,
540
- intervals: TimeInterval[]
544
+ intervals: TimeInterval[],
545
+ tz: string
541
546
  ): boolean {
542
547
  // Check if the slot is fully contained in any of the available intervals
543
548
  return intervals.some((interval) => {
544
- const intervalStart = DateTime.fromMillis(interval.start.toMillis());
545
- const intervalEnd = DateTime.fromMillis(interval.end.toMillis());
549
+ const intervalStart = DateTime.fromMillis(interval.start.toMillis(), { zone: tz });
550
+ const intervalEnd = DateTime.fromMillis(interval.end.toMillis(), { zone: tz });
546
551
 
547
552
  return slotStart >= intervalStart && slotEnd <= intervalEnd;
548
553
  });
@@ -122,11 +122,55 @@ Alignment expectation:
122
122
  - Interval sizes that don’t divide 60 (e.g., 20 min), and variable procedure durations.
123
123
 
124
124
  ### Actionable checklist
125
- - Make all DateTime constructions in slot generation and containment checks use `{ zone: tz }`.
126
- - Replace `fromJSDate` with `fromMillis` in calculator paths.
127
- - Update event fetching to include overlaps with the timeframe; union queries or adjust model.
128
- - Re-assert the type contract: calculator receives only client `Timestamp` + `tz`.
129
- - Document UI responsibility to convert wall-clock (clinic `tz`) → UTC Timestamp exactly once.
125
+ - Make all DateTime constructions in slot generation and containment checks use `{ zone: tz }`.
126
+ - Replace `fromJSDate` with `fromMillis` in calculator paths.
127
+ - Update event fetching to include overlaps with the timeframe; union queries or adjust model.
128
+ - Re-assert the type contract: calculator receives only client `Timestamp` + `tz`.
129
+ - Document UI responsibility to convert wall-clock (clinic `tz`) → UTC Timestamp exactly once.
130
+
131
+ ### PHASE 1 COMPLETED: Backend Calculator Fixes ✅
132
+
133
+ **Date Completed:** October 1, 2025
134
+
135
+ **Changes Made:**
136
+
137
+ 1. **booking.calculator.ts - generateAvailableSlots()**
138
+ - Added `tz: string` parameter
139
+ - Changed `DateTime.fromJSDate(intervalStart)` → `DateTime.fromMillis(intervalStart.getTime(), { zone: tz })`
140
+ - Now explicitly creates DateTime in clinic timezone for all slot calculations
141
+
142
+ 2. **booking.calculator.ts - isSlotFullyAvailable()**
143
+ - Added `tz: string` parameter
144
+ - Changed `DateTime.fromMillis(interval.start.toMillis())` → `DateTime.fromMillis(interval.start.toMillis(), { zone: tz })`
145
+ - Interval boundary checks now use clinic timezone context
146
+
147
+ 3. **booking.calculator.ts - calculateSlots()**
148
+ - Updated call to `generateAvailableSlots()` to pass `tz` parameter
149
+ - Updated call to `isSlotFullyAvailable()` to pass `tz` parameter
150
+
151
+ 4. **booking.admin.ts - getClinicCalendarEvents()**
152
+ - Fixed event overlap logic with bounded query
153
+ - Added lower bound: `queryStart = start - 24 hours` to prevent querying all historical events
154
+ - Query: `eventTime.start >= queryStart AND eventTime.start < end`
155
+ - Added post-filter: `eventTime.end > start` to catch all overlapping events
156
+ - Prevents missing events that start before window but overlap into it
157
+ - Performance optimized: only queries ~24-48 hours of events instead of entire history
158
+
159
+ 5. **booking.admin.ts - getPractitionerCalendarEvents()**
160
+ - Applied same overlap fix with 24-hour lookback window
161
+ - Ensures busy time subtraction includes all conflicting events
162
+ - Efficient: assumes no appointments longer than 24 hours
163
+
164
+ **Impact:**
165
+ - Slot generation now correctly happens in clinic timezone
166
+ - Slots are properly converted to UTC for storage/transmission
167
+ - Event blocking now catches all overlapping events, not just those starting within window
168
+ - Fixes timezone display issues for users in different timezones
169
+
170
+ **Next Steps:**
171
+ - Phase 2: Mobile app fixes (use original UTC timestamp from slots)
172
+ - Phase 3: ClinicApp fixes (same as Mobile)
173
+ - Phase 4: Testing across multiple timezones and DST boundaries
130
174
 
131
175
  ### Known risks
132
176
  - DST transitions can produce days with 23/25 hours; rounding/iteration must not assume 24h.
@@ -17,7 +17,7 @@ import {
17
17
  arrayUnion,
18
18
  arrayRemove,
19
19
  } from "firebase/firestore";
20
- import { getFunctions, httpsCallable, connectFunctionsEmulator } from "firebase/functions";
20
+ import { getFunctions, httpsCallable } from "firebase/functions";
21
21
  import { BaseService } from "../base.service";
22
22
  import {
23
23
  Clinic,