@blackcode_sa/metaestetics-api 1.6.14 → 1.6.16

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.
@@ -30,11 +30,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/admin/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ APPOINTMENTS_COLLECTION: () => APPOINTMENTS_COLLECTION,
33
34
  AppointmentAggregationService: () => AppointmentAggregationService,
35
+ AppointmentMailingService: () => AppointmentMailingService,
34
36
  AppointmentStatus: () => AppointmentStatus,
35
37
  BaseMailingService: () => BaseMailingService,
36
38
  BookingAdmin: () => BookingAdmin,
39
+ BookingAvailabilityCalculator: () => BookingAvailabilityCalculator,
40
+ CalendarAdminService: () => CalendarAdminService,
37
41
  ClinicAggregationService: () => ClinicAggregationService,
42
+ DocumentManagerAdminService: () => DocumentManagerAdminService,
43
+ Logger: () => Logger,
44
+ MediaType: () => MediaType,
38
45
  NOTIFICATIONS_COLLECTION: () => NOTIFICATIONS_COLLECTION,
39
46
  NotificationStatus: () => NotificationStatus,
40
47
  NotificationType: () => NotificationType,
@@ -44,6 +51,7 @@ __export(index_exports, {
44
51
  PatientInstructionStatus: () => PatientInstructionStatus,
45
52
  PatientRequirementOverallStatus: () => PatientRequirementOverallStatus,
46
53
  PatientRequirementsAdminService: () => PatientRequirementsAdminService,
54
+ PaymentStatus: () => PaymentStatus,
47
55
  PractitionerAggregationService: () => PractitionerAggregationService,
48
56
  PractitionerInviteMailingService: () => PractitionerInviteMailingService,
49
57
  PractitionerTokenStatus: () => PractitionerTokenStatus,
@@ -84,7 +92,7 @@ var NotificationStatus = /* @__PURE__ */ ((NotificationStatus2) => {
84
92
  })(NotificationStatus || {});
85
93
 
86
94
  // src/admin/notifications/notifications.admin.ts
87
- var admin2 = __toESM(require("firebase-admin"));
95
+ var admin = __toESM(require("firebase-admin"));
88
96
  var import_expo_server_sdk = require("expo-server-sdk");
89
97
 
90
98
  // src/types/appointment/index.ts
@@ -101,6 +109,21 @@ var AppointmentStatus = /* @__PURE__ */ ((AppointmentStatus2) => {
101
109
  AppointmentStatus2["RESCHEDULED_BY_CLINIC"] = "rescheduled_by_clinic";
102
110
  return AppointmentStatus2;
103
111
  })(AppointmentStatus || {});
112
+ var PaymentStatus = /* @__PURE__ */ ((PaymentStatus2) => {
113
+ PaymentStatus2["UNPAID"] = "unpaid";
114
+ PaymentStatus2["PAID"] = "paid";
115
+ PaymentStatus2["PARTIALLY_PAID"] = "partially_paid";
116
+ PaymentStatus2["REFUNDED"] = "refunded";
117
+ PaymentStatus2["NOT_APPLICABLE"] = "not_applicable";
118
+ return PaymentStatus2;
119
+ })(PaymentStatus || {});
120
+ var MediaType = /* @__PURE__ */ ((MediaType2) => {
121
+ MediaType2["BEFORE_PHOTO"] = "before_photo";
122
+ MediaType2["AFTER_PHOTO"] = "after_photo";
123
+ MediaType2["CONSENT_SCAN"] = "consent_scan";
124
+ MediaType2["OTHER_DOCUMENT"] = "other_document";
125
+ return MediaType2;
126
+ })(MediaType || {});
104
127
  var APPOINTMENTS_COLLECTION = "appointments";
105
128
 
106
129
  // src/types/documentation-templates/index.ts
@@ -119,109 +142,61 @@ var UserRole = /* @__PURE__ */ ((UserRole2) => {
119
142
  return UserRole2;
120
143
  })(UserRole || {});
121
144
 
122
- // src/utils/TimestampUtils.ts
123
- var admin = __toESM(require("firebase-admin"));
124
- var import_firestore = require("firebase/firestore");
125
- var TimestampUtils = class {
126
- /**
127
- * Converts an admin Firestore Timestamp to a client Firestore Timestamp
128
- * @param adminTimestamp - Admin SDK Timestamp (from firebase-admin)
129
- * @returns A client SDK Timestamp (from firebase/firestore) with same seconds/nanoseconds or null if input is null
130
- */
131
- static adminToClient(adminTimestamp) {
132
- if (!adminTimestamp) return null;
133
- return new import_firestore.Timestamp(
134
- adminTimestamp.seconds,
135
- adminTimestamp.nanoseconds
136
- );
137
- }
138
- /**
139
- * Converts a client Firestore Timestamp to an admin Firestore Timestamp
140
- * @param clientTimestamp - Client SDK Timestamp (from firebase/firestore)
141
- * @returns An admin SDK Timestamp (from firebase-admin) with same seconds/nanoseconds or null if input is null
142
- */
143
- static clientToAdmin(clientTimestamp) {
144
- if (!clientTimestamp) return null;
145
- return new admin.firestore.Timestamp(
146
- clientTimestamp.seconds,
147
- clientTimestamp.nanoseconds
148
- );
149
- }
150
- /**
151
- * Creates a client-compatible timestamp for the current time
152
- * @returns A client SDK Timestamp (from firebase/firestore) for the current time
153
- */
154
- static nowAsClient() {
155
- const now = admin.firestore.Timestamp.now();
156
- return this.adminToClient(now);
157
- }
145
+ // src/admin/logger/index.ts
146
+ var firebaseFunctionsLogger;
147
+ try {
148
+ firebaseFunctionsLogger = require("firebase-functions/logger");
149
+ require("firebase-functions/logger/compat");
150
+ } catch (e) {
151
+ }
152
+ var Logger = class {
158
153
  /**
159
- * Converts a Date object to a client Firestore Timestamp
160
- * @param date - JavaScript Date object
161
- * @returns A client SDK Timestamp (from firebase/firestore) or null if input is null
154
+ * Log an error message
155
+ * @param message Message to log
156
+ * @param data Optional data to include
162
157
  */
163
- static dateToClientTimestamp(date) {
164
- if (!date) return null;
165
- return import_firestore.Timestamp.fromDate(date);
158
+ static error(message, data) {
159
+ if (firebaseFunctionsLogger) {
160
+ firebaseFunctionsLogger.error(message, data);
161
+ } else {
162
+ console.error(message, data !== void 0 ? data : "");
163
+ }
166
164
  }
167
165
  /**
168
- * Converts a Date object to an admin Firestore Timestamp
169
- * @param date - JavaScript Date object
170
- * @returns An admin SDK Timestamp (from firebase-admin) or null if input is null
166
+ * Log a warning message
167
+ * @param message Message to log
168
+ * @param data Optional data to include
171
169
  */
172
- static dateToAdminTimestamp(date) {
173
- if (!date) return null;
174
- return admin.firestore.Timestamp.fromDate(date);
170
+ static warn(message, data) {
171
+ if (firebaseFunctionsLogger) {
172
+ firebaseFunctionsLogger.warn(message, data);
173
+ } else {
174
+ console.warn(message, data !== void 0 ? data : "");
175
+ }
175
176
  }
176
177
  /**
177
- * Deeply converts all admin Firestore Timestamps in an object to client Firestore Timestamps
178
- * @param obj - Object that may contain admin Firestore Timestamps
179
- * @returns The same object with all admin Timestamps converted to client Timestamps
178
+ * Log an info message
179
+ * @param message Message to log
180
+ * @param data Optional data to include
180
181
  */
181
- static convertObjectTimestampsAdminToClient(obj) {
182
- if (!obj || typeof obj !== "object") {
183
- return obj;
184
- }
185
- if (obj instanceof admin.firestore.Timestamp) {
186
- return this.adminToClient(obj);
187
- }
188
- if (Array.isArray(obj)) {
189
- return obj.map(
190
- (item) => this.convertObjectTimestampsAdminToClient(item)
191
- );
192
- }
193
- const result = { ...obj };
194
- for (const key in result) {
195
- if (Object.prototype.hasOwnProperty.call(result, key)) {
196
- result[key] = this.convertObjectTimestampsAdminToClient(result[key]);
197
- }
182
+ static info(message, data) {
183
+ if (firebaseFunctionsLogger) {
184
+ firebaseFunctionsLogger.info(message, data);
185
+ } else {
186
+ console.info(message, data !== void 0 ? data : "");
198
187
  }
199
- return result;
200
188
  }
201
189
  /**
202
- * Deeply converts all client Firestore Timestamps in an object to admin Firestore Timestamps
203
- * @param obj - Object that may contain client Firestore Timestamps
204
- * @returns The same object with all client Timestamps converted to admin Timestamps
190
+ * Log a debug message
191
+ * @param message Message to log
192
+ * @param data Optional data to include
205
193
  */
206
- static convertObjectTimestampsClientToAdmin(obj) {
207
- if (!obj || typeof obj !== "object") {
208
- return obj;
209
- }
210
- if (obj instanceof import_firestore.Timestamp) {
211
- return this.clientToAdmin(obj);
212
- }
213
- if (Array.isArray(obj)) {
214
- return obj.map(
215
- (item) => this.convertObjectTimestampsClientToAdmin(item)
216
- );
217
- }
218
- const result = { ...obj };
219
- for (const key in result) {
220
- if (Object.prototype.hasOwnProperty.call(result, key)) {
221
- result[key] = this.convertObjectTimestampsClientToAdmin(result[key]);
222
- }
194
+ static debug(message, data) {
195
+ if (firebaseFunctionsLogger) {
196
+ firebaseFunctionsLogger.debug(message, data);
197
+ } else {
198
+ console.debug(message, data !== void 0 ? data : "");
223
199
  }
224
- return result;
225
200
  }
226
201
  };
227
202
 
@@ -229,7 +204,7 @@ var TimestampUtils = class {
229
204
  var NotificationsAdmin = class {
230
205
  constructor(firestore12) {
231
206
  this.expo = new import_expo_server_sdk.Expo();
232
- this.db = firestore12 || admin2.firestore();
207
+ this.db = firestore12 || admin.firestore();
233
208
  }
234
209
  /**
235
210
  * Dohvata notifikaciju po ID-u
@@ -245,8 +220,8 @@ var NotificationsAdmin = class {
245
220
  const docRef = await this.db.collection("notifications").add({
246
221
  ...notification,
247
222
  status: "pending" /* PENDING */,
248
- createdAt: admin2.firestore.FieldValue.serverTimestamp(),
249
- updatedAt: admin2.firestore.FieldValue.serverTimestamp()
223
+ createdAt: admin.firestore.FieldValue.serverTimestamp(),
224
+ updatedAt: admin.firestore.FieldValue.serverTimestamp()
250
225
  });
251
226
  return docRef.id;
252
227
  }
@@ -254,7 +229,18 @@ var NotificationsAdmin = class {
254
229
  * Priprema Expo poruku za slanje
255
230
  */
256
231
  prepareExpoMessage(notification) {
257
- return notification.notificationTokens.filter((token) => import_expo_server_sdk.Expo.isExpoPushToken(token)).map((token) => ({
232
+ const validTokens = notification.notificationTokens.filter(
233
+ (token) => import_expo_server_sdk.Expo.isExpoPushToken(token)
234
+ );
235
+ Logger.info(
236
+ `[NotificationsAdmin] Preparing Expo messages for notification ${notification.id}`,
237
+ {
238
+ totalTokens: notification.notificationTokens.length,
239
+ validTokens: validTokens.length,
240
+ invalidTokensCount: notification.notificationTokens.length - validTokens.length
241
+ }
242
+ );
243
+ return validTokens.map((token) => ({
258
244
  to: token,
259
245
  sound: "default",
260
246
  title: notification.title,
@@ -272,10 +258,10 @@ var NotificationsAdmin = class {
272
258
  async updateNotificationStatus(notificationId, status, error) {
273
259
  const update = {
274
260
  status,
275
- updatedAt: admin2.firestore.FieldValue.serverTimestamp()
261
+ updatedAt: admin.firestore.FieldValue.serverTimestamp()
276
262
  };
277
263
  if (status === "sent" /* SENT */) {
278
- update.sentAt = admin2.firestore.FieldValue.serverTimestamp();
264
+ update.sentAt = admin.firestore.FieldValue.serverTimestamp();
279
265
  }
280
266
  if (error) {
281
267
  update.error = error;
@@ -286,45 +272,94 @@ var NotificationsAdmin = class {
286
272
  * Šalje notifikaciju kroz Expo servis sa boljim error handlingom
287
273
  */
288
274
  async sendPushNotification(notification) {
275
+ var _a;
289
276
  try {
277
+ Logger.info(
278
+ `[NotificationsAdmin] Processing notification ${notification.id} for sending`,
279
+ {
280
+ userId: notification.userId,
281
+ tokenCount: ((_a = notification.notificationTokens) == null ? void 0 : _a.length) || 0,
282
+ type: notification.notificationType
283
+ }
284
+ );
290
285
  const messages = this.prepareExpoMessage(notification);
291
286
  if (messages.length === 0) {
287
+ const errorMsg = "No valid notification tokens found";
288
+ Logger.error(
289
+ `[NotificationsAdmin] ${errorMsg} for notification ${notification.id}`
290
+ );
292
291
  await this.updateNotificationStatus(
293
292
  notification.id,
294
293
  "failed" /* FAILED */,
295
- "No valid notification tokens found"
294
+ errorMsg
296
295
  );
297
296
  return false;
298
297
  }
299
298
  const chunks = this.expo.chunkPushNotifications(messages);
299
+ Logger.info(
300
+ `[NotificationsAdmin] Sending ${messages.length} messages in ${chunks.length} chunks for notification ${notification.id}`
301
+ );
300
302
  const tickets = [];
301
- for (const chunk of chunks) {
303
+ for (let i = 0; i < chunks.length; i++) {
304
+ const chunk = chunks[i];
302
305
  try {
306
+ Logger.info(
307
+ `[NotificationsAdmin] Sending chunk ${i + 1}/${chunks.length} with ${chunk.length} messages`
308
+ );
303
309
  const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
310
+ Logger.info(
311
+ `[NotificationsAdmin] Received ${ticketChunk.length} tickets for chunk ${i + 1}`
312
+ );
304
313
  tickets.push(ticketChunk);
305
314
  } catch (error) {
306
- console.error(`Chunk sending error:`, error);
315
+ Logger.error(
316
+ `[NotificationsAdmin] Chunk ${i + 1} sending error:`,
317
+ error
318
+ );
307
319
  throw error;
308
320
  }
309
321
  }
310
322
  let hasErrors = false;
311
323
  const errors = [];
312
- tickets.flat().forEach((ticket, index) => {
324
+ const ticketsFlat = tickets.flat();
325
+ const ticketResults = {
326
+ total: ticketsFlat.length,
327
+ success: 0,
328
+ error: 0,
329
+ errorDetails: {}
330
+ };
331
+ ticketsFlat.forEach((ticket, index) => {
313
332
  if (ticket.status === "error") {
314
333
  hasErrors = true;
315
- errors.push(
316
- `Token ${notification.notificationTokens[index]}: ${ticket.message}`
317
- );
334
+ ticketResults.error++;
335
+ const errorMessage = ticket.message || "Unknown error";
336
+ ticketResults.errorDetails[errorMessage] = (ticketResults.errorDetails[errorMessage] || 0) + 1;
337
+ const tokenInfo = index < notification.notificationTokens.length ? `Token ${notification.notificationTokens[index]}` : `Token at index ${index}`;
338
+ errors.push(`${tokenInfo}: ${errorMessage}`);
339
+ } else {
340
+ ticketResults.success++;
318
341
  }
319
342
  });
343
+ Logger.info(
344
+ `[NotificationsAdmin] Ticket results for notification ${notification.id}`,
345
+ ticketResults
346
+ );
320
347
  if (hasErrors) {
348
+ const errorSummary = errors.join("; ");
349
+ Logger.warn(
350
+ `[NotificationsAdmin] Partial success or errors in notification ${notification.id}`,
351
+ { errorCount: errors.length, errorSummary }
352
+ );
321
353
  await this.updateNotificationStatus(
322
354
  notification.id,
323
- "partialSuccess" /* PARTIAL_SUCCESS */,
324
- errors.join("; ")
355
+ ticketResults.success > 0 ? "partialSuccess" /* PARTIAL_SUCCESS */ : "failed" /* FAILED */,
356
+ errorSummary
325
357
  );
326
- return false;
358
+ return ticketResults.success > 0;
327
359
  }
360
+ Logger.info(
361
+ `[NotificationsAdmin] Successfully sent notification ${notification.id} to all recipients`
362
+ );
328
363
  await this.updateNotificationStatus(
329
364
  notification.id,
330
365
  "sent" /* SENT */
@@ -332,7 +367,10 @@ var NotificationsAdmin = class {
332
367
  return true;
333
368
  } catch (error) {
334
369
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
335
- console.error("Gre\u0161ka pri slanju notifikacije:", error);
370
+ Logger.error(
371
+ `[NotificationsAdmin] Critical error sending notification ${notification.id}:`,
372
+ error
373
+ );
336
374
  await this.updateNotificationStatus(
337
375
  notification.id,
338
376
  "failed" /* FAILED */,
@@ -345,14 +383,29 @@ var NotificationsAdmin = class {
345
383
  * Procesira notifikacije koje čekaju na slanje sa batch limitom
346
384
  */
347
385
  async processPendingNotifications(batchSize = 100) {
348
- const now = admin2.firestore.Timestamp.now();
386
+ const now = admin.firestore.Timestamp.now();
387
+ Logger.info(
388
+ `[NotificationsAdmin] Starting to process pending notifications with batch size ${batchSize}`
389
+ );
349
390
  const pendingNotifications = await this.db.collection("notifications").where("status", "==", "pending" /* PENDING */).where("notificationTime", "<=", now).limit(batchSize).get();
391
+ Logger.info(
392
+ `[NotificationsAdmin] Found ${pendingNotifications.size} pending notifications to process`
393
+ );
394
+ if (pendingNotifications.empty) {
395
+ Logger.info(
396
+ "[NotificationsAdmin] No pending notifications found to process"
397
+ );
398
+ return;
399
+ }
350
400
  const results = await Promise.allSettled(
351
401
  pendingNotifications.docs.map(async (doc) => {
352
402
  const notification = {
353
403
  id: doc.id,
354
404
  ...doc.data()
355
405
  };
406
+ Logger.info(
407
+ `[NotificationsAdmin] Processing notification ${notification.id} of type ${notification.notificationType}`
408
+ );
356
409
  return this.sendPushNotification(notification);
357
410
  })
358
411
  );
@@ -362,8 +415,8 @@ var NotificationsAdmin = class {
362
415
  const failed = results.filter(
363
416
  (r) => r.status === "rejected" || r.status === "fulfilled" && !r.value
364
417
  ).length;
365
- console.log(
366
- `Processed ${results.length} notifications: ${successful} successful, ${failed} failed`
418
+ Logger.info(
419
+ `[NotificationsAdmin] Processed ${results.length} notifications: ${successful} successful, ${failed} failed`
367
420
  );
368
421
  }
369
422
  /**
@@ -372,13 +425,20 @@ var NotificationsAdmin = class {
372
425
  async cleanupOldNotifications(daysOld = 30, batchSize = 500) {
373
426
  const cutoffDate = /* @__PURE__ */ new Date();
374
427
  cutoffDate.setDate(cutoffDate.getDate() - daysOld);
428
+ Logger.info(
429
+ `[NotificationsAdmin] Starting cleanup of notifications older than ${daysOld} days`
430
+ );
431
+ let totalDeleted = 0;
375
432
  while (true) {
376
433
  const oldNotifications = await this.db.collection("notifications").where(
377
434
  "createdAt",
378
435
  "<=",
379
- admin2.firestore.Timestamp.fromDate(cutoffDate)
436
+ admin.firestore.Timestamp.fromDate(cutoffDate)
380
437
  ).limit(batchSize).get();
381
438
  if (oldNotifications.empty) {
439
+ Logger.info(
440
+ `[NotificationsAdmin] No more old notifications to delete. Total deleted: ${totalDeleted}`
441
+ );
382
442
  break;
383
443
  }
384
444
  const batch = this.db.batch();
@@ -386,8 +446,9 @@ var NotificationsAdmin = class {
386
446
  batch.delete(doc.ref);
387
447
  });
388
448
  await batch.commit();
389
- console.log(
390
- `Deleted batch of ${oldNotifications.size} old notifications`
449
+ totalDeleted += oldNotifications.size;
450
+ Logger.info(
451
+ `[NotificationsAdmin] Deleted batch of ${oldNotifications.size} old notifications. Running total: ${totalDeleted}`
391
452
  );
392
453
  }
393
454
  }
@@ -418,15 +479,12 @@ var NotificationsAdmin = class {
418
479
  title = "New Appointment Confirmed";
419
480
  body = `Appointment for ${appointment.procedureInfo.name} with ${appointment.patientInfo.fullName} on ${appointment.appointmentStartTime.toDate().toLocaleDateString()} is confirmed.`;
420
481
  }
421
- const adminTsNow = admin2.firestore.Timestamp.now();
422
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
423
- adminTsNow
424
- );
482
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
425
483
  const notificationData = {
426
484
  userId: recipientUserId,
427
485
  userRole: recipientRole,
428
486
  notificationType: "appointmentStatusChange" /* APPOINTMENT_STATUS_CHANGE */,
429
- notificationTime: clientCompatibleNotificationTime,
487
+ notificationTime: notificationTimestampForDb,
430
488
  notificationTokens: recipientExpoTokens,
431
489
  title,
432
490
  body,
@@ -472,15 +530,12 @@ var NotificationsAdmin = class {
472
530
  body += ` Reason: ${appointment.cancellationReason}`;
473
531
  }
474
532
  }
475
- const adminTsNow = admin2.firestore.Timestamp.now();
476
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
477
- adminTsNow
478
- );
533
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
479
534
  const notificationData = {
480
535
  userId: recipientUserId,
481
536
  userRole: recipientRole,
482
537
  notificationType: "appointmentCancelled" /* APPOINTMENT_CANCELLED */,
483
- notificationTime: clientCompatibleNotificationTime,
538
+ notificationTime: notificationTimestampForDb,
484
539
  notificationTokens: recipientExpoTokens,
485
540
  title,
486
541
  body,
@@ -511,15 +566,12 @@ var NotificationsAdmin = class {
511
566
  }
512
567
  const title = "Appointment Reschedule Proposed";
513
568
  const body = `Action Required: A new time has been proposed for your appointment for ${appointment.procedureInfo.name}. Please review in the app.`;
514
- const adminTsNow = admin2.firestore.Timestamp.now();
515
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
516
- adminTsNow
517
- );
569
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
518
570
  const notificationData = {
519
571
  userId: patientUserId,
520
572
  userRole: "patient" /* PATIENT */,
521
573
  notificationType: "appointmentRescheduledProposal" /* APPOINTMENT_RESCHEDULED_PROPOSAL */,
522
- notificationTime: clientCompatibleNotificationTime,
574
+ notificationTime: notificationTimestampForDb,
523
575
  notificationTokens: patientExpoTokens,
524
576
  title,
525
577
  body,
@@ -550,16 +602,13 @@ var NotificationsAdmin = class {
550
602
  }
551
603
  const title = "Payment Updated";
552
604
  const body = `Your payment status for the appointment (${appointment.procedureInfo.name}) on ${appointment.appointmentStartTime.toDate().toLocaleDateString()} is now ${appointment.paymentStatus}.`;
553
- const adminTsNow = admin2.firestore.Timestamp.now();
554
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
555
- adminTsNow
556
- );
605
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
557
606
  const notificationType = appointment.paymentStatus === "paid" /* PAID */ ? "paymentConfirmation" /* PAYMENT_CONFIRMATION */ : "generalMessage" /* GENERAL_MESSAGE */;
558
607
  const notificationData = {
559
608
  userId: patientUserId,
560
609
  userRole: "patient" /* PATIENT */,
561
610
  notificationType,
562
- notificationTime: clientCompatibleNotificationTime,
611
+ notificationTime: notificationTimestampForDb,
563
612
  notificationTokens: patientExpoTokens,
564
613
  title,
565
614
  body,
@@ -590,15 +639,12 @@ var NotificationsAdmin = class {
590
639
  }
591
640
  const title = "Leave a Review";
592
641
  const body = `How was your recent appointment for ${appointment.procedureInfo.name}? We'd love to hear your feedback!`;
593
- const adminTsNow = admin2.firestore.Timestamp.now();
594
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
595
- adminTsNow
596
- );
642
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
597
643
  const notificationData = {
598
644
  userId: patientUserId,
599
645
  userRole: "patient" /* PATIENT */,
600
646
  notificationType: "reviewRequest" /* REVIEW_REQUEST */,
601
- notificationTime: clientCompatibleNotificationTime,
647
+ notificationTime: notificationTimestampForDb,
602
648
  notificationTokens: patientExpoTokens,
603
649
  title,
604
650
  body,
@@ -635,16 +681,13 @@ var NotificationsAdmin = class {
635
681
  }
636
682
  const title = "New Review Received";
637
683
  const body = `A new review has been added by ${appointment.patientInfo.fullName} for your appointment on ${appointment.appointmentStartTime.toDate().toLocaleDateString()}.`;
638
- const adminTsNow = admin2.firestore.Timestamp.now();
639
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
640
- adminTsNow
641
- );
684
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
642
685
  const tempNotificationType = "generalMessage" /* GENERAL_MESSAGE */;
643
686
  const notificationData = {
644
687
  userId: recipientUserId,
645
688
  userRole: "practitioner" /* PRACTITIONER */,
646
689
  notificationType: tempNotificationType,
647
- notificationTime: clientCompatibleNotificationTime,
690
+ notificationTime: notificationTimestampForDb,
648
691
  notificationTokens: recipientExpoTokens,
649
692
  title,
650
693
  body,
@@ -668,6 +711,187 @@ var NotificationsAdmin = class {
668
711
  }
669
712
  };
670
713
 
714
+ // src/utils/TimestampUtils.ts
715
+ var admin2 = __toESM(require("firebase-admin"));
716
+ var import_firestore = require("firebase/firestore");
717
+ var IS_SERVER_ENV = process.env.NODE_ENV === "production" || process.env.FUNCTIONS_EMULATOR === "true" || process.env.FIREBASE_CONFIG !== void 0;
718
+ var TimestampUtils = class {
719
+ /**
720
+ * Enables server mode where admin timestamps are preserved when saving
721
+ * to Firestore via the admin SDK
722
+ */
723
+ static enableServerMode() {
724
+ this.serverMode = true;
725
+ }
726
+ /**
727
+ * Disables server mode - use this only for client-side or mixed environments
728
+ */
729
+ static disableServerMode() {
730
+ this.serverMode = false;
731
+ }
732
+ /**
733
+ * Converts an admin Firestore Timestamp to a client Firestore Timestamp
734
+ * In server mode, returns the original admin timestamp for storage consistency
735
+ *
736
+ * @param adminTimestamp - Admin SDK Timestamp (from firebase-admin)
737
+ * @returns A client SDK Timestamp (from firebase/firestore) with same seconds/nanoseconds,
738
+ * or the original admin timestamp in server mode,
739
+ * or null if input is null
740
+ */
741
+ static adminToClient(adminTimestamp) {
742
+ if (!adminTimestamp) return null;
743
+ if (this.serverMode) {
744
+ return adminTimestamp;
745
+ }
746
+ return new import_firestore.Timestamp(
747
+ adminTimestamp.seconds,
748
+ adminTimestamp.nanoseconds
749
+ );
750
+ }
751
+ /**
752
+ * Converts a client Firestore Timestamp to an admin Firestore Timestamp
753
+ * @param clientTimestamp - Client SDK Timestamp (from firebase/firestore)
754
+ * @returns An admin SDK Timestamp (from firebase-admin) with same seconds/nanoseconds or null if input is null
755
+ */
756
+ static clientToAdmin(clientTimestamp) {
757
+ if (!clientTimestamp) return null;
758
+ return new admin2.firestore.Timestamp(
759
+ clientTimestamp.seconds,
760
+ clientTimestamp.nanoseconds
761
+ );
762
+ }
763
+ /**
764
+ * Creates a timestamp for the current time in the appropriate format based on environment
765
+ * @returns A timestamp for the current time (admin timestamp in server mode, client in client mode)
766
+ */
767
+ static nowAsTimestamp() {
768
+ const now = admin2.firestore.Timestamp.now();
769
+ if (this.serverMode) {
770
+ return now;
771
+ }
772
+ return this.adminToClient(now);
773
+ }
774
+ /**
775
+ * @deprecated Use nowAsTimestamp() instead for better cross-environment compatibility
776
+ */
777
+ static nowAsClient() {
778
+ const now = admin2.firestore.Timestamp.now();
779
+ return this.adminToClient(now);
780
+ }
781
+ /**
782
+ * Converts a Date object to a timestamp in the appropriate format based on environment
783
+ * @param date - JavaScript Date object
784
+ * @returns A timestamp (admin timestamp in server mode, client in client mode) or null if input is null
785
+ */
786
+ static dateToTimestamp(date) {
787
+ if (!date) return null;
788
+ if (this.serverMode) {
789
+ return admin2.firestore.Timestamp.fromDate(date);
790
+ }
791
+ return import_firestore.Timestamp.fromDate(date);
792
+ }
793
+ /**
794
+ * @deprecated Use dateToTimestamp() instead for better cross-environment compatibility
795
+ */
796
+ static dateToClientTimestamp(date) {
797
+ if (!date) return null;
798
+ return import_firestore.Timestamp.fromDate(date);
799
+ }
800
+ /**
801
+ * @deprecated Use dateToTimestamp() instead for better cross-environment compatibility
802
+ */
803
+ static dateToAdminTimestamp(date) {
804
+ if (!date) return null;
805
+ return admin2.firestore.Timestamp.fromDate(date);
806
+ }
807
+ /**
808
+ * Gets a server timestamp field value for use in create/update operations
809
+ * Works in both admin and client environments
810
+ */
811
+ static serverTimestamp() {
812
+ if (this.serverMode) {
813
+ return admin2.firestore.FieldValue.serverTimestamp();
814
+ }
815
+ throw new Error("Server timestamp in client mode not implemented");
816
+ }
817
+ /**
818
+ * For objects with mixed timestamp types, ensures all timestamps are
819
+ * in the correct format for the current environment
820
+ */
821
+ static normalizeTimestamps(obj) {
822
+ if (!obj || typeof obj !== "object") {
823
+ return obj;
824
+ }
825
+ if (obj instanceof admin2.firestore.Timestamp) {
826
+ return this.serverMode ? obj : this.adminToClient(obj);
827
+ }
828
+ if (obj instanceof import_firestore.Timestamp && this.serverMode) {
829
+ return this.clientToAdmin(obj);
830
+ }
831
+ if (Array.isArray(obj)) {
832
+ return obj.map((item) => this.normalizeTimestamps(item));
833
+ }
834
+ const result = { ...obj };
835
+ for (const key in result) {
836
+ if (Object.prototype.hasOwnProperty.call(result, key)) {
837
+ result[key] = this.normalizeTimestamps(result[key]);
838
+ }
839
+ }
840
+ return result;
841
+ }
842
+ /**
843
+ * @deprecated Use normalizeTimestamps() instead for better cross-environment compatibility
844
+ */
845
+ static convertObjectTimestampsAdminToClient(obj) {
846
+ if (!obj || typeof obj !== "object") {
847
+ return obj;
848
+ }
849
+ if (obj instanceof admin2.firestore.Timestamp) {
850
+ return this.adminToClient(obj);
851
+ }
852
+ if (Array.isArray(obj)) {
853
+ return obj.map(
854
+ (item) => this.convertObjectTimestampsAdminToClient(item)
855
+ );
856
+ }
857
+ const result = { ...obj };
858
+ for (const key in result) {
859
+ if (Object.prototype.hasOwnProperty.call(result, key)) {
860
+ result[key] = this.convertObjectTimestampsAdminToClient(result[key]);
861
+ }
862
+ }
863
+ return result;
864
+ }
865
+ /**
866
+ * @deprecated Use normalizeTimestamps() instead for better cross-environment compatibility
867
+ */
868
+ static convertObjectTimestampsClientToAdmin(obj) {
869
+ if (!obj || typeof obj !== "object") {
870
+ return obj;
871
+ }
872
+ if (obj instanceof import_firestore.Timestamp) {
873
+ return this.clientToAdmin(obj);
874
+ }
875
+ if (Array.isArray(obj)) {
876
+ return obj.map(
877
+ (item) => this.convertObjectTimestampsClientToAdmin(item)
878
+ );
879
+ }
880
+ const result = { ...obj };
881
+ for (const key in result) {
882
+ if (Object.prototype.hasOwnProperty.call(result, key)) {
883
+ result[key] = this.convertObjectTimestampsClientToAdmin(result[key]);
884
+ }
885
+ }
886
+ return result;
887
+ }
888
+ };
889
+ /**
890
+ * Flag to force server mode where admin timestamps are preserved
891
+ * This should be true in Cloud Functions environments
892
+ */
893
+ TimestampUtils.serverMode = IS_SERVER_ENV;
894
+
671
895
  // src/admin/aggregation/clinic/clinic.aggregation.service.ts
672
896
  var admin3 = __toESM(require("firebase-admin"));
673
897
 
@@ -2128,7 +2352,7 @@ var PatientRequirementsAdminService = class {
2128
2352
  userRole: "patient" /* PATIENT */,
2129
2353
  notificationType: "requirementInstructionDue" /* REQUIREMENT_INSTRUCTION_DUE */,
2130
2354
  notificationTime: currentInstruction.dueTime,
2131
- // This is a Firestore Timestamp
2355
+ // dueTime should be an admin.firestore.Timestamp already
2132
2356
  notificationTokens: patientExpoTokens,
2133
2357
  title: `Reminder: ${instance.requirementName}`,
2134
2358
  body: currentInstruction.instructionText,
@@ -2356,66 +2580,6 @@ var PatientRequirementsAdminService = class {
2356
2580
 
2357
2581
  // src/admin/calendar/calendar.admin.service.ts
2358
2582
  var admin8 = __toESM(require("firebase-admin"));
2359
-
2360
- // src/admin/logger/index.ts
2361
- var firebaseFunctionsLogger;
2362
- try {
2363
- firebaseFunctionsLogger = require("firebase-functions/logger");
2364
- require("firebase-functions/logger/compat");
2365
- } catch (e) {
2366
- }
2367
- var Logger = class {
2368
- /**
2369
- * Log an error message
2370
- * @param message Message to log
2371
- * @param data Optional data to include
2372
- */
2373
- static error(message, data) {
2374
- if (firebaseFunctionsLogger) {
2375
- firebaseFunctionsLogger.error(message, data);
2376
- } else {
2377
- console.error(message, data !== void 0 ? data : "");
2378
- }
2379
- }
2380
- /**
2381
- * Log a warning message
2382
- * @param message Message to log
2383
- * @param data Optional data to include
2384
- */
2385
- static warn(message, data) {
2386
- if (firebaseFunctionsLogger) {
2387
- firebaseFunctionsLogger.warn(message, data);
2388
- } else {
2389
- console.warn(message, data !== void 0 ? data : "");
2390
- }
2391
- }
2392
- /**
2393
- * Log an info message
2394
- * @param message Message to log
2395
- * @param data Optional data to include
2396
- */
2397
- static info(message, data) {
2398
- if (firebaseFunctionsLogger) {
2399
- firebaseFunctionsLogger.info(message, data);
2400
- } else {
2401
- console.info(message, data !== void 0 ? data : "");
2402
- }
2403
- }
2404
- /**
2405
- * Log a debug message
2406
- * @param message Message to log
2407
- * @param data Optional data to include
2408
- */
2409
- static debug(message, data) {
2410
- if (firebaseFunctionsLogger) {
2411
- firebaseFunctionsLogger.debug(message, data);
2412
- } else {
2413
- console.debug(message, data !== void 0 ? data : "");
2414
- }
2415
- }
2416
- };
2417
-
2418
- // src/admin/calendar/calendar.admin.service.ts
2419
2583
  var CalendarAdminService = class {
2420
2584
  constructor(firestore12) {
2421
2585
  this.db = firestore12 || admin8.firestore();
@@ -2470,14 +2634,8 @@ var CalendarAdminService = class {
2470
2634
  const batch = this.db.batch();
2471
2635
  const serverTimestamp = admin8.firestore.FieldValue.serverTimestamp();
2472
2636
  const firestoreCompatibleEventTime = {
2473
- start: {
2474
- seconds: newEventTime.start.seconds,
2475
- nanoseconds: newEventTime.start.nanoseconds
2476
- },
2477
- end: {
2478
- seconds: newEventTime.end.seconds,
2479
- nanoseconds: newEventTime.end.nanoseconds
2480
- }
2637
+ start: TimestampUtils.clientToAdmin(newEventTime.start) || admin8.firestore.Timestamp.now(),
2638
+ end: TimestampUtils.clientToAdmin(newEventTime.end) || admin8.firestore.Timestamp.now()
2481
2639
  };
2482
2640
  if (appointment.calendarEventId) {
2483
2641
  const practitionerEventRef = this.db.doc(
@@ -3225,7 +3383,8 @@ var AppointmentAggregationService = class {
3225
3383
  status: "pendingNotification" /* PENDING_NOTIFICATION */,
3226
3384
  originalNotifyAtValue: notifyAtValue,
3227
3385
  originalTimeframeUnit: template.timeframe.unit,
3228
- updatedAt: admin10.firestore.FieldValue.serverTimestamp(),
3386
+ updatedAt: admin10.firestore.Timestamp.now(),
3387
+ // Use current server timestamp
3229
3388
  notificationId: void 0,
3230
3389
  actionTakenAt: void 0
3231
3390
  };
@@ -3335,7 +3494,8 @@ var AppointmentAggregationService = class {
3335
3494
  status: "pendingNotification" /* PENDING_NOTIFICATION */,
3336
3495
  originalNotifyAtValue: notifyAtValue,
3337
3496
  originalTimeframeUnit: template.timeframe.unit,
3338
- updatedAt: admin10.firestore.FieldValue.serverTimestamp(),
3497
+ updatedAt: admin10.firestore.Timestamp.now(),
3498
+ // Use current server timestamp
3339
3499
  notificationId: void 0,
3340
3500
  actionTakenAt: void 0
3341
3501
  };
@@ -4546,8 +4706,12 @@ var BookingAdmin = class {
4546
4706
  return events.map((event) => ({
4547
4707
  ...event,
4548
4708
  eventTime: {
4549
- start: TimestampUtils.adminToClient(event.eventTime.start),
4550
- end: TimestampUtils.adminToClient(event.eventTime.end)
4709
+ start: TimestampUtils.adminToClient(
4710
+ event.eventTime.start
4711
+ ),
4712
+ end: TimestampUtils.adminToClient(
4713
+ event.eventTime.end
4714
+ )
4551
4715
  }
4552
4716
  // Convert any other timestamps in the event if needed
4553
4717
  }));
@@ -5012,13 +5176,21 @@ var BookingAdmin = class {
5012
5176
 
5013
5177
  // src/admin/index.ts
5014
5178
  console.log("[Admin Module] Initialized and services exported.");
5179
+ TimestampUtils.enableServerMode();
5015
5180
  // Annotate the CommonJS export names for ESM import in node:
5016
5181
  0 && (module.exports = {
5182
+ APPOINTMENTS_COLLECTION,
5017
5183
  AppointmentAggregationService,
5184
+ AppointmentMailingService,
5018
5185
  AppointmentStatus,
5019
5186
  BaseMailingService,
5020
5187
  BookingAdmin,
5188
+ BookingAvailabilityCalculator,
5189
+ CalendarAdminService,
5021
5190
  ClinicAggregationService,
5191
+ DocumentManagerAdminService,
5192
+ Logger,
5193
+ MediaType,
5022
5194
  NOTIFICATIONS_COLLECTION,
5023
5195
  NotificationStatus,
5024
5196
  NotificationType,
@@ -5028,6 +5200,7 @@ console.log("[Admin Module] Initialized and services exported.");
5028
5200
  PatientInstructionStatus,
5029
5201
  PatientRequirementOverallStatus,
5030
5202
  PatientRequirementsAdminService,
5203
+ PaymentStatus,
5031
5204
  PractitionerAggregationService,
5032
5205
  PractitionerInviteMailingService,
5033
5206
  PractitionerTokenStatus,