@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.
@@ -37,7 +37,7 @@ var NotificationStatus = /* @__PURE__ */ ((NotificationStatus2) => {
37
37
  })(NotificationStatus || {});
38
38
 
39
39
  // src/admin/notifications/notifications.admin.ts
40
- import * as admin2 from "firebase-admin";
40
+ import * as admin from "firebase-admin";
41
41
  import { Expo } from "expo-server-sdk";
42
42
 
43
43
  // src/types/appointment/index.ts
@@ -54,6 +54,21 @@ var AppointmentStatus = /* @__PURE__ */ ((AppointmentStatus2) => {
54
54
  AppointmentStatus2["RESCHEDULED_BY_CLINIC"] = "rescheduled_by_clinic";
55
55
  return AppointmentStatus2;
56
56
  })(AppointmentStatus || {});
57
+ var PaymentStatus = /* @__PURE__ */ ((PaymentStatus2) => {
58
+ PaymentStatus2["UNPAID"] = "unpaid";
59
+ PaymentStatus2["PAID"] = "paid";
60
+ PaymentStatus2["PARTIALLY_PAID"] = "partially_paid";
61
+ PaymentStatus2["REFUNDED"] = "refunded";
62
+ PaymentStatus2["NOT_APPLICABLE"] = "not_applicable";
63
+ return PaymentStatus2;
64
+ })(PaymentStatus || {});
65
+ var MediaType = /* @__PURE__ */ ((MediaType2) => {
66
+ MediaType2["BEFORE_PHOTO"] = "before_photo";
67
+ MediaType2["AFTER_PHOTO"] = "after_photo";
68
+ MediaType2["CONSENT_SCAN"] = "consent_scan";
69
+ MediaType2["OTHER_DOCUMENT"] = "other_document";
70
+ return MediaType2;
71
+ })(MediaType || {});
57
72
  var APPOINTMENTS_COLLECTION = "appointments";
58
73
 
59
74
  // src/types/documentation-templates/index.ts
@@ -72,109 +87,61 @@ var UserRole = /* @__PURE__ */ ((UserRole2) => {
72
87
  return UserRole2;
73
88
  })(UserRole || {});
74
89
 
75
- // src/utils/TimestampUtils.ts
76
- import * as admin from "firebase-admin";
77
- import { Timestamp as ClientTimestamp } from "firebase/firestore";
78
- var TimestampUtils = class {
79
- /**
80
- * Converts an admin Firestore Timestamp to a client Firestore Timestamp
81
- * @param adminTimestamp - Admin SDK Timestamp (from firebase-admin)
82
- * @returns A client SDK Timestamp (from firebase/firestore) with same seconds/nanoseconds or null if input is null
83
- */
84
- static adminToClient(adminTimestamp) {
85
- if (!adminTimestamp) return null;
86
- return new ClientTimestamp(
87
- adminTimestamp.seconds,
88
- adminTimestamp.nanoseconds
89
- );
90
- }
91
- /**
92
- * Converts a client Firestore Timestamp to an admin Firestore Timestamp
93
- * @param clientTimestamp - Client SDK Timestamp (from firebase/firestore)
94
- * @returns An admin SDK Timestamp (from firebase-admin) with same seconds/nanoseconds or null if input is null
95
- */
96
- static clientToAdmin(clientTimestamp) {
97
- if (!clientTimestamp) return null;
98
- return new admin.firestore.Timestamp(
99
- clientTimestamp.seconds,
100
- clientTimestamp.nanoseconds
101
- );
102
- }
103
- /**
104
- * Creates a client-compatible timestamp for the current time
105
- * @returns A client SDK Timestamp (from firebase/firestore) for the current time
106
- */
107
- static nowAsClient() {
108
- const now = admin.firestore.Timestamp.now();
109
- return this.adminToClient(now);
110
- }
90
+ // src/admin/logger/index.ts
91
+ var firebaseFunctionsLogger;
92
+ try {
93
+ firebaseFunctionsLogger = __require("firebase-functions/logger");
94
+ __require("firebase-functions/logger/compat");
95
+ } catch (e) {
96
+ }
97
+ var Logger = class {
111
98
  /**
112
- * Converts a Date object to a client Firestore Timestamp
113
- * @param date - JavaScript Date object
114
- * @returns A client SDK Timestamp (from firebase/firestore) or null if input is null
99
+ * Log an error message
100
+ * @param message Message to log
101
+ * @param data Optional data to include
115
102
  */
116
- static dateToClientTimestamp(date) {
117
- if (!date) return null;
118
- return ClientTimestamp.fromDate(date);
103
+ static error(message, data) {
104
+ if (firebaseFunctionsLogger) {
105
+ firebaseFunctionsLogger.error(message, data);
106
+ } else {
107
+ console.error(message, data !== void 0 ? data : "");
108
+ }
119
109
  }
120
110
  /**
121
- * Converts a Date object to an admin Firestore Timestamp
122
- * @param date - JavaScript Date object
123
- * @returns An admin SDK Timestamp (from firebase-admin) or null if input is null
111
+ * Log a warning message
112
+ * @param message Message to log
113
+ * @param data Optional data to include
124
114
  */
125
- static dateToAdminTimestamp(date) {
126
- if (!date) return null;
127
- return admin.firestore.Timestamp.fromDate(date);
115
+ static warn(message, data) {
116
+ if (firebaseFunctionsLogger) {
117
+ firebaseFunctionsLogger.warn(message, data);
118
+ } else {
119
+ console.warn(message, data !== void 0 ? data : "");
120
+ }
128
121
  }
129
122
  /**
130
- * Deeply converts all admin Firestore Timestamps in an object to client Firestore Timestamps
131
- * @param obj - Object that may contain admin Firestore Timestamps
132
- * @returns The same object with all admin Timestamps converted to client Timestamps
123
+ * Log an info message
124
+ * @param message Message to log
125
+ * @param data Optional data to include
133
126
  */
134
- static convertObjectTimestampsAdminToClient(obj) {
135
- if (!obj || typeof obj !== "object") {
136
- return obj;
137
- }
138
- if (obj instanceof admin.firestore.Timestamp) {
139
- return this.adminToClient(obj);
140
- }
141
- if (Array.isArray(obj)) {
142
- return obj.map(
143
- (item) => this.convertObjectTimestampsAdminToClient(item)
144
- );
145
- }
146
- const result = { ...obj };
147
- for (const key in result) {
148
- if (Object.prototype.hasOwnProperty.call(result, key)) {
149
- result[key] = this.convertObjectTimestampsAdminToClient(result[key]);
150
- }
127
+ static info(message, data) {
128
+ if (firebaseFunctionsLogger) {
129
+ firebaseFunctionsLogger.info(message, data);
130
+ } else {
131
+ console.info(message, data !== void 0 ? data : "");
151
132
  }
152
- return result;
153
133
  }
154
134
  /**
155
- * Deeply converts all client Firestore Timestamps in an object to admin Firestore Timestamps
156
- * @param obj - Object that may contain client Firestore Timestamps
157
- * @returns The same object with all client Timestamps converted to admin Timestamps
135
+ * Log a debug message
136
+ * @param message Message to log
137
+ * @param data Optional data to include
158
138
  */
159
- static convertObjectTimestampsClientToAdmin(obj) {
160
- if (!obj || typeof obj !== "object") {
161
- return obj;
162
- }
163
- if (obj instanceof ClientTimestamp) {
164
- return this.clientToAdmin(obj);
165
- }
166
- if (Array.isArray(obj)) {
167
- return obj.map(
168
- (item) => this.convertObjectTimestampsClientToAdmin(item)
169
- );
170
- }
171
- const result = { ...obj };
172
- for (const key in result) {
173
- if (Object.prototype.hasOwnProperty.call(result, key)) {
174
- result[key] = this.convertObjectTimestampsClientToAdmin(result[key]);
175
- }
139
+ static debug(message, data) {
140
+ if (firebaseFunctionsLogger) {
141
+ firebaseFunctionsLogger.debug(message, data);
142
+ } else {
143
+ console.debug(message, data !== void 0 ? data : "");
176
144
  }
177
- return result;
178
145
  }
179
146
  };
180
147
 
@@ -182,7 +149,7 @@ var TimestampUtils = class {
182
149
  var NotificationsAdmin = class {
183
150
  constructor(firestore12) {
184
151
  this.expo = new Expo();
185
- this.db = firestore12 || admin2.firestore();
152
+ this.db = firestore12 || admin.firestore();
186
153
  }
187
154
  /**
188
155
  * Dohvata notifikaciju po ID-u
@@ -198,8 +165,8 @@ var NotificationsAdmin = class {
198
165
  const docRef = await this.db.collection("notifications").add({
199
166
  ...notification,
200
167
  status: "pending" /* PENDING */,
201
- createdAt: admin2.firestore.FieldValue.serverTimestamp(),
202
- updatedAt: admin2.firestore.FieldValue.serverTimestamp()
168
+ createdAt: admin.firestore.FieldValue.serverTimestamp(),
169
+ updatedAt: admin.firestore.FieldValue.serverTimestamp()
203
170
  });
204
171
  return docRef.id;
205
172
  }
@@ -207,7 +174,18 @@ var NotificationsAdmin = class {
207
174
  * Priprema Expo poruku za slanje
208
175
  */
209
176
  prepareExpoMessage(notification) {
210
- return notification.notificationTokens.filter((token) => Expo.isExpoPushToken(token)).map((token) => ({
177
+ const validTokens = notification.notificationTokens.filter(
178
+ (token) => Expo.isExpoPushToken(token)
179
+ );
180
+ Logger.info(
181
+ `[NotificationsAdmin] Preparing Expo messages for notification ${notification.id}`,
182
+ {
183
+ totalTokens: notification.notificationTokens.length,
184
+ validTokens: validTokens.length,
185
+ invalidTokensCount: notification.notificationTokens.length - validTokens.length
186
+ }
187
+ );
188
+ return validTokens.map((token) => ({
211
189
  to: token,
212
190
  sound: "default",
213
191
  title: notification.title,
@@ -225,10 +203,10 @@ var NotificationsAdmin = class {
225
203
  async updateNotificationStatus(notificationId, status, error) {
226
204
  const update = {
227
205
  status,
228
- updatedAt: admin2.firestore.FieldValue.serverTimestamp()
206
+ updatedAt: admin.firestore.FieldValue.serverTimestamp()
229
207
  };
230
208
  if (status === "sent" /* SENT */) {
231
- update.sentAt = admin2.firestore.FieldValue.serverTimestamp();
209
+ update.sentAt = admin.firestore.FieldValue.serverTimestamp();
232
210
  }
233
211
  if (error) {
234
212
  update.error = error;
@@ -239,45 +217,94 @@ var NotificationsAdmin = class {
239
217
  * Šalje notifikaciju kroz Expo servis sa boljim error handlingom
240
218
  */
241
219
  async sendPushNotification(notification) {
220
+ var _a;
242
221
  try {
222
+ Logger.info(
223
+ `[NotificationsAdmin] Processing notification ${notification.id} for sending`,
224
+ {
225
+ userId: notification.userId,
226
+ tokenCount: ((_a = notification.notificationTokens) == null ? void 0 : _a.length) || 0,
227
+ type: notification.notificationType
228
+ }
229
+ );
243
230
  const messages = this.prepareExpoMessage(notification);
244
231
  if (messages.length === 0) {
232
+ const errorMsg = "No valid notification tokens found";
233
+ Logger.error(
234
+ `[NotificationsAdmin] ${errorMsg} for notification ${notification.id}`
235
+ );
245
236
  await this.updateNotificationStatus(
246
237
  notification.id,
247
238
  "failed" /* FAILED */,
248
- "No valid notification tokens found"
239
+ errorMsg
249
240
  );
250
241
  return false;
251
242
  }
252
243
  const chunks = this.expo.chunkPushNotifications(messages);
244
+ Logger.info(
245
+ `[NotificationsAdmin] Sending ${messages.length} messages in ${chunks.length} chunks for notification ${notification.id}`
246
+ );
253
247
  const tickets = [];
254
- for (const chunk of chunks) {
248
+ for (let i = 0; i < chunks.length; i++) {
249
+ const chunk = chunks[i];
255
250
  try {
251
+ Logger.info(
252
+ `[NotificationsAdmin] Sending chunk ${i + 1}/${chunks.length} with ${chunk.length} messages`
253
+ );
256
254
  const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
255
+ Logger.info(
256
+ `[NotificationsAdmin] Received ${ticketChunk.length} tickets for chunk ${i + 1}`
257
+ );
257
258
  tickets.push(ticketChunk);
258
259
  } catch (error) {
259
- console.error(`Chunk sending error:`, error);
260
+ Logger.error(
261
+ `[NotificationsAdmin] Chunk ${i + 1} sending error:`,
262
+ error
263
+ );
260
264
  throw error;
261
265
  }
262
266
  }
263
267
  let hasErrors = false;
264
268
  const errors = [];
265
- tickets.flat().forEach((ticket, index) => {
269
+ const ticketsFlat = tickets.flat();
270
+ const ticketResults = {
271
+ total: ticketsFlat.length,
272
+ success: 0,
273
+ error: 0,
274
+ errorDetails: {}
275
+ };
276
+ ticketsFlat.forEach((ticket, index) => {
266
277
  if (ticket.status === "error") {
267
278
  hasErrors = true;
268
- errors.push(
269
- `Token ${notification.notificationTokens[index]}: ${ticket.message}`
270
- );
279
+ ticketResults.error++;
280
+ const errorMessage = ticket.message || "Unknown error";
281
+ ticketResults.errorDetails[errorMessage] = (ticketResults.errorDetails[errorMessage] || 0) + 1;
282
+ const tokenInfo = index < notification.notificationTokens.length ? `Token ${notification.notificationTokens[index]}` : `Token at index ${index}`;
283
+ errors.push(`${tokenInfo}: ${errorMessage}`);
284
+ } else {
285
+ ticketResults.success++;
271
286
  }
272
287
  });
288
+ Logger.info(
289
+ `[NotificationsAdmin] Ticket results for notification ${notification.id}`,
290
+ ticketResults
291
+ );
273
292
  if (hasErrors) {
293
+ const errorSummary = errors.join("; ");
294
+ Logger.warn(
295
+ `[NotificationsAdmin] Partial success or errors in notification ${notification.id}`,
296
+ { errorCount: errors.length, errorSummary }
297
+ );
274
298
  await this.updateNotificationStatus(
275
299
  notification.id,
276
- "partialSuccess" /* PARTIAL_SUCCESS */,
277
- errors.join("; ")
300
+ ticketResults.success > 0 ? "partialSuccess" /* PARTIAL_SUCCESS */ : "failed" /* FAILED */,
301
+ errorSummary
278
302
  );
279
- return false;
303
+ return ticketResults.success > 0;
280
304
  }
305
+ Logger.info(
306
+ `[NotificationsAdmin] Successfully sent notification ${notification.id} to all recipients`
307
+ );
281
308
  await this.updateNotificationStatus(
282
309
  notification.id,
283
310
  "sent" /* SENT */
@@ -285,7 +312,10 @@ var NotificationsAdmin = class {
285
312
  return true;
286
313
  } catch (error) {
287
314
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
288
- console.error("Gre\u0161ka pri slanju notifikacije:", error);
315
+ Logger.error(
316
+ `[NotificationsAdmin] Critical error sending notification ${notification.id}:`,
317
+ error
318
+ );
289
319
  await this.updateNotificationStatus(
290
320
  notification.id,
291
321
  "failed" /* FAILED */,
@@ -298,14 +328,29 @@ var NotificationsAdmin = class {
298
328
  * Procesira notifikacije koje čekaju na slanje sa batch limitom
299
329
  */
300
330
  async processPendingNotifications(batchSize = 100) {
301
- const now = admin2.firestore.Timestamp.now();
331
+ const now = admin.firestore.Timestamp.now();
332
+ Logger.info(
333
+ `[NotificationsAdmin] Starting to process pending notifications with batch size ${batchSize}`
334
+ );
302
335
  const pendingNotifications = await this.db.collection("notifications").where("status", "==", "pending" /* PENDING */).where("notificationTime", "<=", now).limit(batchSize).get();
336
+ Logger.info(
337
+ `[NotificationsAdmin] Found ${pendingNotifications.size} pending notifications to process`
338
+ );
339
+ if (pendingNotifications.empty) {
340
+ Logger.info(
341
+ "[NotificationsAdmin] No pending notifications found to process"
342
+ );
343
+ return;
344
+ }
303
345
  const results = await Promise.allSettled(
304
346
  pendingNotifications.docs.map(async (doc) => {
305
347
  const notification = {
306
348
  id: doc.id,
307
349
  ...doc.data()
308
350
  };
351
+ Logger.info(
352
+ `[NotificationsAdmin] Processing notification ${notification.id} of type ${notification.notificationType}`
353
+ );
309
354
  return this.sendPushNotification(notification);
310
355
  })
311
356
  );
@@ -315,8 +360,8 @@ var NotificationsAdmin = class {
315
360
  const failed = results.filter(
316
361
  (r) => r.status === "rejected" || r.status === "fulfilled" && !r.value
317
362
  ).length;
318
- console.log(
319
- `Processed ${results.length} notifications: ${successful} successful, ${failed} failed`
363
+ Logger.info(
364
+ `[NotificationsAdmin] Processed ${results.length} notifications: ${successful} successful, ${failed} failed`
320
365
  );
321
366
  }
322
367
  /**
@@ -325,13 +370,20 @@ var NotificationsAdmin = class {
325
370
  async cleanupOldNotifications(daysOld = 30, batchSize = 500) {
326
371
  const cutoffDate = /* @__PURE__ */ new Date();
327
372
  cutoffDate.setDate(cutoffDate.getDate() - daysOld);
373
+ Logger.info(
374
+ `[NotificationsAdmin] Starting cleanup of notifications older than ${daysOld} days`
375
+ );
376
+ let totalDeleted = 0;
328
377
  while (true) {
329
378
  const oldNotifications = await this.db.collection("notifications").where(
330
379
  "createdAt",
331
380
  "<=",
332
- admin2.firestore.Timestamp.fromDate(cutoffDate)
381
+ admin.firestore.Timestamp.fromDate(cutoffDate)
333
382
  ).limit(batchSize).get();
334
383
  if (oldNotifications.empty) {
384
+ Logger.info(
385
+ `[NotificationsAdmin] No more old notifications to delete. Total deleted: ${totalDeleted}`
386
+ );
335
387
  break;
336
388
  }
337
389
  const batch = this.db.batch();
@@ -339,8 +391,9 @@ var NotificationsAdmin = class {
339
391
  batch.delete(doc.ref);
340
392
  });
341
393
  await batch.commit();
342
- console.log(
343
- `Deleted batch of ${oldNotifications.size} old notifications`
394
+ totalDeleted += oldNotifications.size;
395
+ Logger.info(
396
+ `[NotificationsAdmin] Deleted batch of ${oldNotifications.size} old notifications. Running total: ${totalDeleted}`
344
397
  );
345
398
  }
346
399
  }
@@ -371,15 +424,12 @@ var NotificationsAdmin = class {
371
424
  title = "New Appointment Confirmed";
372
425
  body = `Appointment for ${appointment.procedureInfo.name} with ${appointment.patientInfo.fullName} on ${appointment.appointmentStartTime.toDate().toLocaleDateString()} is confirmed.`;
373
426
  }
374
- const adminTsNow = admin2.firestore.Timestamp.now();
375
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
376
- adminTsNow
377
- );
427
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
378
428
  const notificationData = {
379
429
  userId: recipientUserId,
380
430
  userRole: recipientRole,
381
431
  notificationType: "appointmentStatusChange" /* APPOINTMENT_STATUS_CHANGE */,
382
- notificationTime: clientCompatibleNotificationTime,
432
+ notificationTime: notificationTimestampForDb,
383
433
  notificationTokens: recipientExpoTokens,
384
434
  title,
385
435
  body,
@@ -425,15 +475,12 @@ var NotificationsAdmin = class {
425
475
  body += ` Reason: ${appointment.cancellationReason}`;
426
476
  }
427
477
  }
428
- const adminTsNow = admin2.firestore.Timestamp.now();
429
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
430
- adminTsNow
431
- );
478
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
432
479
  const notificationData = {
433
480
  userId: recipientUserId,
434
481
  userRole: recipientRole,
435
482
  notificationType: "appointmentCancelled" /* APPOINTMENT_CANCELLED */,
436
- notificationTime: clientCompatibleNotificationTime,
483
+ notificationTime: notificationTimestampForDb,
437
484
  notificationTokens: recipientExpoTokens,
438
485
  title,
439
486
  body,
@@ -464,15 +511,12 @@ var NotificationsAdmin = class {
464
511
  }
465
512
  const title = "Appointment Reschedule Proposed";
466
513
  const body = `Action Required: A new time has been proposed for your appointment for ${appointment.procedureInfo.name}. Please review in the app.`;
467
- const adminTsNow = admin2.firestore.Timestamp.now();
468
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
469
- adminTsNow
470
- );
514
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
471
515
  const notificationData = {
472
516
  userId: patientUserId,
473
517
  userRole: "patient" /* PATIENT */,
474
518
  notificationType: "appointmentRescheduledProposal" /* APPOINTMENT_RESCHEDULED_PROPOSAL */,
475
- notificationTime: clientCompatibleNotificationTime,
519
+ notificationTime: notificationTimestampForDb,
476
520
  notificationTokens: patientExpoTokens,
477
521
  title,
478
522
  body,
@@ -503,16 +547,13 @@ var NotificationsAdmin = class {
503
547
  }
504
548
  const title = "Payment Updated";
505
549
  const body = `Your payment status for the appointment (${appointment.procedureInfo.name}) on ${appointment.appointmentStartTime.toDate().toLocaleDateString()} is now ${appointment.paymentStatus}.`;
506
- const adminTsNow = admin2.firestore.Timestamp.now();
507
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
508
- adminTsNow
509
- );
550
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
510
551
  const notificationType = appointment.paymentStatus === "paid" /* PAID */ ? "paymentConfirmation" /* PAYMENT_CONFIRMATION */ : "generalMessage" /* GENERAL_MESSAGE */;
511
552
  const notificationData = {
512
553
  userId: patientUserId,
513
554
  userRole: "patient" /* PATIENT */,
514
555
  notificationType,
515
- notificationTime: clientCompatibleNotificationTime,
556
+ notificationTime: notificationTimestampForDb,
516
557
  notificationTokens: patientExpoTokens,
517
558
  title,
518
559
  body,
@@ -543,15 +584,12 @@ var NotificationsAdmin = class {
543
584
  }
544
585
  const title = "Leave a Review";
545
586
  const body = `How was your recent appointment for ${appointment.procedureInfo.name}? We'd love to hear your feedback!`;
546
- const adminTsNow = admin2.firestore.Timestamp.now();
547
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
548
- adminTsNow
549
- );
587
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
550
588
  const notificationData = {
551
589
  userId: patientUserId,
552
590
  userRole: "patient" /* PATIENT */,
553
591
  notificationType: "reviewRequest" /* REVIEW_REQUEST */,
554
- notificationTime: clientCompatibleNotificationTime,
592
+ notificationTime: notificationTimestampForDb,
555
593
  notificationTokens: patientExpoTokens,
556
594
  title,
557
595
  body,
@@ -588,16 +626,13 @@ var NotificationsAdmin = class {
588
626
  }
589
627
  const title = "New Review Received";
590
628
  const body = `A new review has been added by ${appointment.patientInfo.fullName} for your appointment on ${appointment.appointmentStartTime.toDate().toLocaleDateString()}.`;
591
- const adminTsNow = admin2.firestore.Timestamp.now();
592
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
593
- adminTsNow
594
- );
629
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
595
630
  const tempNotificationType = "generalMessage" /* GENERAL_MESSAGE */;
596
631
  const notificationData = {
597
632
  userId: recipientUserId,
598
633
  userRole: "practitioner" /* PRACTITIONER */,
599
634
  notificationType: tempNotificationType,
600
- notificationTime: clientCompatibleNotificationTime,
635
+ notificationTime: notificationTimestampForDb,
601
636
  notificationTokens: recipientExpoTokens,
602
637
  title,
603
638
  body,
@@ -621,6 +656,187 @@ var NotificationsAdmin = class {
621
656
  }
622
657
  };
623
658
 
659
+ // src/utils/TimestampUtils.ts
660
+ import * as admin2 from "firebase-admin";
661
+ import { Timestamp as ClientTimestamp } from "firebase/firestore";
662
+ var IS_SERVER_ENV = process.env.NODE_ENV === "production" || process.env.FUNCTIONS_EMULATOR === "true" || process.env.FIREBASE_CONFIG !== void 0;
663
+ var TimestampUtils = class {
664
+ /**
665
+ * Enables server mode where admin timestamps are preserved when saving
666
+ * to Firestore via the admin SDK
667
+ */
668
+ static enableServerMode() {
669
+ this.serverMode = true;
670
+ }
671
+ /**
672
+ * Disables server mode - use this only for client-side or mixed environments
673
+ */
674
+ static disableServerMode() {
675
+ this.serverMode = false;
676
+ }
677
+ /**
678
+ * Converts an admin Firestore Timestamp to a client Firestore Timestamp
679
+ * In server mode, returns the original admin timestamp for storage consistency
680
+ *
681
+ * @param adminTimestamp - Admin SDK Timestamp (from firebase-admin)
682
+ * @returns A client SDK Timestamp (from firebase/firestore) with same seconds/nanoseconds,
683
+ * or the original admin timestamp in server mode,
684
+ * or null if input is null
685
+ */
686
+ static adminToClient(adminTimestamp) {
687
+ if (!adminTimestamp) return null;
688
+ if (this.serverMode) {
689
+ return adminTimestamp;
690
+ }
691
+ return new ClientTimestamp(
692
+ adminTimestamp.seconds,
693
+ adminTimestamp.nanoseconds
694
+ );
695
+ }
696
+ /**
697
+ * Converts a client Firestore Timestamp to an admin Firestore Timestamp
698
+ * @param clientTimestamp - Client SDK Timestamp (from firebase/firestore)
699
+ * @returns An admin SDK Timestamp (from firebase-admin) with same seconds/nanoseconds or null if input is null
700
+ */
701
+ static clientToAdmin(clientTimestamp) {
702
+ if (!clientTimestamp) return null;
703
+ return new admin2.firestore.Timestamp(
704
+ clientTimestamp.seconds,
705
+ clientTimestamp.nanoseconds
706
+ );
707
+ }
708
+ /**
709
+ * Creates a timestamp for the current time in the appropriate format based on environment
710
+ * @returns A timestamp for the current time (admin timestamp in server mode, client in client mode)
711
+ */
712
+ static nowAsTimestamp() {
713
+ const now = admin2.firestore.Timestamp.now();
714
+ if (this.serverMode) {
715
+ return now;
716
+ }
717
+ return this.adminToClient(now);
718
+ }
719
+ /**
720
+ * @deprecated Use nowAsTimestamp() instead for better cross-environment compatibility
721
+ */
722
+ static nowAsClient() {
723
+ const now = admin2.firestore.Timestamp.now();
724
+ return this.adminToClient(now);
725
+ }
726
+ /**
727
+ * Converts a Date object to a timestamp in the appropriate format based on environment
728
+ * @param date - JavaScript Date object
729
+ * @returns A timestamp (admin timestamp in server mode, client in client mode) or null if input is null
730
+ */
731
+ static dateToTimestamp(date) {
732
+ if (!date) return null;
733
+ if (this.serverMode) {
734
+ return admin2.firestore.Timestamp.fromDate(date);
735
+ }
736
+ return ClientTimestamp.fromDate(date);
737
+ }
738
+ /**
739
+ * @deprecated Use dateToTimestamp() instead for better cross-environment compatibility
740
+ */
741
+ static dateToClientTimestamp(date) {
742
+ if (!date) return null;
743
+ return ClientTimestamp.fromDate(date);
744
+ }
745
+ /**
746
+ * @deprecated Use dateToTimestamp() instead for better cross-environment compatibility
747
+ */
748
+ static dateToAdminTimestamp(date) {
749
+ if (!date) return null;
750
+ return admin2.firestore.Timestamp.fromDate(date);
751
+ }
752
+ /**
753
+ * Gets a server timestamp field value for use in create/update operations
754
+ * Works in both admin and client environments
755
+ */
756
+ static serverTimestamp() {
757
+ if (this.serverMode) {
758
+ return admin2.firestore.FieldValue.serverTimestamp();
759
+ }
760
+ throw new Error("Server timestamp in client mode not implemented");
761
+ }
762
+ /**
763
+ * For objects with mixed timestamp types, ensures all timestamps are
764
+ * in the correct format for the current environment
765
+ */
766
+ static normalizeTimestamps(obj) {
767
+ if (!obj || typeof obj !== "object") {
768
+ return obj;
769
+ }
770
+ if (obj instanceof admin2.firestore.Timestamp) {
771
+ return this.serverMode ? obj : this.adminToClient(obj);
772
+ }
773
+ if (obj instanceof ClientTimestamp && this.serverMode) {
774
+ return this.clientToAdmin(obj);
775
+ }
776
+ if (Array.isArray(obj)) {
777
+ return obj.map((item) => this.normalizeTimestamps(item));
778
+ }
779
+ const result = { ...obj };
780
+ for (const key in result) {
781
+ if (Object.prototype.hasOwnProperty.call(result, key)) {
782
+ result[key] = this.normalizeTimestamps(result[key]);
783
+ }
784
+ }
785
+ return result;
786
+ }
787
+ /**
788
+ * @deprecated Use normalizeTimestamps() instead for better cross-environment compatibility
789
+ */
790
+ static convertObjectTimestampsAdminToClient(obj) {
791
+ if (!obj || typeof obj !== "object") {
792
+ return obj;
793
+ }
794
+ if (obj instanceof admin2.firestore.Timestamp) {
795
+ return this.adminToClient(obj);
796
+ }
797
+ if (Array.isArray(obj)) {
798
+ return obj.map(
799
+ (item) => this.convertObjectTimestampsAdminToClient(item)
800
+ );
801
+ }
802
+ const result = { ...obj };
803
+ for (const key in result) {
804
+ if (Object.prototype.hasOwnProperty.call(result, key)) {
805
+ result[key] = this.convertObjectTimestampsAdminToClient(result[key]);
806
+ }
807
+ }
808
+ return result;
809
+ }
810
+ /**
811
+ * @deprecated Use normalizeTimestamps() instead for better cross-environment compatibility
812
+ */
813
+ static convertObjectTimestampsClientToAdmin(obj) {
814
+ if (!obj || typeof obj !== "object") {
815
+ return obj;
816
+ }
817
+ if (obj instanceof ClientTimestamp) {
818
+ return this.clientToAdmin(obj);
819
+ }
820
+ if (Array.isArray(obj)) {
821
+ return obj.map(
822
+ (item) => this.convertObjectTimestampsClientToAdmin(item)
823
+ );
824
+ }
825
+ const result = { ...obj };
826
+ for (const key in result) {
827
+ if (Object.prototype.hasOwnProperty.call(result, key)) {
828
+ result[key] = this.convertObjectTimestampsClientToAdmin(result[key]);
829
+ }
830
+ }
831
+ return result;
832
+ }
833
+ };
834
+ /**
835
+ * Flag to force server mode where admin timestamps are preserved
836
+ * This should be true in Cloud Functions environments
837
+ */
838
+ TimestampUtils.serverMode = IS_SERVER_ENV;
839
+
624
840
  // src/admin/aggregation/clinic/clinic.aggregation.service.ts
625
841
  import * as admin3 from "firebase-admin";
626
842
 
@@ -2081,7 +2297,7 @@ var PatientRequirementsAdminService = class {
2081
2297
  userRole: "patient" /* PATIENT */,
2082
2298
  notificationType: "requirementInstructionDue" /* REQUIREMENT_INSTRUCTION_DUE */,
2083
2299
  notificationTime: currentInstruction.dueTime,
2084
- // This is a Firestore Timestamp
2300
+ // dueTime should be an admin.firestore.Timestamp already
2085
2301
  notificationTokens: patientExpoTokens,
2086
2302
  title: `Reminder: ${instance.requirementName}`,
2087
2303
  body: currentInstruction.instructionText,
@@ -2309,66 +2525,6 @@ var PatientRequirementsAdminService = class {
2309
2525
 
2310
2526
  // src/admin/calendar/calendar.admin.service.ts
2311
2527
  import * as admin8 from "firebase-admin";
2312
-
2313
- // src/admin/logger/index.ts
2314
- var firebaseFunctionsLogger;
2315
- try {
2316
- firebaseFunctionsLogger = __require("firebase-functions/logger");
2317
- __require("firebase-functions/logger/compat");
2318
- } catch (e) {
2319
- }
2320
- var Logger = class {
2321
- /**
2322
- * Log an error message
2323
- * @param message Message to log
2324
- * @param data Optional data to include
2325
- */
2326
- static error(message, data) {
2327
- if (firebaseFunctionsLogger) {
2328
- firebaseFunctionsLogger.error(message, data);
2329
- } else {
2330
- console.error(message, data !== void 0 ? data : "");
2331
- }
2332
- }
2333
- /**
2334
- * Log a warning message
2335
- * @param message Message to log
2336
- * @param data Optional data to include
2337
- */
2338
- static warn(message, data) {
2339
- if (firebaseFunctionsLogger) {
2340
- firebaseFunctionsLogger.warn(message, data);
2341
- } else {
2342
- console.warn(message, data !== void 0 ? data : "");
2343
- }
2344
- }
2345
- /**
2346
- * Log an info message
2347
- * @param message Message to log
2348
- * @param data Optional data to include
2349
- */
2350
- static info(message, data) {
2351
- if (firebaseFunctionsLogger) {
2352
- firebaseFunctionsLogger.info(message, data);
2353
- } else {
2354
- console.info(message, data !== void 0 ? data : "");
2355
- }
2356
- }
2357
- /**
2358
- * Log a debug message
2359
- * @param message Message to log
2360
- * @param data Optional data to include
2361
- */
2362
- static debug(message, data) {
2363
- if (firebaseFunctionsLogger) {
2364
- firebaseFunctionsLogger.debug(message, data);
2365
- } else {
2366
- console.debug(message, data !== void 0 ? data : "");
2367
- }
2368
- }
2369
- };
2370
-
2371
- // src/admin/calendar/calendar.admin.service.ts
2372
2528
  var CalendarAdminService = class {
2373
2529
  constructor(firestore12) {
2374
2530
  this.db = firestore12 || admin8.firestore();
@@ -2423,14 +2579,8 @@ var CalendarAdminService = class {
2423
2579
  const batch = this.db.batch();
2424
2580
  const serverTimestamp = admin8.firestore.FieldValue.serverTimestamp();
2425
2581
  const firestoreCompatibleEventTime = {
2426
- start: {
2427
- seconds: newEventTime.start.seconds,
2428
- nanoseconds: newEventTime.start.nanoseconds
2429
- },
2430
- end: {
2431
- seconds: newEventTime.end.seconds,
2432
- nanoseconds: newEventTime.end.nanoseconds
2433
- }
2582
+ start: TimestampUtils.clientToAdmin(newEventTime.start) || admin8.firestore.Timestamp.now(),
2583
+ end: TimestampUtils.clientToAdmin(newEventTime.end) || admin8.firestore.Timestamp.now()
2434
2584
  };
2435
2585
  if (appointment.calendarEventId) {
2436
2586
  const practitionerEventRef = this.db.doc(
@@ -3178,7 +3328,8 @@ var AppointmentAggregationService = class {
3178
3328
  status: "pendingNotification" /* PENDING_NOTIFICATION */,
3179
3329
  originalNotifyAtValue: notifyAtValue,
3180
3330
  originalTimeframeUnit: template.timeframe.unit,
3181
- updatedAt: admin10.firestore.FieldValue.serverTimestamp(),
3331
+ updatedAt: admin10.firestore.Timestamp.now(),
3332
+ // Use current server timestamp
3182
3333
  notificationId: void 0,
3183
3334
  actionTakenAt: void 0
3184
3335
  };
@@ -3288,7 +3439,8 @@ var AppointmentAggregationService = class {
3288
3439
  status: "pendingNotification" /* PENDING_NOTIFICATION */,
3289
3440
  originalNotifyAtValue: notifyAtValue,
3290
3441
  originalTimeframeUnit: template.timeframe.unit,
3291
- updatedAt: admin10.firestore.FieldValue.serverTimestamp(),
3442
+ updatedAt: admin10.firestore.Timestamp.now(),
3443
+ // Use current server timestamp
3292
3444
  notificationId: void 0,
3293
3445
  actionTakenAt: void 0
3294
3446
  };
@@ -4499,8 +4651,12 @@ var BookingAdmin = class {
4499
4651
  return events.map((event) => ({
4500
4652
  ...event,
4501
4653
  eventTime: {
4502
- start: TimestampUtils.adminToClient(event.eventTime.start),
4503
- end: TimestampUtils.adminToClient(event.eventTime.end)
4654
+ start: TimestampUtils.adminToClient(
4655
+ event.eventTime.start
4656
+ ),
4657
+ end: TimestampUtils.adminToClient(
4658
+ event.eventTime.end
4659
+ )
4504
4660
  }
4505
4661
  // Convert any other timestamps in the event if needed
4506
4662
  }));
@@ -4965,12 +5121,20 @@ var BookingAdmin = class {
4965
5121
 
4966
5122
  // src/admin/index.ts
4967
5123
  console.log("[Admin Module] Initialized and services exported.");
5124
+ TimestampUtils.enableServerMode();
4968
5125
  export {
5126
+ APPOINTMENTS_COLLECTION,
4969
5127
  AppointmentAggregationService,
5128
+ AppointmentMailingService,
4970
5129
  AppointmentStatus,
4971
5130
  BaseMailingService,
4972
5131
  BookingAdmin,
5132
+ BookingAvailabilityCalculator,
5133
+ CalendarAdminService,
4973
5134
  ClinicAggregationService,
5135
+ DocumentManagerAdminService,
5136
+ Logger,
5137
+ MediaType,
4974
5138
  NOTIFICATIONS_COLLECTION,
4975
5139
  NotificationStatus,
4976
5140
  NotificationType,
@@ -4980,6 +5144,7 @@ export {
4980
5144
  PatientInstructionStatus,
4981
5145
  PatientRequirementOverallStatus,
4982
5146
  PatientRequirementsAdminService,
5147
+ PaymentStatus,
4983
5148
  PractitionerAggregationService,
4984
5149
  PractitionerInviteMailingService,
4985
5150
  PractitionerTokenStatus,