@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/dist/admin/index.d.mts +343 -2
- package/dist/admin/index.d.ts +343 -2
- package/dist/admin/index.js +390 -217
- package/dist/admin/index.mjs +382 -217
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +2 -2
- package/src/admin/booking/booking.admin.ts +8 -4
- package/src/admin/calendar/calendar.admin.service.ts +8 -11
- package/src/admin/index.ts +21 -0
- package/src/admin/notifications/notifications.admin.ts +152 -58
- package/src/admin/requirements/patient-requirements.admin.service.ts +1 -1
- package/src/utils/TimestampUtils.ts +122 -17
package/dist/admin/index.mjs
CHANGED
|
@@ -37,7 +37,7 @@ var NotificationStatus = /* @__PURE__ */ ((NotificationStatus2) => {
|
|
|
37
37
|
})(NotificationStatus || {});
|
|
38
38
|
|
|
39
39
|
// src/admin/notifications/notifications.admin.ts
|
|
40
|
-
import * as
|
|
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/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
*
|
|
113
|
-
* @param
|
|
114
|
-
* @
|
|
99
|
+
* Log an error message
|
|
100
|
+
* @param message Message to log
|
|
101
|
+
* @param data Optional data to include
|
|
115
102
|
*/
|
|
116
|
-
static
|
|
117
|
-
if (
|
|
118
|
-
|
|
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
|
-
*
|
|
122
|
-
* @param
|
|
123
|
-
* @
|
|
111
|
+
* Log a warning message
|
|
112
|
+
* @param message Message to log
|
|
113
|
+
* @param data Optional data to include
|
|
124
114
|
*/
|
|
125
|
-
static
|
|
126
|
-
if (
|
|
127
|
-
|
|
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
|
-
*
|
|
131
|
-
* @param
|
|
132
|
-
* @
|
|
123
|
+
* Log an info message
|
|
124
|
+
* @param message Message to log
|
|
125
|
+
* @param data Optional data to include
|
|
133
126
|
*/
|
|
134
|
-
static
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
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
|
-
*
|
|
156
|
-
* @param
|
|
157
|
-
* @
|
|
135
|
+
* Log a debug message
|
|
136
|
+
* @param message Message to log
|
|
137
|
+
* @param data Optional data to include
|
|
158
138
|
*/
|
|
159
|
-
static
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
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 ||
|
|
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:
|
|
202
|
-
updatedAt:
|
|
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
|
-
|
|
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:
|
|
206
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp()
|
|
229
207
|
};
|
|
230
208
|
if (status === "sent" /* SENT */) {
|
|
231
|
-
update.sentAt =
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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()
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
300
|
+
ticketResults.success > 0 ? "partialSuccess" /* PARTIAL_SUCCESS */ : "failed" /* FAILED */,
|
|
301
|
+
errorSummary
|
|
278
302
|
);
|
|
279
|
-
return
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
4503
|
-
|
|
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,
|