@blackcode_sa/metaestetics-api 1.12.57 → 1.12.58

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.
@@ -3558,6 +3558,20 @@ interface ReviewAddedEmailData extends AppointmentEmailDataBase {
3558
3558
  declare class AppointmentMailingService extends BaseMailingService {
3559
3559
  private readonly DEFAULT_MAILGUN_DOMAIN;
3560
3560
  constructor(firestore: admin.firestore.Firestore, mailgunClient: NewMailgunClient$2);
3561
+ /**
3562
+ * Formats a Firestore Timestamp in the clinic's timezone
3563
+ * @param timestamp - Firestore Timestamp (UTC)
3564
+ * @param clinicTimezone - IANA timezone string (e.g., "Europe/Zurich")
3565
+ * @param format - Format type: 'date', 'time', or 'datetime'
3566
+ * @returns Formatted string in clinic's local timezone
3567
+ */
3568
+ private formatTimestampInClinicTimezone;
3569
+ /**
3570
+ * Gets a user-friendly display name for a timezone
3571
+ * @param timezone - IANA timezone string (e.g., "Europe/Zurich")
3572
+ * @returns User-friendly timezone name (e.g., "Clinic Time - Europe/Zurich")
3573
+ */
3574
+ private getTimezoneDisplayName;
3561
3575
  sendAppointmentConfirmedEmail(data: AppointmentConfirmationEmailData): Promise<any>;
3562
3576
  sendAppointmentRequestedEmailToClinic(data: AppointmentRequestedEmailData): Promise<any>;
3563
3577
  sendAppointmentCancelledEmail(data: AppointmentCancellationEmailData): Promise<any>;
@@ -3558,6 +3558,20 @@ interface ReviewAddedEmailData extends AppointmentEmailDataBase {
3558
3558
  declare class AppointmentMailingService extends BaseMailingService {
3559
3559
  private readonly DEFAULT_MAILGUN_DOMAIN;
3560
3560
  constructor(firestore: admin.firestore.Firestore, mailgunClient: NewMailgunClient$2);
3561
+ /**
3562
+ * Formats a Firestore Timestamp in the clinic's timezone
3563
+ * @param timestamp - Firestore Timestamp (UTC)
3564
+ * @param clinicTimezone - IANA timezone string (e.g., "Europe/Zurich")
3565
+ * @param format - Format type: 'date', 'time', or 'datetime'
3566
+ * @returns Formatted string in clinic's local timezone
3567
+ */
3568
+ private formatTimestampInClinicTimezone;
3569
+ /**
3570
+ * Gets a user-friendly display name for a timezone
3571
+ * @param timezone - IANA timezone string (e.g., "Europe/Zurich")
3572
+ * @returns User-friendly timezone name (e.g., "Clinic Time - Europe/Zurich")
3573
+ */
3574
+ private getTimezoneDisplayName;
3561
3575
  sendAppointmentConfirmedEmail(data: AppointmentConfirmationEmailData): Promise<any>;
3562
3576
  sendAppointmentRequestedEmailToClinic(data: AppointmentRequestedEmailData): Promise<any>;
3563
3577
  sendAppointmentCancelledEmail(data: AppointmentCancellationEmailData): Promise<any>;
@@ -1765,6 +1765,9 @@ var CalendarAdminService = class {
1765
1765
  }
1766
1766
  };
1767
1767
 
1768
+ // src/admin/mailing/appointment/appointment.mailing.service.ts
1769
+ var import_luxon = require("luxon");
1770
+
1768
1771
  // src/admin/mailing/base.mailing.service.ts
1769
1772
  var admin5 = __toESM(require("firebase-admin"));
1770
1773
  var BaseMailingService = class {
@@ -2323,6 +2326,62 @@ var AppointmentMailingService = class extends BaseMailingService {
2323
2326
  this.DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
2324
2327
  Logger.info("[AppointmentMailingService] Initialized.");
2325
2328
  }
2329
+ /**
2330
+ * Formats a Firestore Timestamp in the clinic's timezone
2331
+ * @param timestamp - Firestore Timestamp (UTC)
2332
+ * @param clinicTimezone - IANA timezone string (e.g., "Europe/Zurich")
2333
+ * @param format - Format type: 'date', 'time', or 'datetime'
2334
+ * @returns Formatted string in clinic's local timezone
2335
+ */
2336
+ formatTimestampInClinicTimezone(timestamp, clinicTimezone, format = "datetime") {
2337
+ try {
2338
+ const dateTimeInClinicTz = import_luxon.DateTime.fromMillis(timestamp.toMillis(), {
2339
+ zone: clinicTimezone
2340
+ });
2341
+ switch (format) {
2342
+ case "date":
2343
+ return dateTimeInClinicTz.toLocaleString(import_luxon.DateTime.DATE_FULL);
2344
+ case "time":
2345
+ return dateTimeInClinicTz.toLocaleString(import_luxon.DateTime.TIME_SIMPLE);
2346
+ case "datetime":
2347
+ return dateTimeInClinicTz.toLocaleString(import_luxon.DateTime.DATETIME_FULL);
2348
+ default:
2349
+ return dateTimeInClinicTz.toLocaleString(import_luxon.DateTime.DATETIME_FULL);
2350
+ }
2351
+ } catch (error) {
2352
+ Logger.error("[AppointmentMailingService] Error formatting timestamp in clinic timezone:", {
2353
+ error: error instanceof Error ? error.message : String(error),
2354
+ clinicTimezone
2355
+ });
2356
+ return timestamp.toDate().toLocaleString();
2357
+ }
2358
+ }
2359
+ /**
2360
+ * Gets a user-friendly display name for a timezone
2361
+ * @param timezone - IANA timezone string (e.g., "Europe/Zurich")
2362
+ * @returns User-friendly timezone name (e.g., "Clinic Time - Europe/Zurich")
2363
+ */
2364
+ getTimezoneDisplayName(timezone) {
2365
+ try {
2366
+ const timezoneMap = {
2367
+ "Europe/Zurich": "Clinic Time - Central European Time",
2368
+ "Europe/London": "Clinic Time - GMT/BST",
2369
+ "America/New_York": "Clinic Time - Eastern Time",
2370
+ "America/Los_Angeles": "Clinic Time - Pacific Time",
2371
+ "Asia/Dubai": "Clinic Time - Gulf Standard Time",
2372
+ "Asia/Karachi": "Clinic Time - Pakistan Standard Time",
2373
+ "Asia/Kolkata": "Clinic Time - India Standard Time",
2374
+ "Australia/Sydney": "Clinic Time - Australian Eastern Time"
2375
+ };
2376
+ return timezoneMap[timezone] || `Clinic Time - ${timezone}`;
2377
+ } catch (error) {
2378
+ Logger.error("[AppointmentMailingService] Error getting timezone display name:", {
2379
+ error: error instanceof Error ? error.message : String(error),
2380
+ timezone
2381
+ });
2382
+ return `Clinic Time - ${timezone}`;
2383
+ }
2384
+ }
2326
2385
  async sendAppointmentConfirmedEmail(data) {
2327
2386
  var _a, _b, _c, _d, _e;
2328
2387
  Logger.info(
@@ -2336,11 +2395,26 @@ var AppointmentMailingService = class extends BaseMailingService {
2336
2395
  });
2337
2396
  throw new Error("Recipient email address is missing.");
2338
2397
  }
2398
+ const clinicTimezone = data.appointment.clinic_tz || "UTC";
2399
+ Logger.debug("[AppointmentMailingService] Formatting appointment time", {
2400
+ clinicTimezone,
2401
+ utcTime: data.appointment.appointmentStartTime.toDate().toISOString()
2402
+ });
2403
+ const formattedTime = this.formatTimestampInClinicTimezone(
2404
+ data.appointment.appointmentStartTime,
2405
+ clinicTimezone,
2406
+ "time"
2407
+ );
2408
+ const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
2339
2409
  const templateVariables = {
2340
2410
  patientName: data.appointment.patientInfo.fullName,
2341
2411
  procedureName: data.appointment.procedureInfo.name,
2342
- appointmentDate: data.appointment.appointmentStartTime.toDate().toLocaleDateString(),
2343
- appointmentTime: data.appointment.appointmentStartTime.toDate().toLocaleTimeString(),
2412
+ appointmentDate: this.formatTimestampInClinicTimezone(
2413
+ data.appointment.appointmentStartTime,
2414
+ clinicTimezone,
2415
+ "date"
2416
+ ),
2417
+ appointmentTime: `${formattedTime} (${timezoneName})`,
2344
2418
  practitionerName: data.appointment.practitionerInfo.name,
2345
2419
  clinicName: data.appointment.clinicInfo.name
2346
2420
  };
@@ -2387,12 +2461,27 @@ var AppointmentMailingService = class extends BaseMailingService {
2387
2461
  );
2388
2462
  throw new Error("Clinic contact email address is missing.");
2389
2463
  }
2464
+ const clinicTimezone = data.appointment.clinic_tz || "UTC";
2465
+ Logger.debug("[AppointmentMailingService] Formatting appointment time for clinic", {
2466
+ clinicTimezone,
2467
+ utcTime: data.appointment.appointmentStartTime.toDate().toISOString()
2468
+ });
2469
+ const formattedTime = this.formatTimestampInClinicTimezone(
2470
+ data.appointment.appointmentStartTime,
2471
+ clinicTimezone,
2472
+ "time"
2473
+ );
2474
+ const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
2390
2475
  const templateVariables = {
2391
2476
  clinicName: data.clinicProfile.name,
2392
2477
  patientName: data.appointment.patientInfo.fullName,
2393
2478
  procedureName: data.appointment.procedureInfo.name,
2394
- appointmentDate: data.appointment.appointmentStartTime.toDate().toLocaleDateString(),
2395
- appointmentTime: data.appointment.appointmentStartTime.toDate().toLocaleTimeString(),
2479
+ appointmentDate: this.formatTimestampInClinicTimezone(
2480
+ data.appointment.appointmentStartTime,
2481
+ clinicTimezone,
2482
+ "date"
2483
+ ),
2484
+ appointmentTime: `${formattedTime} (${timezoneName})`,
2396
2485
  practitionerName: data.appointment.practitionerInfo.name
2397
2486
  };
2398
2487
  const html = this.renderTemplate(clinicAppointmentRequestedTemplate, templateVariables);
@@ -6576,7 +6665,7 @@ var ReviewsAggregationService = class {
6576
6665
 
6577
6666
  // src/admin/booking/booking.calculator.ts
6578
6667
  var import_firestore2 = require("firebase/firestore");
6579
- var import_luxon = require("luxon");
6668
+ var import_luxon2 = require("luxon");
6580
6669
  var BookingAvailabilityCalculator = class {
6581
6670
  /**
6582
6671
  * Calculate available booking slots based on the provided data
@@ -6667,8 +6756,8 @@ var BookingAvailabilityCalculator = class {
6667
6756
  */
6668
6757
  static createWorkingHoursIntervals(workingHours, startDate, endDate, tz) {
6669
6758
  const workingIntervals = [];
6670
- let start = import_luxon.DateTime.fromMillis(startDate.getTime(), { zone: tz });
6671
- const end = import_luxon.DateTime.fromMillis(endDate.getTime(), { zone: tz });
6759
+ let start = import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz });
6760
+ const end = import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz });
6672
6761
  while (start <= end) {
6673
6762
  const dayOfWeek = start.weekday;
6674
6763
  const dayName = [
@@ -6698,8 +6787,8 @@ var BookingAvailabilityCalculator = class {
6698
6787
  millisecond: 0
6699
6788
  });
6700
6789
  if (workEnd.toMillis() > startDate.getTime() && workStart.toMillis() < endDate.getTime()) {
6701
- const intervalStart = workStart < import_luxon.DateTime.fromMillis(startDate.getTime(), { zone: tz }) ? import_luxon.DateTime.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
6702
- const intervalEnd = workEnd > import_luxon.DateTime.fromMillis(endDate.getTime(), { zone: tz }) ? import_luxon.DateTime.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
6790
+ const intervalStart = workStart < import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz }) ? import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
6791
+ const intervalEnd = workEnd > import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz }) ? import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
6703
6792
  workingIntervals.push({
6704
6793
  start: import_firestore2.Timestamp.fromMillis(intervalStart.toMillis()),
6705
6794
  end: import_firestore2.Timestamp.fromMillis(intervalEnd.toMillis())
@@ -6806,8 +6895,8 @@ var BookingAvailabilityCalculator = class {
6806
6895
  */
6807
6896
  static createPractitionerWorkingHoursIntervals(workingHours, startDate, endDate, tz) {
6808
6897
  const workingIntervals = [];
6809
- let start = import_luxon.DateTime.fromMillis(startDate.getTime(), { zone: tz });
6810
- const end = import_luxon.DateTime.fromMillis(endDate.getTime(), { zone: tz });
6898
+ let start = import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz });
6899
+ const end = import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz });
6811
6900
  while (start <= end) {
6812
6901
  const dayOfWeek = start.weekday;
6813
6902
  const dayName = [
@@ -6830,8 +6919,8 @@ var BookingAvailabilityCalculator = class {
6830
6919
  });
6831
6920
  const workEnd = start.set({ hour: endHours, minute: endMinutes });
6832
6921
  if (workEnd.toMillis() > startDate.getTime() && workStart.toMillis() < endDate.getTime()) {
6833
- const intervalStart = workStart < import_luxon.DateTime.fromMillis(startDate.getTime(), { zone: tz }) ? import_luxon.DateTime.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
6834
- const intervalEnd = workEnd > import_luxon.DateTime.fromMillis(endDate.getTime(), { zone: tz }) ? import_luxon.DateTime.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
6922
+ const intervalStart = workStart < import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz }) ? import_luxon2.DateTime.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
6923
+ const intervalEnd = workEnd > import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz }) ? import_luxon2.DateTime.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
6835
6924
  workingIntervals.push({
6836
6925
  start: import_firestore2.Timestamp.fromMillis(intervalStart.toMillis()),
6837
6926
  end: import_firestore2.Timestamp.fromMillis(intervalEnd.toMillis())
@@ -6895,7 +6984,7 @@ var BookingAvailabilityCalculator = class {
6895
6984
  for (const interval of intervals) {
6896
6985
  const intervalStart = interval.start.toDate();
6897
6986
  const intervalEnd = interval.end.toDate();
6898
- let slotStart = import_luxon.DateTime.fromMillis(intervalStart.getTime(), { zone: tz });
6987
+ let slotStart = import_luxon2.DateTime.fromMillis(intervalStart.getTime(), { zone: tz });
6899
6988
  const minutesIntoDay = slotStart.hour * 60 + slotStart.minute;
6900
6989
  const minutesRemainder = minutesIntoDay % intervalMinutes;
6901
6990
  if (minutesRemainder > 0) {
@@ -6927,8 +7016,8 @@ var BookingAvailabilityCalculator = class {
6927
7016
  */
6928
7017
  static isSlotFullyAvailable(slotStart, slotEnd, intervals, tz) {
6929
7018
  return intervals.some((interval) => {
6930
- const intervalStart = import_luxon.DateTime.fromMillis(interval.start.toMillis(), { zone: tz });
6931
- const intervalEnd = import_luxon.DateTime.fromMillis(interval.end.toMillis(), { zone: tz });
7019
+ const intervalStart = import_luxon2.DateTime.fromMillis(interval.start.toMillis(), { zone: tz });
7020
+ const intervalEnd = import_luxon2.DateTime.fromMillis(interval.end.toMillis(), { zone: tz });
6932
7021
  return slotStart >= intervalStart && slotEnd <= intervalEnd;
6933
7022
  });
6934
7023
  }
@@ -1703,6 +1703,9 @@ var CalendarAdminService = class {
1703
1703
  }
1704
1704
  };
1705
1705
 
1706
+ // src/admin/mailing/appointment/appointment.mailing.service.ts
1707
+ import { DateTime } from "luxon";
1708
+
1706
1709
  // src/admin/mailing/base.mailing.service.ts
1707
1710
  import * as admin5 from "firebase-admin";
1708
1711
  var BaseMailingService = class {
@@ -2261,6 +2264,62 @@ var AppointmentMailingService = class extends BaseMailingService {
2261
2264
  this.DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
2262
2265
  Logger.info("[AppointmentMailingService] Initialized.");
2263
2266
  }
2267
+ /**
2268
+ * Formats a Firestore Timestamp in the clinic's timezone
2269
+ * @param timestamp - Firestore Timestamp (UTC)
2270
+ * @param clinicTimezone - IANA timezone string (e.g., "Europe/Zurich")
2271
+ * @param format - Format type: 'date', 'time', or 'datetime'
2272
+ * @returns Formatted string in clinic's local timezone
2273
+ */
2274
+ formatTimestampInClinicTimezone(timestamp, clinicTimezone, format = "datetime") {
2275
+ try {
2276
+ const dateTimeInClinicTz = DateTime.fromMillis(timestamp.toMillis(), {
2277
+ zone: clinicTimezone
2278
+ });
2279
+ switch (format) {
2280
+ case "date":
2281
+ return dateTimeInClinicTz.toLocaleString(DateTime.DATE_FULL);
2282
+ case "time":
2283
+ return dateTimeInClinicTz.toLocaleString(DateTime.TIME_SIMPLE);
2284
+ case "datetime":
2285
+ return dateTimeInClinicTz.toLocaleString(DateTime.DATETIME_FULL);
2286
+ default:
2287
+ return dateTimeInClinicTz.toLocaleString(DateTime.DATETIME_FULL);
2288
+ }
2289
+ } catch (error) {
2290
+ Logger.error("[AppointmentMailingService] Error formatting timestamp in clinic timezone:", {
2291
+ error: error instanceof Error ? error.message : String(error),
2292
+ clinicTimezone
2293
+ });
2294
+ return timestamp.toDate().toLocaleString();
2295
+ }
2296
+ }
2297
+ /**
2298
+ * Gets a user-friendly display name for a timezone
2299
+ * @param timezone - IANA timezone string (e.g., "Europe/Zurich")
2300
+ * @returns User-friendly timezone name (e.g., "Clinic Time - Europe/Zurich")
2301
+ */
2302
+ getTimezoneDisplayName(timezone) {
2303
+ try {
2304
+ const timezoneMap = {
2305
+ "Europe/Zurich": "Clinic Time - Central European Time",
2306
+ "Europe/London": "Clinic Time - GMT/BST",
2307
+ "America/New_York": "Clinic Time - Eastern Time",
2308
+ "America/Los_Angeles": "Clinic Time - Pacific Time",
2309
+ "Asia/Dubai": "Clinic Time - Gulf Standard Time",
2310
+ "Asia/Karachi": "Clinic Time - Pakistan Standard Time",
2311
+ "Asia/Kolkata": "Clinic Time - India Standard Time",
2312
+ "Australia/Sydney": "Clinic Time - Australian Eastern Time"
2313
+ };
2314
+ return timezoneMap[timezone] || `Clinic Time - ${timezone}`;
2315
+ } catch (error) {
2316
+ Logger.error("[AppointmentMailingService] Error getting timezone display name:", {
2317
+ error: error instanceof Error ? error.message : String(error),
2318
+ timezone
2319
+ });
2320
+ return `Clinic Time - ${timezone}`;
2321
+ }
2322
+ }
2264
2323
  async sendAppointmentConfirmedEmail(data) {
2265
2324
  var _a, _b, _c, _d, _e;
2266
2325
  Logger.info(
@@ -2274,11 +2333,26 @@ var AppointmentMailingService = class extends BaseMailingService {
2274
2333
  });
2275
2334
  throw new Error("Recipient email address is missing.");
2276
2335
  }
2336
+ const clinicTimezone = data.appointment.clinic_tz || "UTC";
2337
+ Logger.debug("[AppointmentMailingService] Formatting appointment time", {
2338
+ clinicTimezone,
2339
+ utcTime: data.appointment.appointmentStartTime.toDate().toISOString()
2340
+ });
2341
+ const formattedTime = this.formatTimestampInClinicTimezone(
2342
+ data.appointment.appointmentStartTime,
2343
+ clinicTimezone,
2344
+ "time"
2345
+ );
2346
+ const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
2277
2347
  const templateVariables = {
2278
2348
  patientName: data.appointment.patientInfo.fullName,
2279
2349
  procedureName: data.appointment.procedureInfo.name,
2280
- appointmentDate: data.appointment.appointmentStartTime.toDate().toLocaleDateString(),
2281
- appointmentTime: data.appointment.appointmentStartTime.toDate().toLocaleTimeString(),
2350
+ appointmentDate: this.formatTimestampInClinicTimezone(
2351
+ data.appointment.appointmentStartTime,
2352
+ clinicTimezone,
2353
+ "date"
2354
+ ),
2355
+ appointmentTime: `${formattedTime} (${timezoneName})`,
2282
2356
  practitionerName: data.appointment.practitionerInfo.name,
2283
2357
  clinicName: data.appointment.clinicInfo.name
2284
2358
  };
@@ -2325,12 +2399,27 @@ var AppointmentMailingService = class extends BaseMailingService {
2325
2399
  );
2326
2400
  throw new Error("Clinic contact email address is missing.");
2327
2401
  }
2402
+ const clinicTimezone = data.appointment.clinic_tz || "UTC";
2403
+ Logger.debug("[AppointmentMailingService] Formatting appointment time for clinic", {
2404
+ clinicTimezone,
2405
+ utcTime: data.appointment.appointmentStartTime.toDate().toISOString()
2406
+ });
2407
+ const formattedTime = this.formatTimestampInClinicTimezone(
2408
+ data.appointment.appointmentStartTime,
2409
+ clinicTimezone,
2410
+ "time"
2411
+ );
2412
+ const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
2328
2413
  const templateVariables = {
2329
2414
  clinicName: data.clinicProfile.name,
2330
2415
  patientName: data.appointment.patientInfo.fullName,
2331
2416
  procedureName: data.appointment.procedureInfo.name,
2332
- appointmentDate: data.appointment.appointmentStartTime.toDate().toLocaleDateString(),
2333
- appointmentTime: data.appointment.appointmentStartTime.toDate().toLocaleTimeString(),
2417
+ appointmentDate: this.formatTimestampInClinicTimezone(
2418
+ data.appointment.appointmentStartTime,
2419
+ clinicTimezone,
2420
+ "date"
2421
+ ),
2422
+ appointmentTime: `${formattedTime} (${timezoneName})`,
2334
2423
  practitionerName: data.appointment.practitionerInfo.name
2335
2424
  };
2336
2425
  const html = this.renderTemplate(clinicAppointmentRequestedTemplate, templateVariables);
@@ -6514,7 +6603,7 @@ var ReviewsAggregationService = class {
6514
6603
 
6515
6604
  // src/admin/booking/booking.calculator.ts
6516
6605
  import { Timestamp } from "firebase/firestore";
6517
- import { DateTime } from "luxon";
6606
+ import { DateTime as DateTime2 } from "luxon";
6518
6607
  var BookingAvailabilityCalculator = class {
6519
6608
  /**
6520
6609
  * Calculate available booking slots based on the provided data
@@ -6605,8 +6694,8 @@ var BookingAvailabilityCalculator = class {
6605
6694
  */
6606
6695
  static createWorkingHoursIntervals(workingHours, startDate, endDate, tz) {
6607
6696
  const workingIntervals = [];
6608
- let start = DateTime.fromMillis(startDate.getTime(), { zone: tz });
6609
- const end = DateTime.fromMillis(endDate.getTime(), { zone: tz });
6697
+ let start = DateTime2.fromMillis(startDate.getTime(), { zone: tz });
6698
+ const end = DateTime2.fromMillis(endDate.getTime(), { zone: tz });
6610
6699
  while (start <= end) {
6611
6700
  const dayOfWeek = start.weekday;
6612
6701
  const dayName = [
@@ -6636,8 +6725,8 @@ var BookingAvailabilityCalculator = class {
6636
6725
  millisecond: 0
6637
6726
  });
6638
6727
  if (workEnd.toMillis() > startDate.getTime() && workStart.toMillis() < endDate.getTime()) {
6639
- const intervalStart = workStart < DateTime.fromMillis(startDate.getTime(), { zone: tz }) ? DateTime.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
6640
- const intervalEnd = workEnd > DateTime.fromMillis(endDate.getTime(), { zone: tz }) ? DateTime.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
6728
+ const intervalStart = workStart < DateTime2.fromMillis(startDate.getTime(), { zone: tz }) ? DateTime2.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
6729
+ const intervalEnd = workEnd > DateTime2.fromMillis(endDate.getTime(), { zone: tz }) ? DateTime2.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
6641
6730
  workingIntervals.push({
6642
6731
  start: Timestamp.fromMillis(intervalStart.toMillis()),
6643
6732
  end: Timestamp.fromMillis(intervalEnd.toMillis())
@@ -6744,8 +6833,8 @@ var BookingAvailabilityCalculator = class {
6744
6833
  */
6745
6834
  static createPractitionerWorkingHoursIntervals(workingHours, startDate, endDate, tz) {
6746
6835
  const workingIntervals = [];
6747
- let start = DateTime.fromMillis(startDate.getTime(), { zone: tz });
6748
- const end = DateTime.fromMillis(endDate.getTime(), { zone: tz });
6836
+ let start = DateTime2.fromMillis(startDate.getTime(), { zone: tz });
6837
+ const end = DateTime2.fromMillis(endDate.getTime(), { zone: tz });
6749
6838
  while (start <= end) {
6750
6839
  const dayOfWeek = start.weekday;
6751
6840
  const dayName = [
@@ -6768,8 +6857,8 @@ var BookingAvailabilityCalculator = class {
6768
6857
  });
6769
6858
  const workEnd = start.set({ hour: endHours, minute: endMinutes });
6770
6859
  if (workEnd.toMillis() > startDate.getTime() && workStart.toMillis() < endDate.getTime()) {
6771
- const intervalStart = workStart < DateTime.fromMillis(startDate.getTime(), { zone: tz }) ? DateTime.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
6772
- const intervalEnd = workEnd > DateTime.fromMillis(endDate.getTime(), { zone: tz }) ? DateTime.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
6860
+ const intervalStart = workStart < DateTime2.fromMillis(startDate.getTime(), { zone: tz }) ? DateTime2.fromMillis(startDate.getTime(), { zone: tz }) : workStart;
6861
+ const intervalEnd = workEnd > DateTime2.fromMillis(endDate.getTime(), { zone: tz }) ? DateTime2.fromMillis(endDate.getTime(), { zone: tz }) : workEnd;
6773
6862
  workingIntervals.push({
6774
6863
  start: Timestamp.fromMillis(intervalStart.toMillis()),
6775
6864
  end: Timestamp.fromMillis(intervalEnd.toMillis())
@@ -6833,7 +6922,7 @@ var BookingAvailabilityCalculator = class {
6833
6922
  for (const interval of intervals) {
6834
6923
  const intervalStart = interval.start.toDate();
6835
6924
  const intervalEnd = interval.end.toDate();
6836
- let slotStart = DateTime.fromMillis(intervalStart.getTime(), { zone: tz });
6925
+ let slotStart = DateTime2.fromMillis(intervalStart.getTime(), { zone: tz });
6837
6926
  const minutesIntoDay = slotStart.hour * 60 + slotStart.minute;
6838
6927
  const minutesRemainder = minutesIntoDay % intervalMinutes;
6839
6928
  if (minutesRemainder > 0) {
@@ -6865,8 +6954,8 @@ var BookingAvailabilityCalculator = class {
6865
6954
  */
6866
6955
  static isSlotFullyAvailable(slotStart, slotEnd, intervals, tz) {
6867
6956
  return intervals.some((interval) => {
6868
- const intervalStart = DateTime.fromMillis(interval.start.toMillis(), { zone: tz });
6869
- const intervalEnd = DateTime.fromMillis(interval.end.toMillis(), { zone: tz });
6957
+ const intervalStart = DateTime2.fromMillis(interval.start.toMillis(), { zone: tz });
6958
+ const intervalEnd = DateTime2.fromMillis(interval.end.toMillis(), { zone: tz });
6870
6959
  return slotStart >= intervalStart && slotEnd <= intervalEnd;
6871
6960
  });
6872
6961
  }
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.57",
4
+ "version": "1.12.58",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -1,4 +1,5 @@
1
1
  import * as admin from 'firebase-admin';
2
+ import { DateTime } from 'luxon';
2
3
  import { BaseMailingService, NewMailgunClient } from '../base.mailing.service';
3
4
  import { Logger } from '../../logger';
4
5
  import { Appointment } from '../../../types/appointment';
@@ -466,6 +467,78 @@ export class AppointmentMailingService extends BaseMailingService {
466
467
  Logger.info('[AppointmentMailingService] Initialized.');
467
468
  }
468
469
 
470
+ /**
471
+ * Formats a Firestore Timestamp in the clinic's timezone
472
+ * @param timestamp - Firestore Timestamp (UTC)
473
+ * @param clinicTimezone - IANA timezone string (e.g., "Europe/Zurich")
474
+ * @param format - Format type: 'date', 'time', or 'datetime'
475
+ * @returns Formatted string in clinic's local timezone
476
+ */
477
+ private formatTimestampInClinicTimezone(
478
+ timestamp: admin.firestore.Timestamp,
479
+ clinicTimezone: string,
480
+ format: 'date' | 'time' | 'datetime' = 'datetime',
481
+ ): string {
482
+ try {
483
+ // Convert UTC timestamp to clinic's timezone
484
+ const dateTimeInClinicTz = DateTime.fromMillis(timestamp.toMillis(), {
485
+ zone: clinicTimezone,
486
+ });
487
+
488
+ // Format based on requested type
489
+ switch (format) {
490
+ case 'date':
491
+ // Format: "Monday, November 5, 2025"
492
+ return dateTimeInClinicTz.toLocaleString(DateTime.DATE_FULL);
493
+ case 'time':
494
+ // Format: "2:30 PM"
495
+ return dateTimeInClinicTz.toLocaleString(DateTime.TIME_SIMPLE);
496
+ case 'datetime':
497
+ // Format: "Monday, November 5, 2025 at 2:30 PM"
498
+ return dateTimeInClinicTz.toLocaleString(DateTime.DATETIME_FULL);
499
+ default:
500
+ return dateTimeInClinicTz.toLocaleString(DateTime.DATETIME_FULL);
501
+ }
502
+ } catch (error) {
503
+ Logger.error('[AppointmentMailingService] Error formatting timestamp in clinic timezone:', {
504
+ error: error instanceof Error ? error.message : String(error),
505
+ clinicTimezone,
506
+ });
507
+ // Fallback to UTC formatting if timezone conversion fails
508
+ return timestamp.toDate().toLocaleString();
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Gets a user-friendly display name for a timezone
514
+ * @param timezone - IANA timezone string (e.g., "Europe/Zurich")
515
+ * @returns User-friendly timezone name (e.g., "Clinic Time - Europe/Zurich")
516
+ */
517
+ private getTimezoneDisplayName(timezone: string): string {
518
+ try {
519
+ // For common timezones, provide a more readable format
520
+ const timezoneMap: Record<string, string> = {
521
+ 'Europe/Zurich': 'Clinic Time - Central European Time',
522
+ 'Europe/London': 'Clinic Time - GMT/BST',
523
+ 'America/New_York': 'Clinic Time - Eastern Time',
524
+ 'America/Los_Angeles': 'Clinic Time - Pacific Time',
525
+ 'Asia/Dubai': 'Clinic Time - Gulf Standard Time',
526
+ 'Asia/Karachi': 'Clinic Time - Pakistan Standard Time',
527
+ 'Asia/Kolkata': 'Clinic Time - India Standard Time',
528
+ 'Australia/Sydney': 'Clinic Time - Australian Eastern Time',
529
+ };
530
+
531
+ // Return mapped name if available, otherwise use the IANA timezone
532
+ return timezoneMap[timezone] || `Clinic Time - ${timezone}`;
533
+ } catch (error) {
534
+ Logger.error('[AppointmentMailingService] Error getting timezone display name:', {
535
+ error: error instanceof Error ? error.message : String(error),
536
+ timezone,
537
+ });
538
+ return `Clinic Time - ${timezone}`;
539
+ }
540
+ }
541
+
469
542
  async sendAppointmentConfirmedEmail(data: AppointmentConfirmationEmailData): Promise<any> {
470
543
  Logger.info(
471
544
  `[AppointmentMailingService] Preparing to send appointment confirmation email to ${data.recipientRole}: ${data.recipientProfile.id}`,
@@ -481,11 +554,31 @@ export class AppointmentMailingService extends BaseMailingService {
481
554
  throw new Error('Recipient email address is missing.');
482
555
  }
483
556
 
557
+ // Get clinic timezone from appointment data, default to UTC if not available
558
+ const clinicTimezone = data.appointment.clinic_tz || 'UTC';
559
+
560
+ Logger.debug('[AppointmentMailingService] Formatting appointment time', {
561
+ clinicTimezone,
562
+ utcTime: data.appointment.appointmentStartTime.toDate().toISOString(),
563
+ });
564
+
565
+ // Format time with timezone label for clarity
566
+ const formattedTime = this.formatTimestampInClinicTimezone(
567
+ data.appointment.appointmentStartTime,
568
+ clinicTimezone,
569
+ 'time',
570
+ );
571
+ const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
572
+
484
573
  const templateVariables = {
485
574
  patientName: data.appointment.patientInfo.fullName,
486
575
  procedureName: data.appointment.procedureInfo.name,
487
- appointmentDate: data.appointment.appointmentStartTime.toDate().toLocaleDateString(),
488
- appointmentTime: data.appointment.appointmentStartTime.toDate().toLocaleTimeString(),
576
+ appointmentDate: this.formatTimestampInClinicTimezone(
577
+ data.appointment.appointmentStartTime,
578
+ clinicTimezone,
579
+ 'date',
580
+ ),
581
+ appointmentTime: `${formattedTime} (${timezoneName})`,
489
582
  practitionerName: data.appointment.practitionerInfo.name,
490
583
  clinicName: data.appointment.clinicInfo.name,
491
584
  };
@@ -539,12 +632,32 @@ export class AppointmentMailingService extends BaseMailingService {
539
632
  throw new Error('Clinic contact email address is missing.');
540
633
  }
541
634
 
635
+ // Get clinic timezone from appointment data, default to UTC if not available
636
+ const clinicTimezone = data.appointment.clinic_tz || 'UTC';
637
+
638
+ Logger.debug('[AppointmentMailingService] Formatting appointment time for clinic', {
639
+ clinicTimezone,
640
+ utcTime: data.appointment.appointmentStartTime.toDate().toISOString(),
641
+ });
642
+
643
+ // Format time with timezone label for clarity
644
+ const formattedTime = this.formatTimestampInClinicTimezone(
645
+ data.appointment.appointmentStartTime,
646
+ clinicTimezone,
647
+ 'time',
648
+ );
649
+ const timezoneName = this.getTimezoneDisplayName(clinicTimezone);
650
+
542
651
  const templateVariables = {
543
652
  clinicName: data.clinicProfile.name,
544
653
  patientName: data.appointment.patientInfo.fullName,
545
654
  procedureName: data.appointment.procedureInfo.name,
546
- appointmentDate: data.appointment.appointmentStartTime.toDate().toLocaleDateString(),
547
- appointmentTime: data.appointment.appointmentStartTime.toDate().toLocaleTimeString(),
655
+ appointmentDate: this.formatTimestampInClinicTimezone(
656
+ data.appointment.appointmentStartTime,
657
+ clinicTimezone,
658
+ 'date',
659
+ ),
660
+ appointmentTime: `${formattedTime} (${timezoneName})`,
548
661
  practitionerName: data.appointment.practitionerInfo.name,
549
662
  };
550
663