@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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.6.14",
4
+ "version": "1.6.16",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",
@@ -565,7 +565,7 @@ export class AppointmentAggregationService {
565
565
  status: PatientInstructionStatus.PENDING_NOTIFICATION,
566
566
  originalNotifyAtValue: notifyAtValue,
567
567
  originalTimeframeUnit: template.timeframe.unit,
568
- updatedAt: admin.firestore.FieldValue.serverTimestamp() as any,
568
+ updatedAt: admin.firestore.Timestamp.now() as any, // Use current server timestamp
569
569
  notificationId: undefined,
570
570
  actionTakenAt: undefined,
571
571
  };
@@ -707,7 +707,7 @@ export class AppointmentAggregationService {
707
707
  status: PatientInstructionStatus.PENDING_NOTIFICATION,
708
708
  originalNotifyAtValue: notifyAtValue,
709
709
  originalTimeframeUnit: template.timeframe.unit,
710
- updatedAt: admin.firestore.FieldValue.serverTimestamp() as any,
710
+ updatedAt: admin.firestore.Timestamp.now() as any, // Use current server timestamp
711
711
  notificationId: undefined,
712
712
  actionTakenAt: undefined,
713
713
  } as PatientRequirementInstruction;
@@ -205,9 +205,9 @@ export class BookingAdmin {
205
205
  */
206
206
  private adminTimestampToClientTimestamp(
207
207
  timestamp: admin.firestore.Timestamp
208
- ): any {
208
+ ): FirebaseClientTimestamp {
209
209
  // Use TimestampUtils instead of custom implementation
210
- return TimestampUtils.adminToClient(timestamp);
210
+ return TimestampUtils.adminToClient(timestamp) as FirebaseClientTimestamp;
211
211
  }
212
212
 
213
213
  /**
@@ -217,8 +217,12 @@ export class BookingAdmin {
217
217
  return events.map((event) => ({
218
218
  ...event,
219
219
  eventTime: {
220
- start: TimestampUtils.adminToClient(event.eventTime.start),
221
- end: TimestampUtils.adminToClient(event.eventTime.end),
220
+ start: TimestampUtils.adminToClient(
221
+ event.eventTime.start
222
+ ) as FirebaseClientTimestamp,
223
+ end: TimestampUtils.adminToClient(
224
+ event.eventTime.end
225
+ ) as FirebaseClientTimestamp,
222
226
  },
223
227
  // Convert any other timestamps in the event if needed
224
228
  }));
@@ -10,6 +10,7 @@ import { Logger } from "../logger";
10
10
  import { PRACTITIONERS_COLLECTION } from "../../types/practitioner";
11
11
  import { PATIENTS_COLLECTION } from "../../types/patient";
12
12
  import { CLINICS_COLLECTION } from "../../types/clinic";
13
+ import { TimestampUtils } from "../../utils/TimestampUtils";
13
14
 
14
15
  /**
15
16
  * @class CalendarAdminService
@@ -103,18 +104,14 @@ export class CalendarAdminService {
103
104
  const batch = this.db.batch();
104
105
  const serverTimestamp = admin.firestore.FieldValue.serverTimestamp();
105
106
 
106
- // Convert FirebaseClientTimestamp to a plain object for Firestore admin SDK if needed,
107
- // or ensure the CalendarEventTime type is directly compatible.
108
- // Firestore admin SDK usually handles { seconds: X, nanoseconds: Y } objects correctly.
107
+ // Convert client timestamps to admin timestamps since we're in server mode
109
108
  const firestoreCompatibleEventTime = {
110
- start: {
111
- seconds: newEventTime.start.seconds,
112
- nanoseconds: newEventTime.start.nanoseconds,
113
- },
114
- end: {
115
- seconds: newEventTime.end.seconds,
116
- nanoseconds: newEventTime.end.nanoseconds,
117
- },
109
+ start:
110
+ TimestampUtils.clientToAdmin(newEventTime.start) ||
111
+ admin.firestore.Timestamp.now(),
112
+ end:
113
+ TimestampUtils.clientToAdmin(newEventTime.end) ||
114
+ admin.firestore.Timestamp.now(),
118
115
  };
119
116
 
120
117
  // TODO: Confirm paths as in updateAppointmentCalendarEventsStatus
@@ -1,4 +1,6 @@
1
1
  import { NotificationsAdmin } from "./notifications/notifications.admin";
2
+ import * as admin from "firebase-admin";
3
+ import { TimestampUtils } from "../utils/TimestampUtils";
2
4
 
3
5
  import { UserRole } from "../types";
4
6
  // Import types needed by admin consumers (like Cloud Functions)
@@ -111,3 +113,22 @@ console.log("[Admin Module] Initialized and services exported.");
111
113
  // if they have dependencies (like Firestore db) that need to be injected.
112
114
  // The initialization pattern might differ depending on how this module is consumed
113
115
  // (e.g., within a larger NestJS app vs. standalone Cloud Functions).
116
+
117
+ // Initialize TimestampUtils for server-side use
118
+ TimestampUtils.enableServerMode();
119
+
120
+ // Export all admin services that are needed
121
+ export * from "./booking/booking.admin";
122
+ export * from "./booking/booking.calculator";
123
+ export * from "./booking/booking.types";
124
+ export * from "./calendar/calendar.admin.service";
125
+ export * from "./documentation-templates/document-manager.admin";
126
+ export * from "./logger";
127
+ export * from "./mailing/appointment/appointment.mailing.service";
128
+ export * from "./notifications/notifications.admin";
129
+ export * from "./requirements/patient-requirements.admin.service";
130
+ export * from "./aggregation/appointment/appointment.aggregation.service";
131
+
132
+ // Re-export types that Cloud Functions might need direct access to
133
+ export * from "../types/appointment";
134
+ // Add other types as needed
@@ -9,6 +9,7 @@ import { Appointment, PaymentStatus } from "../../types/appointment";
9
9
  import { UserRole } from "../../types";
10
10
  import { Timestamp as FirebaseClientTimestamp } from "@firebase/firestore";
11
11
  import { TimestampUtils } from "../../utils/TimestampUtils";
12
+ import { Logger } from "../logger";
12
13
 
13
14
  export class NotificationsAdmin {
14
15
  private expo: Expo;
@@ -46,19 +47,31 @@ export class NotificationsAdmin {
46
47
  * Priprema Expo poruku za slanje
47
48
  */
48
49
  private prepareExpoMessage(notification: Notification): ExpoPushMessage[] {
49
- return notification.notificationTokens
50
- .filter((token) => Expo.isExpoPushToken(token))
51
- .map((token) => ({
52
- to: token,
53
- sound: "default",
54
- title: notification.title,
55
- body: notification.body,
56
- data: {
57
- notificationId: notification.id,
58
- notificationType: notification.notificationType,
59
- userId: notification.userId,
60
- },
61
- }));
50
+ const validTokens = notification.notificationTokens.filter((token) =>
51
+ Expo.isExpoPushToken(token)
52
+ );
53
+
54
+ Logger.info(
55
+ `[NotificationsAdmin] Preparing Expo messages for notification ${notification.id}`,
56
+ {
57
+ totalTokens: notification.notificationTokens.length,
58
+ validTokens: validTokens.length,
59
+ invalidTokensCount:
60
+ notification.notificationTokens.length - validTokens.length,
61
+ }
62
+ );
63
+
64
+ return validTokens.map((token) => ({
65
+ to: token,
66
+ sound: "default",
67
+ title: notification.title,
68
+ body: notification.body,
69
+ data: {
70
+ notificationId: notification.id,
71
+ notificationType: notification.notificationType,
72
+ userId: notification.userId,
73
+ },
74
+ }));
62
75
  }
63
76
 
64
77
  /**
@@ -93,27 +106,58 @@ export class NotificationsAdmin {
93
106
  */
94
107
  async sendPushNotification(notification: Notification): Promise<boolean> {
95
108
  try {
109
+ Logger.info(
110
+ `[NotificationsAdmin] Processing notification ${notification.id} for sending`,
111
+ {
112
+ userId: notification.userId,
113
+ tokenCount: notification.notificationTokens?.length || 0,
114
+ type: notification.notificationType,
115
+ }
116
+ );
117
+
96
118
  const messages = this.prepareExpoMessage(notification);
97
119
 
98
120
  if (messages.length === 0) {
121
+ const errorMsg = "No valid notification tokens found";
122
+ Logger.error(
123
+ `[NotificationsAdmin] ${errorMsg} for notification ${notification.id}`
124
+ );
99
125
  await this.updateNotificationStatus(
100
126
  notification.id!,
101
127
  NotificationStatus.FAILED,
102
- "No valid notification tokens found"
128
+ errorMsg
103
129
  );
104
130
  return false;
105
131
  }
106
132
 
107
133
  const chunks = this.expo.chunkPushNotifications(messages);
134
+ Logger.info(
135
+ `[NotificationsAdmin] Sending ${messages.length} messages in ${chunks.length} chunks for notification ${notification.id}`
136
+ );
137
+
108
138
  const tickets: ExpoPushTicket[][] = [];
109
139
 
110
140
  // Šaljemo sve chunks
111
- for (const chunk of chunks) {
141
+ for (let i = 0; i < chunks.length; i++) {
142
+ const chunk = chunks[i];
112
143
  try {
144
+ Logger.info(
145
+ `[NotificationsAdmin] Sending chunk ${i + 1}/${
146
+ chunks.length
147
+ } with ${chunk.length} messages`
148
+ );
113
149
  const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
150
+ Logger.info(
151
+ `[NotificationsAdmin] Received ${
152
+ ticketChunk.length
153
+ } tickets for chunk ${i + 1}`
154
+ );
114
155
  tickets.push(ticketChunk);
115
156
  } catch (error) {
116
- console.error(`Chunk sending error:`, error);
157
+ Logger.error(
158
+ `[NotificationsAdmin] Chunk ${i + 1} sending error:`,
159
+ error
160
+ );
117
161
  throw error;
118
162
  }
119
163
  }
@@ -121,25 +165,62 @@ export class NotificationsAdmin {
121
165
  // Proveravamo rezultate
122
166
  let hasErrors = false;
123
167
  const errors: string[] = [];
168
+ const ticketsFlat = tickets.flat();
124
169
 
125
- tickets.flat().forEach((ticket, index) => {
170
+ // Log detailed ticket information
171
+ const ticketResults = {
172
+ total: ticketsFlat.length,
173
+ success: 0,
174
+ error: 0,
175
+ errorDetails: {} as Record<string, number>,
176
+ };
177
+
178
+ ticketsFlat.forEach((ticket, index) => {
126
179
  if (ticket.status === "error") {
127
180
  hasErrors = true;
128
- errors.push(
129
- `Token ${notification.notificationTokens[index]}: ${ticket.message}`
130
- );
181
+ ticketResults.error++;
182
+
183
+ // Count each error type
184
+ const errorMessage = ticket.message || "Unknown error";
185
+ ticketResults.errorDetails[errorMessage] =
186
+ (ticketResults.errorDetails[errorMessage] || 0) + 1;
187
+
188
+ const tokenInfo =
189
+ index < notification.notificationTokens.length
190
+ ? `Token ${notification.notificationTokens[index]}`
191
+ : `Token at index ${index}`;
192
+
193
+ errors.push(`${tokenInfo}: ${errorMessage}`);
194
+ } else {
195
+ ticketResults.success++;
131
196
  }
132
197
  });
133
198
 
199
+ Logger.info(
200
+ `[NotificationsAdmin] Ticket results for notification ${notification.id}`,
201
+ ticketResults
202
+ );
203
+
134
204
  if (hasErrors) {
205
+ const errorSummary = errors.join("; ");
206
+ Logger.warn(
207
+ `[NotificationsAdmin] Partial success or errors in notification ${notification.id}`,
208
+ { errorCount: errors.length, errorSummary }
209
+ );
210
+
135
211
  await this.updateNotificationStatus(
136
212
  notification.id!,
137
- NotificationStatus.PARTIAL_SUCCESS,
138
- errors.join("; ")
213
+ ticketResults.success > 0
214
+ ? NotificationStatus.PARTIAL_SUCCESS
215
+ : NotificationStatus.FAILED,
216
+ errorSummary
139
217
  );
140
- return false;
218
+ return ticketResults.success > 0;
141
219
  }
142
220
 
221
+ Logger.info(
222
+ `[NotificationsAdmin] Successfully sent notification ${notification.id} to all recipients`
223
+ );
143
224
  await this.updateNotificationStatus(
144
225
  notification.id!,
145
226
  NotificationStatus.SENT
@@ -148,7 +229,10 @@ export class NotificationsAdmin {
148
229
  } catch (error) {
149
230
  const errorMessage =
150
231
  error instanceof Error ? error.message : "Unknown error";
151
- console.error("Greška pri slanju notifikacije:", error);
232
+ Logger.error(
233
+ `[NotificationsAdmin] Critical error sending notification ${notification.id}:`,
234
+ error
235
+ );
152
236
  await this.updateNotificationStatus(
153
237
  notification.id!,
154
238
  NotificationStatus.FAILED,
@@ -164,6 +248,10 @@ export class NotificationsAdmin {
164
248
  async processPendingNotifications(batchSize: number = 100): Promise<void> {
165
249
  const now = admin.firestore.Timestamp.now();
166
250
 
251
+ Logger.info(
252
+ `[NotificationsAdmin] Starting to process pending notifications with batch size ${batchSize}`
253
+ );
254
+
167
255
  const pendingNotifications = await this.db
168
256
  .collection("notifications")
169
257
  .where("status", "==", NotificationStatus.PENDING)
@@ -171,12 +259,27 @@ export class NotificationsAdmin {
171
259
  .limit(batchSize)
172
260
  .get();
173
261
 
262
+ Logger.info(
263
+ `[NotificationsAdmin] Found ${pendingNotifications.size} pending notifications to process`
264
+ );
265
+
266
+ if (pendingNotifications.empty) {
267
+ Logger.info(
268
+ "[NotificationsAdmin] No pending notifications found to process"
269
+ );
270
+ return;
271
+ }
272
+
174
273
  const results = await Promise.allSettled(
175
274
  pendingNotifications.docs.map(async (doc) => {
176
275
  const notification = {
177
276
  id: doc.id,
178
277
  ...doc.data(),
179
278
  } as Notification;
279
+
280
+ Logger.info(
281
+ `[NotificationsAdmin] Processing notification ${notification.id} of type ${notification.notificationType}`
282
+ );
180
283
  return this.sendPushNotification(notification);
181
284
  })
182
285
  );
@@ -189,8 +292,8 @@ export class NotificationsAdmin {
189
292
  (r) => r.status === "rejected" || (r.status === "fulfilled" && !r.value)
190
293
  ).length;
191
294
 
192
- console.log(
193
- `Processed ${results.length} notifications: ${successful} successful, ${failed} failed`
295
+ Logger.info(
296
+ `[NotificationsAdmin] Processed ${results.length} notifications: ${successful} successful, ${failed} failed`
194
297
  );
195
298
  }
196
299
 
@@ -204,6 +307,11 @@ export class NotificationsAdmin {
204
307
  const cutoffDate = new Date();
205
308
  cutoffDate.setDate(cutoffDate.getDate() - daysOld);
206
309
 
310
+ Logger.info(
311
+ `[NotificationsAdmin] Starting cleanup of notifications older than ${daysOld} days`
312
+ );
313
+
314
+ let totalDeleted = 0;
207
315
  while (true) {
208
316
  const oldNotifications = await this.db
209
317
  .collection("notifications")
@@ -216,6 +324,9 @@ export class NotificationsAdmin {
216
324
  .get();
217
325
 
218
326
  if (oldNotifications.empty) {
327
+ Logger.info(
328
+ `[NotificationsAdmin] No more old notifications to delete. Total deleted: ${totalDeleted}`
329
+ );
219
330
  break;
220
331
  }
221
332
 
@@ -225,8 +336,9 @@ export class NotificationsAdmin {
225
336
  });
226
337
 
227
338
  await batch.commit();
228
- console.log(
229
- `Deleted batch of ${oldNotifications.size} old notifications`
339
+ totalDeleted += oldNotifications.size;
340
+ Logger.info(
341
+ `[NotificationsAdmin] Deleted batch of ${oldNotifications.size} old notifications. Running total: ${totalDeleted}`
230
342
  );
231
343
  }
232
344
  }
@@ -277,10 +389,7 @@ export class NotificationsAdmin {
277
389
  .toLocaleDateString()} is confirmed.`;
278
390
  }
279
391
 
280
- const adminTsNow = admin.firestore.Timestamp.now();
281
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
282
- adminTsNow
283
- ) as FirebaseClientTimestamp;
392
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
284
393
 
285
394
  const notificationData: Omit<
286
395
  Notification,
@@ -289,7 +398,7 @@ export class NotificationsAdmin {
289
398
  userId: recipientUserId,
290
399
  userRole: recipientRole,
291
400
  notificationType: NotificationType.APPOINTMENT_STATUS_CHANGE,
292
- notificationTime: clientCompatibleNotificationTime,
401
+ notificationTime: notificationTimestampForDb as any,
293
402
  notificationTokens: recipientExpoTokens,
294
403
  title,
295
404
  body,
@@ -353,10 +462,7 @@ export class NotificationsAdmin {
353
462
  }
354
463
  }
355
464
 
356
- const adminTsNow = admin.firestore.Timestamp.now();
357
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
358
- adminTsNow
359
- ) as FirebaseClientTimestamp;
465
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
360
466
 
361
467
  const notificationData: Omit<
362
468
  Notification,
@@ -365,7 +471,7 @@ export class NotificationsAdmin {
365
471
  userId: recipientUserId,
366
472
  userRole: recipientRole,
367
473
  notificationType: NotificationType.APPOINTMENT_CANCELLED,
368
- notificationTime: clientCompatibleNotificationTime,
474
+ notificationTime: notificationTimestampForDb as any,
369
475
  notificationTokens: recipientExpoTokens,
370
476
  title,
371
477
  body,
@@ -404,10 +510,7 @@ export class NotificationsAdmin {
404
510
  const title = "Appointment Reschedule Proposed";
405
511
  const body = `Action Required: A new time has been proposed for your appointment for ${appointment.procedureInfo.name}. Please review in the app.`;
406
512
 
407
- const adminTsNow = admin.firestore.Timestamp.now();
408
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
409
- adminTsNow
410
- ) as FirebaseClientTimestamp;
513
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
411
514
 
412
515
  const notificationData: Omit<
413
516
  Notification,
@@ -416,7 +519,7 @@ export class NotificationsAdmin {
416
519
  userId: patientUserId,
417
520
  userRole: UserRole.PATIENT,
418
521
  notificationType: NotificationType.APPOINTMENT_RESCHEDULED_PROPOSAL,
419
- notificationTime: clientCompatibleNotificationTime,
522
+ notificationTime: notificationTimestampForDb as any,
420
523
  notificationTokens: patientExpoTokens,
421
524
  title,
422
525
  body,
@@ -458,10 +561,7 @@ export class NotificationsAdmin {
458
561
  .toDate()
459
562
  .toLocaleDateString()} is now ${appointment.paymentStatus}.`;
460
563
 
461
- const adminTsNow = admin.firestore.Timestamp.now();
462
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
463
- adminTsNow
464
- ) as FirebaseClientTimestamp;
564
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
465
565
 
466
566
  const notificationType =
467
567
  appointment.paymentStatus === PaymentStatus.PAID
@@ -475,7 +575,7 @@ export class NotificationsAdmin {
475
575
  userId: patientUserId,
476
576
  userRole: UserRole.PATIENT,
477
577
  notificationType: notificationType,
478
- notificationTime: clientCompatibleNotificationTime,
578
+ notificationTime: notificationTimestampForDb as any,
479
579
  notificationTokens: patientExpoTokens,
480
580
  title,
481
581
  body,
@@ -514,10 +614,7 @@ export class NotificationsAdmin {
514
614
  const title = "Leave a Review";
515
615
  const body = `How was your recent appointment for ${appointment.procedureInfo.name}? We'd love to hear your feedback!`;
516
616
 
517
- const adminTsNow = admin.firestore.Timestamp.now();
518
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
519
- adminTsNow
520
- ) as FirebaseClientTimestamp;
617
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
521
618
 
522
619
  const notificationData: Omit<
523
620
  Notification,
@@ -526,7 +623,7 @@ export class NotificationsAdmin {
526
623
  userId: patientUserId,
527
624
  userRole: UserRole.PATIENT,
528
625
  notificationType: NotificationType.REVIEW_REQUEST,
529
- notificationTime: clientCompatibleNotificationTime,
626
+ notificationTime: notificationTimestampForDb as any,
530
627
  notificationTokens: patientExpoTokens,
531
628
  title,
532
629
  body,
@@ -576,10 +673,7 @@ export class NotificationsAdmin {
576
673
  .toDate()
577
674
  .toLocaleDateString()}.`;
578
675
 
579
- const adminTsNow = admin.firestore.Timestamp.now();
580
- const clientCompatibleNotificationTime = TimestampUtils.adminToClient(
581
- adminTsNow
582
- ) as FirebaseClientTimestamp;
676
+ const notificationTimestampForDb = admin.firestore.Timestamp.now();
583
677
 
584
678
  const tempNotificationType = NotificationType.GENERAL_MESSAGE;
585
679
 
@@ -590,7 +684,7 @@ export class NotificationsAdmin {
590
684
  userId: recipientUserId,
591
685
  userRole: UserRole.PRACTITIONER,
592
686
  notificationType: tempNotificationType,
593
- notificationTime: clientCompatibleNotificationTime,
687
+ notificationTime: notificationTimestampForDb as any,
594
688
  notificationTokens: recipientExpoTokens,
595
689
  title,
596
690
  body,
@@ -176,7 +176,7 @@ export class PatientRequirementsAdminService {
176
176
  userId: patientId,
177
177
  userRole: UserRole.PATIENT,
178
178
  notificationType: NotificationType.REQUIREMENT_INSTRUCTION_DUE,
179
- notificationTime: currentInstruction.dueTime, // This is a Firestore Timestamp
179
+ notificationTime: currentInstruction.dueTime as any, // dueTime should be an admin.firestore.Timestamp already
180
180
  notificationTokens: patientExpoTokens,
181
181
  title: `Reminder: ${instance.requirementName}`,
182
182
  body: currentInstruction.instructionText,