@blackcode_sa/metaestetics-api 1.12.62 → 1.12.63
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 +4 -2
- package/dist/admin/index.d.ts +4 -2
- package/dist/admin/index.js +4 -45
- package/dist/admin/index.mjs +4 -45
- package/dist/backoffice/index.d.mts +9 -0
- package/dist/backoffice/index.d.ts +9 -0
- package/dist/backoffice/index.js +11 -0
- package/dist/backoffice/index.mjs +11 -0
- package/dist/index.d.mts +99 -3
- package/dist/index.d.ts +99 -3
- package/dist/index.js +545 -281
- package/dist/index.mjs +867 -603
- package/package.json +119 -119
- package/src/__mocks__/firstore.ts +10 -10
- package/src/admin/aggregation/README.md +79 -79
- package/src/admin/aggregation/appointment/README.md +128 -128
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1844 -1844
- package/src/admin/aggregation/appointment/index.ts +1 -1
- package/src/admin/aggregation/clinic/README.md +52 -52
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
- package/src/admin/aggregation/clinic/index.ts +1 -1
- package/src/admin/aggregation/forms/README.md +13 -13
- package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
- package/src/admin/aggregation/forms/index.ts +1 -1
- package/src/admin/aggregation/index.ts +8 -8
- package/src/admin/aggregation/patient/README.md +27 -27
- package/src/admin/aggregation/patient/index.ts +1 -1
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
- package/src/admin/aggregation/practitioner/README.md +42 -42
- package/src/admin/aggregation/practitioner/index.ts +1 -1
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
- package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
- package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
- package/src/admin/aggregation/procedure/README.md +43 -43
- package/src/admin/aggregation/procedure/index.ts +1 -1
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
- package/src/admin/aggregation/reviews/index.ts +1 -1
- package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +641 -689
- package/src/admin/booking/README.md +125 -125
- package/src/admin/booking/booking.admin.ts +1037 -1037
- package/src/admin/booking/booking.calculator.ts +712 -712
- package/src/admin/booking/booking.types.ts +59 -59
- package/src/admin/booking/index.ts +3 -3
- package/src/admin/booking/timezones-problem.md +185 -185
- package/src/admin/calendar/README.md +7 -7
- package/src/admin/calendar/calendar.admin.service.ts +345 -345
- package/src/admin/calendar/index.ts +1 -1
- package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
- package/src/admin/documentation-templates/index.ts +1 -1
- package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
- package/src/admin/free-consultation/index.ts +1 -1
- package/src/admin/index.ts +75 -75
- package/src/admin/logger/index.ts +78 -78
- package/src/admin/mailing/README.md +95 -95
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
- package/src/admin/mailing/appointment/index.ts +1 -1
- package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
- package/src/admin/mailing/base.mailing.service.ts +208 -208
- package/src/admin/mailing/index.ts +3 -3
- package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
- package/src/admin/mailing/practitionerInvite/index.ts +2 -2
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
- package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
- package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
- package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
- package/src/admin/notifications/index.ts +1 -1
- package/src/admin/notifications/notifications.admin.ts +710 -710
- package/src/admin/requirements/README.md +128 -128
- package/src/admin/requirements/index.ts +1 -1
- package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
- package/src/admin/users/index.ts +1 -1
- package/src/admin/users/user-profile.admin.ts +405 -405
- package/src/backoffice/constants/certification.constants.ts +13 -13
- package/src/backoffice/constants/index.ts +1 -1
- package/src/backoffice/errors/backoffice.errors.ts +181 -181
- package/src/backoffice/errors/index.ts +1 -1
- package/src/backoffice/expo-safe/README.md +26 -26
- package/src/backoffice/expo-safe/index.ts +41 -41
- package/src/backoffice/index.ts +5 -5
- package/src/backoffice/services/FIXES_README.md +102 -102
- package/src/backoffice/services/README.md +40 -40
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +318 -318
- package/src/backoffice/services/constants.service.ts +385 -385
- package/src/backoffice/services/documentation-template.service.ts +202 -202
- package/src/backoffice/services/index.ts +8 -8
- package/src/backoffice/services/migrate-products.ts +116 -116
- package/src/backoffice/services/product.service.ts +553 -553
- package/src/backoffice/services/requirement.service.ts +235 -235
- package/src/backoffice/services/subcategory.service.ts +395 -395
- package/src/backoffice/services/technology.service.ts +1083 -1070
- package/src/backoffice/types/README.md +12 -12
- package/src/backoffice/types/admin-constants.types.ts +69 -69
- package/src/backoffice/types/brand.types.ts +29 -29
- package/src/backoffice/types/category.types.ts +62 -62
- package/src/backoffice/types/documentation-templates.types.ts +28 -28
- package/src/backoffice/types/index.ts +10 -10
- package/src/backoffice/types/procedure-product.types.ts +38 -38
- package/src/backoffice/types/product.types.ts +240 -240
- package/src/backoffice/types/requirement.types.ts +63 -63
- package/src/backoffice/types/static/README.md +18 -18
- package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
- package/src/backoffice/types/static/certification.types.ts +37 -37
- package/src/backoffice/types/static/contraindication.types.ts +19 -19
- package/src/backoffice/types/static/index.ts +6 -6
- package/src/backoffice/types/static/pricing.types.ts +16 -16
- package/src/backoffice/types/static/procedure-family.types.ts +14 -14
- package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
- package/src/backoffice/types/subcategory.types.ts +34 -34
- package/src/backoffice/types/technology.types.ts +163 -161
- package/src/backoffice/validations/index.ts +1 -1
- package/src/backoffice/validations/schemas.ts +164 -163
- package/src/config/__mocks__/firebase.ts +99 -99
- package/src/config/firebase.ts +78 -78
- package/src/config/index.ts +9 -9
- package/src/errors/auth.error.ts +6 -6
- package/src/errors/auth.errors.ts +200 -200
- package/src/errors/clinic.errors.ts +32 -32
- package/src/errors/firebase.errors.ts +47 -47
- package/src/errors/user.errors.ts +99 -99
- package/src/index.backup.ts +407 -407
- package/src/index.ts +6 -6
- package/src/locales/en.ts +31 -31
- package/src/recommender/admin/index.ts +1 -1
- package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
- package/src/recommender/front/index.ts +1 -1
- package/src/recommender/front/services/onboarding.service.ts +5 -5
- package/src/recommender/front/services/recommender.service.ts +3 -3
- package/src/recommender/index.ts +1 -1
- package/src/services/PATIENTAUTH.MD +197 -197
- package/src/services/README.md +106 -106
- package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
- package/src/services/__tests__/auth/auth.setup.ts +293 -293
- package/src/services/__tests__/auth.service.test.ts +346 -346
- package/src/services/__tests__/base.service.test.ts +77 -77
- package/src/services/__tests__/user.service.test.ts +528 -528
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2505 -2082
- package/src/services/appointment/index.ts +1 -1
- package/src/services/appointment/utils/appointment.utils.ts +552 -552
- package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
- package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
- package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
- package/src/services/appointment/utils/zone-management.utils.ts +353 -353
- package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
- package/src/services/auth/auth.service.ts +989 -989
- package/src/services/auth/auth.v2.service.ts +961 -961
- package/src/services/auth/index.ts +7 -7
- package/src/services/auth/utils/error.utils.ts +90 -90
- package/src/services/auth/utils/firebase.utils.ts +49 -49
- package/src/services/auth/utils/index.ts +21 -21
- package/src/services/auth/utils/practitioner.utils.ts +125 -125
- package/src/services/base.service.ts +41 -41
- package/src/services/calendar/calendar.service.ts +1077 -1077
- package/src/services/calendar/calendar.v2.service.ts +1683 -1683
- package/src/services/calendar/calendar.v3.service.ts +313 -313
- package/src/services/calendar/externalCalendar.service.ts +178 -178
- package/src/services/calendar/index.ts +5 -5
- package/src/services/calendar/synced-calendars.service.ts +743 -743
- package/src/services/calendar/utils/appointment.utils.ts +265 -265
- package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
- package/src/services/calendar/utils/clinic.utils.ts +237 -237
- package/src/services/calendar/utils/docs.utils.ts +157 -157
- package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
- package/src/services/calendar/utils/index.ts +8 -8
- package/src/services/calendar/utils/patient.utils.ts +198 -198
- package/src/services/calendar/utils/practitioner.utils.ts +221 -221
- package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
- package/src/services/clinic/README.md +204 -204
- package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
- package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
- package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
- package/src/services/clinic/billing-transactions.service.ts +217 -217
- package/src/services/clinic/clinic-admin.service.ts +202 -202
- package/src/services/clinic/clinic-group.service.ts +310 -310
- package/src/services/clinic/clinic.service.ts +708 -708
- package/src/services/clinic/index.ts +5 -5
- package/src/services/clinic/practitioner-invite.service.ts +519 -519
- package/src/services/clinic/utils/admin.utils.ts +551 -551
- package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
- package/src/services/clinic/utils/clinic.utils.ts +949 -949
- package/src/services/clinic/utils/filter.utils.d.ts +23 -23
- package/src/services/clinic/utils/filter.utils.ts +446 -446
- package/src/services/clinic/utils/index.ts +11 -11
- package/src/services/clinic/utils/photos.utils.ts +188 -188
- package/src/services/clinic/utils/search.utils.ts +84 -84
- package/src/services/clinic/utils/tag.utils.ts +124 -124
- package/src/services/documentation-templates/documentation-template.service.ts +537 -537
- package/src/services/documentation-templates/filled-document.service.ts +587 -587
- package/src/services/documentation-templates/index.ts +2 -2
- package/src/services/index.ts +13 -13
- package/src/services/media/index.ts +1 -1
- package/src/services/media/media.service.ts +418 -418
- package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/notification.service.ts +215 -215
- package/src/services/patient/README.md +48 -48
- package/src/services/patient/To-Do.md +43 -43
- package/src/services/patient/__tests__/patient.service.test.ts +294 -294
- package/src/services/patient/index.ts +2 -2
- package/src/services/patient/patient.service.ts +883 -883
- package/src/services/patient/patientRequirements.service.ts +285 -285
- package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
- package/src/services/patient/utils/clinic.utils.ts +80 -80
- package/src/services/patient/utils/docs.utils.ts +142 -142
- package/src/services/patient/utils/index.ts +9 -9
- package/src/services/patient/utils/location.utils.ts +126 -126
- package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
- package/src/services/patient/utils/medical.utils.ts +458 -458
- package/src/services/patient/utils/practitioner.utils.ts +260 -260
- package/src/services/patient/utils/profile.utils.ts +510 -510
- package/src/services/patient/utils/sensitive.utils.ts +260 -260
- package/src/services/patient/utils/token.utils.ts +211 -211
- package/src/services/practitioner/README.md +145 -145
- package/src/services/practitioner/index.ts +1 -1
- package/src/services/practitioner/practitioner.service.ts +1742 -1742
- package/src/services/procedure/README.md +163 -163
- package/src/services/procedure/index.ts +1 -1
- package/src/services/procedure/procedure.service.ts +1682 -1682
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +636 -683
- package/src/services/user/index.ts +1 -1
- package/src/services/user/user.service.ts +489 -489
- package/src/services/user/user.v2.service.ts +466 -466
- package/src/types/appointment/index.ts +481 -453
- package/src/types/calendar/index.ts +258 -258
- package/src/types/calendar/synced-calendar.types.ts +66 -66
- package/src/types/clinic/index.ts +489 -489
- package/src/types/clinic/practitioner-invite.types.ts +91 -91
- package/src/types/clinic/preferences.types.ts +159 -159
- package/src/types/clinic/to-do +3 -3
- package/src/types/documentation-templates/index.ts +308 -308
- package/src/types/index.ts +44 -44
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +265 -265
- package/src/types/patient/aesthetic-analysis.types.ts +66 -66
- package/src/types/patient/allergies.ts +58 -58
- package/src/types/patient/index.ts +275 -273
- package/src/types/patient/medical-info.types.ts +152 -152
- package/src/types/patient/patient-requirements.ts +92 -92
- package/src/types/patient/token.types.ts +61 -61
- package/src/types/practitioner/index.ts +206 -206
- package/src/types/procedure/index.ts +181 -181
- package/src/types/profile/index.ts +39 -39
- package/src/types/reviews/index.ts +130 -132
- package/src/types/tz-lookup.d.ts +4 -4
- package/src/types/user/index.ts +38 -38
- package/src/utils/TIMESTAMPS.md +176 -176
- package/src/utils/TimestampUtils.ts +241 -241
- package/src/utils/index.ts +1 -1
- package/src/validations/appointment.schema.ts +574 -574
- package/src/validations/calendar.schema.ts +225 -225
- package/src/validations/clinic.schema.ts +493 -493
- package/src/validations/common.schema.ts +25 -25
- package/src/validations/documentation-templates/index.ts +1 -1
- package/src/validations/documentation-templates/template.schema.ts +220 -220
- package/src/validations/documentation-templates.schema.ts +10 -10
- package/src/validations/index.ts +20 -20
- package/src/validations/media.schema.ts +10 -10
- package/src/validations/notification.schema.ts +90 -90
- package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
- package/src/validations/patient/medical-info.schema.ts +125 -125
- package/src/validations/patient/patient-requirements.schema.ts +84 -84
- package/src/validations/patient/token.schema.ts +29 -29
- package/src/validations/patient.schema.ts +217 -216
- package/src/validations/practitioner.schema.ts +222 -222
- package/src/validations/procedure-product.schema.ts +41 -41
- package/src/validations/procedure.schema.ts +124 -124
- package/src/validations/profile-info.schema.ts +41 -41
- package/src/validations/reviews.schema.ts +189 -195
- package/src/validations/schemas.ts +104 -104
- package/src/validations/shared.schema.ts +78 -78
|
@@ -1,710 +1,710 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Notification,
|
|
3
|
-
NotificationStatus,
|
|
4
|
-
NotificationType,
|
|
5
|
-
} from "../../types/notifications";
|
|
6
|
-
import * as admin from "firebase-admin";
|
|
7
|
-
import { Expo, ExpoPushMessage, ExpoPushTicket } from "expo-server-sdk";
|
|
8
|
-
import { Appointment, PaymentStatus } from "../../types/appointment";
|
|
9
|
-
import { UserRole } from "../../types";
|
|
10
|
-
import { Timestamp as FirebaseClientTimestamp } from "@firebase/firestore";
|
|
11
|
-
import { TimestampUtils } from "../../utils/TimestampUtils";
|
|
12
|
-
import { Logger } from "../logger";
|
|
13
|
-
|
|
14
|
-
export class NotificationsAdmin {
|
|
15
|
-
private expo: Expo;
|
|
16
|
-
private db: admin.firestore.Firestore;
|
|
17
|
-
|
|
18
|
-
constructor(firestore?: admin.firestore.Firestore) {
|
|
19
|
-
this.expo = new Expo();
|
|
20
|
-
this.db = firestore || admin.firestore();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Dohvata notifikaciju po ID-u
|
|
25
|
-
*/
|
|
26
|
-
async getNotification(id: string): Promise<Notification | null> {
|
|
27
|
-
const doc = await this.db.collection("notifications").doc(id).get();
|
|
28
|
-
return doc.exists ? ({ id: doc.id, ...doc.data() } as Notification) : null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Kreira novu notifikaciju
|
|
33
|
-
*/
|
|
34
|
-
async createNotification(
|
|
35
|
-
notification: Omit<Notification, "id">
|
|
36
|
-
): Promise<string> {
|
|
37
|
-
const docRef = await this.db.collection("notifications").add({
|
|
38
|
-
...notification,
|
|
39
|
-
status: NotificationStatus.PENDING,
|
|
40
|
-
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
41
|
-
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
42
|
-
});
|
|
43
|
-
return docRef.id;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Priprema Expo poruku za slanje
|
|
48
|
-
*/
|
|
49
|
-
private prepareExpoMessage(notification: Notification): ExpoPushMessage[] {
|
|
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
|
-
}));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Ažurira status notifikacije
|
|
79
|
-
*/
|
|
80
|
-
public async updateNotificationStatus(
|
|
81
|
-
notificationId: string,
|
|
82
|
-
status: NotificationStatus,
|
|
83
|
-
error?: string
|
|
84
|
-
): Promise<void> {
|
|
85
|
-
const update: any = {
|
|
86
|
-
status,
|
|
87
|
-
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
if (status === NotificationStatus.SENT) {
|
|
91
|
-
update.sentAt = admin.firestore.FieldValue.serverTimestamp();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (error) {
|
|
95
|
-
update.error = error;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
await this.db
|
|
99
|
-
.collection("notifications")
|
|
100
|
-
.doc(notificationId)
|
|
101
|
-
.update(update);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Šalje notifikaciju kroz Expo servis sa boljim error handlingom
|
|
106
|
-
*/
|
|
107
|
-
async sendPushNotification(notification: Notification): Promise<boolean> {
|
|
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
|
-
|
|
118
|
-
const messages = this.prepareExpoMessage(notification);
|
|
119
|
-
|
|
120
|
-
if (messages.length === 0) {
|
|
121
|
-
const errorMsg = "No valid notification tokens found";
|
|
122
|
-
Logger.error(
|
|
123
|
-
`[NotificationsAdmin] ${errorMsg} for notification ${notification.id}`
|
|
124
|
-
);
|
|
125
|
-
await this.updateNotificationStatus(
|
|
126
|
-
notification.id!,
|
|
127
|
-
NotificationStatus.FAILED,
|
|
128
|
-
errorMsg
|
|
129
|
-
);
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
|
|
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
|
-
|
|
138
|
-
const tickets: ExpoPushTicket[][] = [];
|
|
139
|
-
|
|
140
|
-
// Šaljemo sve chunks
|
|
141
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
142
|
-
const chunk = chunks[i];
|
|
143
|
-
try {
|
|
144
|
-
Logger.info(
|
|
145
|
-
`[NotificationsAdmin] Sending chunk ${i + 1}/${
|
|
146
|
-
chunks.length
|
|
147
|
-
} with ${chunk.length} messages`
|
|
148
|
-
);
|
|
149
|
-
const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
|
|
150
|
-
Logger.info(
|
|
151
|
-
`[NotificationsAdmin] Received ${
|
|
152
|
-
ticketChunk.length
|
|
153
|
-
} tickets for chunk ${i + 1}`
|
|
154
|
-
);
|
|
155
|
-
tickets.push(ticketChunk);
|
|
156
|
-
} catch (error) {
|
|
157
|
-
Logger.error(
|
|
158
|
-
`[NotificationsAdmin] Chunk ${i + 1} sending error:`,
|
|
159
|
-
error
|
|
160
|
-
);
|
|
161
|
-
throw error;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Proveravamo rezultate
|
|
166
|
-
let hasErrors = false;
|
|
167
|
-
const errors: string[] = [];
|
|
168
|
-
const ticketsFlat = tickets.flat();
|
|
169
|
-
|
|
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) => {
|
|
179
|
-
if (ticket.status === "error") {
|
|
180
|
-
hasErrors = true;
|
|
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++;
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
Logger.info(
|
|
200
|
-
`[NotificationsAdmin] Ticket results for notification ${notification.id}`,
|
|
201
|
-
ticketResults
|
|
202
|
-
);
|
|
203
|
-
|
|
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
|
-
|
|
211
|
-
await this.updateNotificationStatus(
|
|
212
|
-
notification.id!,
|
|
213
|
-
ticketResults.success > 0
|
|
214
|
-
? NotificationStatus.PARTIAL_SUCCESS
|
|
215
|
-
: NotificationStatus.FAILED,
|
|
216
|
-
errorSummary
|
|
217
|
-
);
|
|
218
|
-
return ticketResults.success > 0;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
Logger.info(
|
|
222
|
-
`[NotificationsAdmin] Successfully sent notification ${notification.id} to all recipients`
|
|
223
|
-
);
|
|
224
|
-
await this.updateNotificationStatus(
|
|
225
|
-
notification.id!,
|
|
226
|
-
NotificationStatus.SENT
|
|
227
|
-
);
|
|
228
|
-
return true;
|
|
229
|
-
} catch (error) {
|
|
230
|
-
const errorMessage =
|
|
231
|
-
error instanceof Error ? error.message : "Unknown error";
|
|
232
|
-
Logger.error(
|
|
233
|
-
`[NotificationsAdmin] Critical error sending notification ${notification.id}:`,
|
|
234
|
-
error
|
|
235
|
-
);
|
|
236
|
-
await this.updateNotificationStatus(
|
|
237
|
-
notification.id!,
|
|
238
|
-
NotificationStatus.FAILED,
|
|
239
|
-
errorMessage
|
|
240
|
-
);
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Procesira notifikacije koje čekaju na slanje sa batch limitom
|
|
247
|
-
*/
|
|
248
|
-
async processPendingNotifications(batchSize: number = 100): Promise<void> {
|
|
249
|
-
const now = admin.firestore.Timestamp.now();
|
|
250
|
-
|
|
251
|
-
Logger.info(
|
|
252
|
-
`[NotificationsAdmin] Starting to process pending notifications with batch size ${batchSize}`
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
const pendingNotifications = await this.db
|
|
256
|
-
.collection("notifications")
|
|
257
|
-
.where("status", "==", NotificationStatus.PENDING)
|
|
258
|
-
.where("notificationTime", "<=", now)
|
|
259
|
-
.limit(batchSize)
|
|
260
|
-
.get();
|
|
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
|
-
|
|
273
|
-
const results = await Promise.allSettled(
|
|
274
|
-
pendingNotifications.docs.map(async (doc) => {
|
|
275
|
-
const notification = {
|
|
276
|
-
id: doc.id,
|
|
277
|
-
...doc.data(),
|
|
278
|
-
} as Notification;
|
|
279
|
-
|
|
280
|
-
Logger.info(
|
|
281
|
-
`[NotificationsAdmin] Processing notification ${notification.id} of type ${notification.notificationType}`
|
|
282
|
-
);
|
|
283
|
-
return this.sendPushNotification(notification);
|
|
284
|
-
})
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
// Logujemo statistiku
|
|
288
|
-
const successful = results.filter(
|
|
289
|
-
(r) => r.status === "fulfilled" && r.value
|
|
290
|
-
).length;
|
|
291
|
-
const failed = results.filter(
|
|
292
|
-
(r) => r.status === "rejected" || (r.status === "fulfilled" && !r.value)
|
|
293
|
-
).length;
|
|
294
|
-
|
|
295
|
-
Logger.info(
|
|
296
|
-
`[NotificationsAdmin] Processed ${results.length} notifications: ${successful} successful, ${failed} failed`
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Briše stare notifikacije sa batch procesiranjem
|
|
302
|
-
*/
|
|
303
|
-
async cleanupOldNotifications(
|
|
304
|
-
daysOld: number = 30,
|
|
305
|
-
batchSize: number = 500
|
|
306
|
-
): Promise<void> {
|
|
307
|
-
const cutoffDate = new Date();
|
|
308
|
-
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
|
309
|
-
|
|
310
|
-
Logger.info(
|
|
311
|
-
`[NotificationsAdmin] Starting cleanup of notifications older than ${daysOld} days`
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
let totalDeleted = 0;
|
|
315
|
-
while (true) {
|
|
316
|
-
const oldNotifications = await this.db
|
|
317
|
-
.collection("notifications")
|
|
318
|
-
.where(
|
|
319
|
-
"createdAt",
|
|
320
|
-
"<=",
|
|
321
|
-
admin.firestore.Timestamp.fromDate(cutoffDate)
|
|
322
|
-
)
|
|
323
|
-
.limit(batchSize)
|
|
324
|
-
.get();
|
|
325
|
-
|
|
326
|
-
if (oldNotifications.empty) {
|
|
327
|
-
Logger.info(
|
|
328
|
-
`[NotificationsAdmin] No more old notifications to delete. Total deleted: ${totalDeleted}`
|
|
329
|
-
);
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const batch = this.db.batch();
|
|
334
|
-
oldNotifications.docs.forEach((doc) => {
|
|
335
|
-
batch.delete(doc.ref);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
await batch.commit();
|
|
339
|
-
totalDeleted += oldNotifications.size;
|
|
340
|
-
Logger.info(
|
|
341
|
-
`[NotificationsAdmin] Deleted batch of ${oldNotifications.size} old notifications. Running total: ${totalDeleted}`
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// --- Business Specific Notification Methods (Revised) ---
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Creates and potentially sends a push notification for a confirmed appointment.
|
|
350
|
-
* @param appointment The confirmed appointment object.
|
|
351
|
-
* @param recipientUserId The ID of the user receiving the notification.
|
|
352
|
-
* @param recipientExpoTokens Array of Expo push tokens for the recipient.
|
|
353
|
-
* @param recipientRole The role of the recipient (e.g., PATIENT, PRACTITIONER).
|
|
354
|
-
*/
|
|
355
|
-
async sendAppointmentConfirmedPush(
|
|
356
|
-
appointment: Appointment,
|
|
357
|
-
recipientUserId: string,
|
|
358
|
-
recipientExpoTokens: string[],
|
|
359
|
-
recipientRole: UserRole
|
|
360
|
-
): Promise<string | null> {
|
|
361
|
-
if (recipientRole === UserRole.CLINIC_ADMIN) {
|
|
362
|
-
console.log(
|
|
363
|
-
`[NotificationsAdmin] Clinic admin roles do not receive push notifications for appointment ${appointment.id} confirmation. Skipping.`
|
|
364
|
-
);
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
if (!recipientExpoTokens || recipientExpoTokens.length === 0) {
|
|
368
|
-
console.log(
|
|
369
|
-
`[NotificationsAdmin] No expo tokens for ${recipientRole} ${recipientUserId} for appointment ${appointment.id} confirmation. Skipping push.`
|
|
370
|
-
);
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
let title = "Appointment Confirmed!";
|
|
375
|
-
let body = `Your appointment for ${
|
|
376
|
-
appointment.procedureInfo.name
|
|
377
|
-
} on ${appointment.appointmentStartTime
|
|
378
|
-
.toDate()
|
|
379
|
-
.toLocaleDateString()} at ${appointment.appointmentStartTime
|
|
380
|
-
.toDate()
|
|
381
|
-
.toLocaleTimeString()} has been confirmed.`;
|
|
382
|
-
|
|
383
|
-
if (recipientRole === UserRole.PRACTITIONER) {
|
|
384
|
-
title = "New Appointment Confirmed";
|
|
385
|
-
body = `Appointment for ${appointment.procedureInfo.name} with ${
|
|
386
|
-
appointment.patientInfo.fullName
|
|
387
|
-
} on ${appointment.appointmentStartTime
|
|
388
|
-
.toDate()
|
|
389
|
-
.toLocaleDateString()} is confirmed.`;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const notificationTimestampForDb = admin.firestore.Timestamp.now();
|
|
393
|
-
|
|
394
|
-
const notificationData: Omit<
|
|
395
|
-
Notification,
|
|
396
|
-
"id" | "createdAt" | "updatedAt" | "status" | "isRead"
|
|
397
|
-
> = {
|
|
398
|
-
userId: recipientUserId,
|
|
399
|
-
userRole: recipientRole,
|
|
400
|
-
notificationType: NotificationType.APPOINTMENT_STATUS_CHANGE,
|
|
401
|
-
notificationTime: notificationTimestampForDb as any,
|
|
402
|
-
notificationTokens: recipientExpoTokens,
|
|
403
|
-
title,
|
|
404
|
-
body,
|
|
405
|
-
appointmentId: appointment.id,
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
const notificationId = await this.createNotification(
|
|
410
|
-
notificationData as Notification
|
|
411
|
-
);
|
|
412
|
-
console.log(
|
|
413
|
-
`[NotificationsAdmin] Created APPOINTMENT_STATUS_CHANGE (Confirmed) notification ${notificationId} for ${recipientRole} ${recipientUserId}.`
|
|
414
|
-
);
|
|
415
|
-
return notificationId;
|
|
416
|
-
} catch (error) {
|
|
417
|
-
console.error(
|
|
418
|
-
`[NotificationsAdmin] Error creating APPOINTMENT_STATUS_CHANGE (Confirmed) notification for ${recipientRole} ${recipientUserId}:`,
|
|
419
|
-
error
|
|
420
|
-
);
|
|
421
|
-
return null;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
async sendAppointmentCancelledPush(
|
|
426
|
-
appointment: Appointment,
|
|
427
|
-
recipientUserId: string,
|
|
428
|
-
recipientExpoTokens: string[],
|
|
429
|
-
recipientRole: UserRole
|
|
430
|
-
): Promise<string | null> {
|
|
431
|
-
if (recipientRole === UserRole.CLINIC_ADMIN) {
|
|
432
|
-
console.log(
|
|
433
|
-
`[NotificationsAdmin] Clinic admin roles do not receive push notifications for appointment ${appointment.id} cancellation. Skipping.`
|
|
434
|
-
);
|
|
435
|
-
return null;
|
|
436
|
-
}
|
|
437
|
-
if (!recipientExpoTokens || recipientExpoTokens.length === 0) {
|
|
438
|
-
console.log(
|
|
439
|
-
`[NotificationsAdmin] No expo tokens for ${recipientRole} ${recipientUserId} for appointment ${appointment.id} cancellation. Skipping push.`
|
|
440
|
-
);
|
|
441
|
-
return null;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
let title = "Appointment Cancelled";
|
|
445
|
-
let body = `Your appointment for ${
|
|
446
|
-
appointment.procedureInfo.name
|
|
447
|
-
} on ${appointment.appointmentStartTime
|
|
448
|
-
.toDate()
|
|
449
|
-
.toLocaleDateString()} has been cancelled.`;
|
|
450
|
-
if (appointment.cancellationReason) {
|
|
451
|
-
body += ` Reason: ${appointment.cancellationReason}`;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
if (recipientRole === UserRole.PRACTITIONER) {
|
|
455
|
-
body = `The appointment for ${appointment.procedureInfo.name} with ${
|
|
456
|
-
appointment.patientInfo.fullName
|
|
457
|
-
} on ${appointment.appointmentStartTime
|
|
458
|
-
.toDate()
|
|
459
|
-
.toLocaleDateString()} has been cancelled.`;
|
|
460
|
-
if (appointment.cancellationReason) {
|
|
461
|
-
body += ` Reason: ${appointment.cancellationReason}`;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const notificationTimestampForDb = admin.firestore.Timestamp.now();
|
|
466
|
-
|
|
467
|
-
const notificationData: Omit<
|
|
468
|
-
Notification,
|
|
469
|
-
"id" | "createdAt" | "updatedAt" | "status" | "isRead"
|
|
470
|
-
> = {
|
|
471
|
-
userId: recipientUserId,
|
|
472
|
-
userRole: recipientRole,
|
|
473
|
-
notificationType: NotificationType.APPOINTMENT_CANCELLED,
|
|
474
|
-
notificationTime: notificationTimestampForDb as any,
|
|
475
|
-
notificationTokens: recipientExpoTokens,
|
|
476
|
-
title,
|
|
477
|
-
body,
|
|
478
|
-
appointmentId: appointment.id,
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
try {
|
|
482
|
-
const notificationId = await this.createNotification(
|
|
483
|
-
notificationData as Notification
|
|
484
|
-
);
|
|
485
|
-
console.log(
|
|
486
|
-
`[NotificationsAdmin] Created APPOINTMENT_CANCELLED notification ${notificationId} for ${recipientRole} ${recipientUserId}.`
|
|
487
|
-
);
|
|
488
|
-
return notificationId;
|
|
489
|
-
} catch (error) {
|
|
490
|
-
console.error(
|
|
491
|
-
`[NotificationsAdmin] Error creating APPOINTMENT_CANCELLED notification for ${recipientRole} ${recipientUserId}:`,
|
|
492
|
-
error
|
|
493
|
-
);
|
|
494
|
-
return null;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
async sendAppointmentRescheduledProposalPush(
|
|
499
|
-
appointment: Appointment,
|
|
500
|
-
patientUserId: string,
|
|
501
|
-
patientExpoTokens: string[]
|
|
502
|
-
): Promise<string | null> {
|
|
503
|
-
if (!patientExpoTokens || patientExpoTokens.length === 0) {
|
|
504
|
-
console.log(
|
|
505
|
-
`[NotificationsAdmin] No expo tokens for patient ${patientUserId} for appointment ${appointment.id} reschedule proposal. Skipping push.`
|
|
506
|
-
);
|
|
507
|
-
return null;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
const title = "Appointment Reschedule Proposed";
|
|
511
|
-
const body = `Action Required: A new time has been proposed for your appointment for ${appointment.procedureInfo.name}. Please review in the app.`;
|
|
512
|
-
|
|
513
|
-
const notificationTimestampForDb = admin.firestore.Timestamp.now();
|
|
514
|
-
|
|
515
|
-
const notificationData: Omit<
|
|
516
|
-
Notification,
|
|
517
|
-
"id" | "createdAt" | "updatedAt" | "status" | "isRead"
|
|
518
|
-
> = {
|
|
519
|
-
userId: patientUserId,
|
|
520
|
-
userRole: UserRole.PATIENT,
|
|
521
|
-
notificationType: NotificationType.APPOINTMENT_RESCHEDULED_PROPOSAL,
|
|
522
|
-
notificationTime: notificationTimestampForDb as any,
|
|
523
|
-
notificationTokens: patientExpoTokens,
|
|
524
|
-
title,
|
|
525
|
-
body,
|
|
526
|
-
appointmentId: appointment.id,
|
|
527
|
-
};
|
|
528
|
-
|
|
529
|
-
try {
|
|
530
|
-
const notificationId = await this.createNotification(
|
|
531
|
-
notificationData as Notification
|
|
532
|
-
);
|
|
533
|
-
console.log(
|
|
534
|
-
`[NotificationsAdmin] Created APPOINTMENT_RESCHEDULED_PROPOSAL notification ${notificationId} for patient ${patientUserId}.`
|
|
535
|
-
);
|
|
536
|
-
return notificationId;
|
|
537
|
-
} catch (error) {
|
|
538
|
-
console.error(
|
|
539
|
-
`[NotificationsAdmin] Error creating APPOINTMENT_RESCHEDULED_PROPOSAL notification for patient ${patientUserId}:`,
|
|
540
|
-
error
|
|
541
|
-
);
|
|
542
|
-
return null;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
async sendPaymentUpdatePush(
|
|
547
|
-
appointment: Appointment,
|
|
548
|
-
patientUserId: string,
|
|
549
|
-
patientExpoTokens: string[]
|
|
550
|
-
): Promise<string | null> {
|
|
551
|
-
if (!patientExpoTokens || patientExpoTokens.length === 0) {
|
|
552
|
-
console.log(
|
|
553
|
-
`[NotificationsAdmin] No expo tokens for patient ${patientUserId} for appointment ${appointment.id} payment update. Skipping push.`
|
|
554
|
-
);
|
|
555
|
-
return null;
|
|
556
|
-
}
|
|
557
|
-
const title = "Payment Updated";
|
|
558
|
-
const body = `Your payment status for the appointment (${
|
|
559
|
-
appointment.procedureInfo.name
|
|
560
|
-
}) on ${appointment.appointmentStartTime
|
|
561
|
-
.toDate()
|
|
562
|
-
.toLocaleDateString()} is now ${appointment.paymentStatus}.`;
|
|
563
|
-
|
|
564
|
-
const notificationTimestampForDb = admin.firestore.Timestamp.now();
|
|
565
|
-
|
|
566
|
-
const notificationType =
|
|
567
|
-
appointment.paymentStatus === PaymentStatus.PAID
|
|
568
|
-
? NotificationType.PAYMENT_CONFIRMATION
|
|
569
|
-
: NotificationType.GENERAL_MESSAGE;
|
|
570
|
-
|
|
571
|
-
const notificationData: Omit<
|
|
572
|
-
Notification,
|
|
573
|
-
"id" | "createdAt" | "updatedAt" | "status" | "isRead"
|
|
574
|
-
> = {
|
|
575
|
-
userId: patientUserId,
|
|
576
|
-
userRole: UserRole.PATIENT,
|
|
577
|
-
notificationType: notificationType,
|
|
578
|
-
notificationTime: notificationTimestampForDb as any,
|
|
579
|
-
notificationTokens: patientExpoTokens,
|
|
580
|
-
title,
|
|
581
|
-
body,
|
|
582
|
-
appointmentId: appointment.id,
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
try {
|
|
586
|
-
const notificationId = await this.createNotification(
|
|
587
|
-
notificationData as Notification
|
|
588
|
-
);
|
|
589
|
-
console.log(
|
|
590
|
-
`[NotificationsAdmin] Created PAYMENT_UPDATE notification ${notificationId} for patient ${patientUserId}.`
|
|
591
|
-
);
|
|
592
|
-
return notificationId;
|
|
593
|
-
} catch (error) {
|
|
594
|
-
console.error(
|
|
595
|
-
`[NotificationsAdmin] Error creating PAYMENT_UPDATE notification for patient ${patientUserId}:`,
|
|
596
|
-
error
|
|
597
|
-
);
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
async sendReviewRequestPush(
|
|
603
|
-
appointment: Appointment,
|
|
604
|
-
patientUserId: string,
|
|
605
|
-
patientExpoTokens: string[]
|
|
606
|
-
): Promise<string | null> {
|
|
607
|
-
if (!patientExpoTokens || patientExpoTokens.length === 0) {
|
|
608
|
-
console.log(
|
|
609
|
-
`[NotificationsAdmin] No expo tokens for patient ${patientUserId} for appointment ${appointment.id} review request. Skipping push.`
|
|
610
|
-
);
|
|
611
|
-
return null;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const title = "Leave a Review";
|
|
615
|
-
const body = `How was your recent appointment for ${appointment.procedureInfo.name}? We'd love to hear your feedback!`;
|
|
616
|
-
|
|
617
|
-
const notificationTimestampForDb = admin.firestore.Timestamp.now();
|
|
618
|
-
|
|
619
|
-
const notificationData: Omit<
|
|
620
|
-
Notification,
|
|
621
|
-
"id" | "createdAt" | "updatedAt" | "status" | "isRead"
|
|
622
|
-
> = {
|
|
623
|
-
userId: patientUserId,
|
|
624
|
-
userRole: UserRole.PATIENT,
|
|
625
|
-
notificationType: NotificationType.REVIEW_REQUEST,
|
|
626
|
-
notificationTime: notificationTimestampForDb as any,
|
|
627
|
-
notificationTokens: patientExpoTokens,
|
|
628
|
-
title,
|
|
629
|
-
body,
|
|
630
|
-
appointmentId: appointment.id,
|
|
631
|
-
};
|
|
632
|
-
|
|
633
|
-
try {
|
|
634
|
-
const notificationId = await this.createNotification(
|
|
635
|
-
notificationData as Notification
|
|
636
|
-
);
|
|
637
|
-
console.log(
|
|
638
|
-
`[NotificationsAdmin] Created REVIEW_REQUEST notification ${notificationId} for patient ${patientUserId}.`
|
|
639
|
-
);
|
|
640
|
-
return notificationId;
|
|
641
|
-
} catch (error) {
|
|
642
|
-
console.error(
|
|
643
|
-
`[NotificationsAdmin] Error creating REVIEW_REQUEST notification for patient ${patientUserId}:`,
|
|
644
|
-
error
|
|
645
|
-
);
|
|
646
|
-
return null;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
async sendReviewAddedPush(
|
|
651
|
-
appointment: Appointment,
|
|
652
|
-
recipientUserId: string,
|
|
653
|
-
recipientExpoTokens: string[],
|
|
654
|
-
recipientRole: UserRole
|
|
655
|
-
): Promise<string | null> {
|
|
656
|
-
if (recipientRole !== UserRole.PRACTITIONER) {
|
|
657
|
-
console.log(
|
|
658
|
-
`[NotificationsAdmin] Only Practitioners receive review added push notifications (Role: ${recipientRole}). Skipping for appointment ${appointment.id}.`
|
|
659
|
-
);
|
|
660
|
-
return null;
|
|
661
|
-
}
|
|
662
|
-
if (!recipientExpoTokens || recipientExpoTokens.length === 0) {
|
|
663
|
-
console.log(
|
|
664
|
-
`[NotificationsAdmin] No expo tokens for practitioner ${recipientUserId} for appointment ${appointment.id} review added. Skipping push.`
|
|
665
|
-
);
|
|
666
|
-
return null;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
const title = "New Review Received";
|
|
670
|
-
const body = `A new review has been added by ${
|
|
671
|
-
appointment.patientInfo.fullName
|
|
672
|
-
} for your appointment on ${appointment.appointmentStartTime
|
|
673
|
-
.toDate()
|
|
674
|
-
.toLocaleDateString()}.`;
|
|
675
|
-
|
|
676
|
-
const notificationTimestampForDb = admin.firestore.Timestamp.now();
|
|
677
|
-
|
|
678
|
-
const tempNotificationType = NotificationType.GENERAL_MESSAGE;
|
|
679
|
-
|
|
680
|
-
const notificationData: Omit<
|
|
681
|
-
Notification,
|
|
682
|
-
"id" | "createdAt" | "updatedAt" | "status" | "isRead"
|
|
683
|
-
> = {
|
|
684
|
-
userId: recipientUserId,
|
|
685
|
-
userRole: UserRole.PRACTITIONER,
|
|
686
|
-
notificationType: tempNotificationType,
|
|
687
|
-
notificationTime: notificationTimestampForDb as any,
|
|
688
|
-
notificationTokens: recipientExpoTokens,
|
|
689
|
-
title,
|
|
690
|
-
body,
|
|
691
|
-
appointmentId: appointment.id,
|
|
692
|
-
};
|
|
693
|
-
|
|
694
|
-
try {
|
|
695
|
-
const notificationId = await this.createNotification(
|
|
696
|
-
notificationData as Notification
|
|
697
|
-
);
|
|
698
|
-
console.log(
|
|
699
|
-
`[NotificationsAdmin] Created REVIEW_ADDED (using temp type ${tempNotificationType}) notification ${notificationId} for practitioner ${recipientUserId}.`
|
|
700
|
-
);
|
|
701
|
-
return notificationId;
|
|
702
|
-
} catch (error) {
|
|
703
|
-
console.error(
|
|
704
|
-
`[NotificationsAdmin] Error creating REVIEW_ADDED (using temp type ${tempNotificationType}) notification for practitioner ${recipientUserId}:`,
|
|
705
|
-
error
|
|
706
|
-
);
|
|
707
|
-
return null;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
Notification,
|
|
3
|
+
NotificationStatus,
|
|
4
|
+
NotificationType,
|
|
5
|
+
} from "../../types/notifications";
|
|
6
|
+
import * as admin from "firebase-admin";
|
|
7
|
+
import { Expo, ExpoPushMessage, ExpoPushTicket } from "expo-server-sdk";
|
|
8
|
+
import { Appointment, PaymentStatus } from "../../types/appointment";
|
|
9
|
+
import { UserRole } from "../../types";
|
|
10
|
+
import { Timestamp as FirebaseClientTimestamp } from "@firebase/firestore";
|
|
11
|
+
import { TimestampUtils } from "../../utils/TimestampUtils";
|
|
12
|
+
import { Logger } from "../logger";
|
|
13
|
+
|
|
14
|
+
export class NotificationsAdmin {
|
|
15
|
+
private expo: Expo;
|
|
16
|
+
private db: admin.firestore.Firestore;
|
|
17
|
+
|
|
18
|
+
constructor(firestore?: admin.firestore.Firestore) {
|
|
19
|
+
this.expo = new Expo();
|
|
20
|
+
this.db = firestore || admin.firestore();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Dohvata notifikaciju po ID-u
|
|
25
|
+
*/
|
|
26
|
+
async getNotification(id: string): Promise<Notification | null> {
|
|
27
|
+
const doc = await this.db.collection("notifications").doc(id).get();
|
|
28
|
+
return doc.exists ? ({ id: doc.id, ...doc.data() } as Notification) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Kreira novu notifikaciju
|
|
33
|
+
*/
|
|
34
|
+
async createNotification(
|
|
35
|
+
notification: Omit<Notification, "id">
|
|
36
|
+
): Promise<string> {
|
|
37
|
+
const docRef = await this.db.collection("notifications").add({
|
|
38
|
+
...notification,
|
|
39
|
+
status: NotificationStatus.PENDING,
|
|
40
|
+
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
41
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
42
|
+
});
|
|
43
|
+
return docRef.id;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Priprema Expo poruku za slanje
|
|
48
|
+
*/
|
|
49
|
+
private prepareExpoMessage(notification: Notification): ExpoPushMessage[] {
|
|
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
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Ažurira status notifikacije
|
|
79
|
+
*/
|
|
80
|
+
public async updateNotificationStatus(
|
|
81
|
+
notificationId: string,
|
|
82
|
+
status: NotificationStatus,
|
|
83
|
+
error?: string
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
const update: any = {
|
|
86
|
+
status,
|
|
87
|
+
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (status === NotificationStatus.SENT) {
|
|
91
|
+
update.sentAt = admin.firestore.FieldValue.serverTimestamp();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (error) {
|
|
95
|
+
update.error = error;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await this.db
|
|
99
|
+
.collection("notifications")
|
|
100
|
+
.doc(notificationId)
|
|
101
|
+
.update(update);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Šalje notifikaciju kroz Expo servis sa boljim error handlingom
|
|
106
|
+
*/
|
|
107
|
+
async sendPushNotification(notification: Notification): Promise<boolean> {
|
|
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
|
+
|
|
118
|
+
const messages = this.prepareExpoMessage(notification);
|
|
119
|
+
|
|
120
|
+
if (messages.length === 0) {
|
|
121
|
+
const errorMsg = "No valid notification tokens found";
|
|
122
|
+
Logger.error(
|
|
123
|
+
`[NotificationsAdmin] ${errorMsg} for notification ${notification.id}`
|
|
124
|
+
);
|
|
125
|
+
await this.updateNotificationStatus(
|
|
126
|
+
notification.id!,
|
|
127
|
+
NotificationStatus.FAILED,
|
|
128
|
+
errorMsg
|
|
129
|
+
);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
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
|
+
|
|
138
|
+
const tickets: ExpoPushTicket[][] = [];
|
|
139
|
+
|
|
140
|
+
// Šaljemo sve chunks
|
|
141
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
142
|
+
const chunk = chunks[i];
|
|
143
|
+
try {
|
|
144
|
+
Logger.info(
|
|
145
|
+
`[NotificationsAdmin] Sending chunk ${i + 1}/${
|
|
146
|
+
chunks.length
|
|
147
|
+
} with ${chunk.length} messages`
|
|
148
|
+
);
|
|
149
|
+
const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
|
|
150
|
+
Logger.info(
|
|
151
|
+
`[NotificationsAdmin] Received ${
|
|
152
|
+
ticketChunk.length
|
|
153
|
+
} tickets for chunk ${i + 1}`
|
|
154
|
+
);
|
|
155
|
+
tickets.push(ticketChunk);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
Logger.error(
|
|
158
|
+
`[NotificationsAdmin] Chunk ${i + 1} sending error:`,
|
|
159
|
+
error
|
|
160
|
+
);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Proveravamo rezultate
|
|
166
|
+
let hasErrors = false;
|
|
167
|
+
const errors: string[] = [];
|
|
168
|
+
const ticketsFlat = tickets.flat();
|
|
169
|
+
|
|
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) => {
|
|
179
|
+
if (ticket.status === "error") {
|
|
180
|
+
hasErrors = true;
|
|
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++;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
Logger.info(
|
|
200
|
+
`[NotificationsAdmin] Ticket results for notification ${notification.id}`,
|
|
201
|
+
ticketResults
|
|
202
|
+
);
|
|
203
|
+
|
|
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
|
+
|
|
211
|
+
await this.updateNotificationStatus(
|
|
212
|
+
notification.id!,
|
|
213
|
+
ticketResults.success > 0
|
|
214
|
+
? NotificationStatus.PARTIAL_SUCCESS
|
|
215
|
+
: NotificationStatus.FAILED,
|
|
216
|
+
errorSummary
|
|
217
|
+
);
|
|
218
|
+
return ticketResults.success > 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Logger.info(
|
|
222
|
+
`[NotificationsAdmin] Successfully sent notification ${notification.id} to all recipients`
|
|
223
|
+
);
|
|
224
|
+
await this.updateNotificationStatus(
|
|
225
|
+
notification.id!,
|
|
226
|
+
NotificationStatus.SENT
|
|
227
|
+
);
|
|
228
|
+
return true;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
const errorMessage =
|
|
231
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
232
|
+
Logger.error(
|
|
233
|
+
`[NotificationsAdmin] Critical error sending notification ${notification.id}:`,
|
|
234
|
+
error
|
|
235
|
+
);
|
|
236
|
+
await this.updateNotificationStatus(
|
|
237
|
+
notification.id!,
|
|
238
|
+
NotificationStatus.FAILED,
|
|
239
|
+
errorMessage
|
|
240
|
+
);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Procesira notifikacije koje čekaju na slanje sa batch limitom
|
|
247
|
+
*/
|
|
248
|
+
async processPendingNotifications(batchSize: number = 100): Promise<void> {
|
|
249
|
+
const now = admin.firestore.Timestamp.now();
|
|
250
|
+
|
|
251
|
+
Logger.info(
|
|
252
|
+
`[NotificationsAdmin] Starting to process pending notifications with batch size ${batchSize}`
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const pendingNotifications = await this.db
|
|
256
|
+
.collection("notifications")
|
|
257
|
+
.where("status", "==", NotificationStatus.PENDING)
|
|
258
|
+
.where("notificationTime", "<=", now)
|
|
259
|
+
.limit(batchSize)
|
|
260
|
+
.get();
|
|
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
|
+
|
|
273
|
+
const results = await Promise.allSettled(
|
|
274
|
+
pendingNotifications.docs.map(async (doc) => {
|
|
275
|
+
const notification = {
|
|
276
|
+
id: doc.id,
|
|
277
|
+
...doc.data(),
|
|
278
|
+
} as Notification;
|
|
279
|
+
|
|
280
|
+
Logger.info(
|
|
281
|
+
`[NotificationsAdmin] Processing notification ${notification.id} of type ${notification.notificationType}`
|
|
282
|
+
);
|
|
283
|
+
return this.sendPushNotification(notification);
|
|
284
|
+
})
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// Logujemo statistiku
|
|
288
|
+
const successful = results.filter(
|
|
289
|
+
(r) => r.status === "fulfilled" && r.value
|
|
290
|
+
).length;
|
|
291
|
+
const failed = results.filter(
|
|
292
|
+
(r) => r.status === "rejected" || (r.status === "fulfilled" && !r.value)
|
|
293
|
+
).length;
|
|
294
|
+
|
|
295
|
+
Logger.info(
|
|
296
|
+
`[NotificationsAdmin] Processed ${results.length} notifications: ${successful} successful, ${failed} failed`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Briše stare notifikacije sa batch procesiranjem
|
|
302
|
+
*/
|
|
303
|
+
async cleanupOldNotifications(
|
|
304
|
+
daysOld: number = 30,
|
|
305
|
+
batchSize: number = 500
|
|
306
|
+
): Promise<void> {
|
|
307
|
+
const cutoffDate = new Date();
|
|
308
|
+
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
|
309
|
+
|
|
310
|
+
Logger.info(
|
|
311
|
+
`[NotificationsAdmin] Starting cleanup of notifications older than ${daysOld} days`
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
let totalDeleted = 0;
|
|
315
|
+
while (true) {
|
|
316
|
+
const oldNotifications = await this.db
|
|
317
|
+
.collection("notifications")
|
|
318
|
+
.where(
|
|
319
|
+
"createdAt",
|
|
320
|
+
"<=",
|
|
321
|
+
admin.firestore.Timestamp.fromDate(cutoffDate)
|
|
322
|
+
)
|
|
323
|
+
.limit(batchSize)
|
|
324
|
+
.get();
|
|
325
|
+
|
|
326
|
+
if (oldNotifications.empty) {
|
|
327
|
+
Logger.info(
|
|
328
|
+
`[NotificationsAdmin] No more old notifications to delete. Total deleted: ${totalDeleted}`
|
|
329
|
+
);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const batch = this.db.batch();
|
|
334
|
+
oldNotifications.docs.forEach((doc) => {
|
|
335
|
+
batch.delete(doc.ref);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await batch.commit();
|
|
339
|
+
totalDeleted += oldNotifications.size;
|
|
340
|
+
Logger.info(
|
|
341
|
+
`[NotificationsAdmin] Deleted batch of ${oldNotifications.size} old notifications. Running total: ${totalDeleted}`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// --- Business Specific Notification Methods (Revised) ---
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Creates and potentially sends a push notification for a confirmed appointment.
|
|
350
|
+
* @param appointment The confirmed appointment object.
|
|
351
|
+
* @param recipientUserId The ID of the user receiving the notification.
|
|
352
|
+
* @param recipientExpoTokens Array of Expo push tokens for the recipient.
|
|
353
|
+
* @param recipientRole The role of the recipient (e.g., PATIENT, PRACTITIONER).
|
|
354
|
+
*/
|
|
355
|
+
async sendAppointmentConfirmedPush(
|
|
356
|
+
appointment: Appointment,
|
|
357
|
+
recipientUserId: string,
|
|
358
|
+
recipientExpoTokens: string[],
|
|
359
|
+
recipientRole: UserRole
|
|
360
|
+
): Promise<string | null> {
|
|
361
|
+
if (recipientRole === UserRole.CLINIC_ADMIN) {
|
|
362
|
+
console.log(
|
|
363
|
+
`[NotificationsAdmin] Clinic admin roles do not receive push notifications for appointment ${appointment.id} confirmation. Skipping.`
|
|
364
|
+
);
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
if (!recipientExpoTokens || recipientExpoTokens.length === 0) {
|
|
368
|
+
console.log(
|
|
369
|
+
`[NotificationsAdmin] No expo tokens for ${recipientRole} ${recipientUserId} for appointment ${appointment.id} confirmation. Skipping push.`
|
|
370
|
+
);
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let title = "Appointment Confirmed!";
|
|
375
|
+
let body = `Your appointment for ${
|
|
376
|
+
appointment.procedureInfo.name
|
|
377
|
+
} on ${appointment.appointmentStartTime
|
|
378
|
+
.toDate()
|
|
379
|
+
.toLocaleDateString()} at ${appointment.appointmentStartTime
|
|
380
|
+
.toDate()
|
|
381
|
+
.toLocaleTimeString()} has been confirmed.`;
|
|
382
|
+
|
|
383
|
+
if (recipientRole === UserRole.PRACTITIONER) {
|
|
384
|
+
title = "New Appointment Confirmed";
|
|
385
|
+
body = `Appointment for ${appointment.procedureInfo.name} with ${
|
|
386
|
+
appointment.patientInfo.fullName
|
|
387
|
+
} on ${appointment.appointmentStartTime
|
|
388
|
+
.toDate()
|
|
389
|
+
.toLocaleDateString()} is confirmed.`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const notificationTimestampForDb = admin.firestore.Timestamp.now();
|
|
393
|
+
|
|
394
|
+
const notificationData: Omit<
|
|
395
|
+
Notification,
|
|
396
|
+
"id" | "createdAt" | "updatedAt" | "status" | "isRead"
|
|
397
|
+
> = {
|
|
398
|
+
userId: recipientUserId,
|
|
399
|
+
userRole: recipientRole,
|
|
400
|
+
notificationType: NotificationType.APPOINTMENT_STATUS_CHANGE,
|
|
401
|
+
notificationTime: notificationTimestampForDb as any,
|
|
402
|
+
notificationTokens: recipientExpoTokens,
|
|
403
|
+
title,
|
|
404
|
+
body,
|
|
405
|
+
appointmentId: appointment.id,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
const notificationId = await this.createNotification(
|
|
410
|
+
notificationData as Notification
|
|
411
|
+
);
|
|
412
|
+
console.log(
|
|
413
|
+
`[NotificationsAdmin] Created APPOINTMENT_STATUS_CHANGE (Confirmed) notification ${notificationId} for ${recipientRole} ${recipientUserId}.`
|
|
414
|
+
);
|
|
415
|
+
return notificationId;
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.error(
|
|
418
|
+
`[NotificationsAdmin] Error creating APPOINTMENT_STATUS_CHANGE (Confirmed) notification for ${recipientRole} ${recipientUserId}:`,
|
|
419
|
+
error
|
|
420
|
+
);
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async sendAppointmentCancelledPush(
|
|
426
|
+
appointment: Appointment,
|
|
427
|
+
recipientUserId: string,
|
|
428
|
+
recipientExpoTokens: string[],
|
|
429
|
+
recipientRole: UserRole
|
|
430
|
+
): Promise<string | null> {
|
|
431
|
+
if (recipientRole === UserRole.CLINIC_ADMIN) {
|
|
432
|
+
console.log(
|
|
433
|
+
`[NotificationsAdmin] Clinic admin roles do not receive push notifications for appointment ${appointment.id} cancellation. Skipping.`
|
|
434
|
+
);
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
if (!recipientExpoTokens || recipientExpoTokens.length === 0) {
|
|
438
|
+
console.log(
|
|
439
|
+
`[NotificationsAdmin] No expo tokens for ${recipientRole} ${recipientUserId} for appointment ${appointment.id} cancellation. Skipping push.`
|
|
440
|
+
);
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let title = "Appointment Cancelled";
|
|
445
|
+
let body = `Your appointment for ${
|
|
446
|
+
appointment.procedureInfo.name
|
|
447
|
+
} on ${appointment.appointmentStartTime
|
|
448
|
+
.toDate()
|
|
449
|
+
.toLocaleDateString()} has been cancelled.`;
|
|
450
|
+
if (appointment.cancellationReason) {
|
|
451
|
+
body += ` Reason: ${appointment.cancellationReason}`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (recipientRole === UserRole.PRACTITIONER) {
|
|
455
|
+
body = `The appointment for ${appointment.procedureInfo.name} with ${
|
|
456
|
+
appointment.patientInfo.fullName
|
|
457
|
+
} on ${appointment.appointmentStartTime
|
|
458
|
+
.toDate()
|
|
459
|
+
.toLocaleDateString()} has been cancelled.`;
|
|
460
|
+
if (appointment.cancellationReason) {
|
|
461
|
+
body += ` Reason: ${appointment.cancellationReason}`;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const notificationTimestampForDb = admin.firestore.Timestamp.now();
|
|
466
|
+
|
|
467
|
+
const notificationData: Omit<
|
|
468
|
+
Notification,
|
|
469
|
+
"id" | "createdAt" | "updatedAt" | "status" | "isRead"
|
|
470
|
+
> = {
|
|
471
|
+
userId: recipientUserId,
|
|
472
|
+
userRole: recipientRole,
|
|
473
|
+
notificationType: NotificationType.APPOINTMENT_CANCELLED,
|
|
474
|
+
notificationTime: notificationTimestampForDb as any,
|
|
475
|
+
notificationTokens: recipientExpoTokens,
|
|
476
|
+
title,
|
|
477
|
+
body,
|
|
478
|
+
appointmentId: appointment.id,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
const notificationId = await this.createNotification(
|
|
483
|
+
notificationData as Notification
|
|
484
|
+
);
|
|
485
|
+
console.log(
|
|
486
|
+
`[NotificationsAdmin] Created APPOINTMENT_CANCELLED notification ${notificationId} for ${recipientRole} ${recipientUserId}.`
|
|
487
|
+
);
|
|
488
|
+
return notificationId;
|
|
489
|
+
} catch (error) {
|
|
490
|
+
console.error(
|
|
491
|
+
`[NotificationsAdmin] Error creating APPOINTMENT_CANCELLED notification for ${recipientRole} ${recipientUserId}:`,
|
|
492
|
+
error
|
|
493
|
+
);
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async sendAppointmentRescheduledProposalPush(
|
|
499
|
+
appointment: Appointment,
|
|
500
|
+
patientUserId: string,
|
|
501
|
+
patientExpoTokens: string[]
|
|
502
|
+
): Promise<string | null> {
|
|
503
|
+
if (!patientExpoTokens || patientExpoTokens.length === 0) {
|
|
504
|
+
console.log(
|
|
505
|
+
`[NotificationsAdmin] No expo tokens for patient ${patientUserId} for appointment ${appointment.id} reschedule proposal. Skipping push.`
|
|
506
|
+
);
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const title = "Appointment Reschedule Proposed";
|
|
511
|
+
const body = `Action Required: A new time has been proposed for your appointment for ${appointment.procedureInfo.name}. Please review in the app.`;
|
|
512
|
+
|
|
513
|
+
const notificationTimestampForDb = admin.firestore.Timestamp.now();
|
|
514
|
+
|
|
515
|
+
const notificationData: Omit<
|
|
516
|
+
Notification,
|
|
517
|
+
"id" | "createdAt" | "updatedAt" | "status" | "isRead"
|
|
518
|
+
> = {
|
|
519
|
+
userId: patientUserId,
|
|
520
|
+
userRole: UserRole.PATIENT,
|
|
521
|
+
notificationType: NotificationType.APPOINTMENT_RESCHEDULED_PROPOSAL,
|
|
522
|
+
notificationTime: notificationTimestampForDb as any,
|
|
523
|
+
notificationTokens: patientExpoTokens,
|
|
524
|
+
title,
|
|
525
|
+
body,
|
|
526
|
+
appointmentId: appointment.id,
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
const notificationId = await this.createNotification(
|
|
531
|
+
notificationData as Notification
|
|
532
|
+
);
|
|
533
|
+
console.log(
|
|
534
|
+
`[NotificationsAdmin] Created APPOINTMENT_RESCHEDULED_PROPOSAL notification ${notificationId} for patient ${patientUserId}.`
|
|
535
|
+
);
|
|
536
|
+
return notificationId;
|
|
537
|
+
} catch (error) {
|
|
538
|
+
console.error(
|
|
539
|
+
`[NotificationsAdmin] Error creating APPOINTMENT_RESCHEDULED_PROPOSAL notification for patient ${patientUserId}:`,
|
|
540
|
+
error
|
|
541
|
+
);
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async sendPaymentUpdatePush(
|
|
547
|
+
appointment: Appointment,
|
|
548
|
+
patientUserId: string,
|
|
549
|
+
patientExpoTokens: string[]
|
|
550
|
+
): Promise<string | null> {
|
|
551
|
+
if (!patientExpoTokens || patientExpoTokens.length === 0) {
|
|
552
|
+
console.log(
|
|
553
|
+
`[NotificationsAdmin] No expo tokens for patient ${patientUserId} for appointment ${appointment.id} payment update. Skipping push.`
|
|
554
|
+
);
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
const title = "Payment Updated";
|
|
558
|
+
const body = `Your payment status for the appointment (${
|
|
559
|
+
appointment.procedureInfo.name
|
|
560
|
+
}) on ${appointment.appointmentStartTime
|
|
561
|
+
.toDate()
|
|
562
|
+
.toLocaleDateString()} is now ${appointment.paymentStatus}.`;
|
|
563
|
+
|
|
564
|
+
const notificationTimestampForDb = admin.firestore.Timestamp.now();
|
|
565
|
+
|
|
566
|
+
const notificationType =
|
|
567
|
+
appointment.paymentStatus === PaymentStatus.PAID
|
|
568
|
+
? NotificationType.PAYMENT_CONFIRMATION
|
|
569
|
+
: NotificationType.GENERAL_MESSAGE;
|
|
570
|
+
|
|
571
|
+
const notificationData: Omit<
|
|
572
|
+
Notification,
|
|
573
|
+
"id" | "createdAt" | "updatedAt" | "status" | "isRead"
|
|
574
|
+
> = {
|
|
575
|
+
userId: patientUserId,
|
|
576
|
+
userRole: UserRole.PATIENT,
|
|
577
|
+
notificationType: notificationType,
|
|
578
|
+
notificationTime: notificationTimestampForDb as any,
|
|
579
|
+
notificationTokens: patientExpoTokens,
|
|
580
|
+
title,
|
|
581
|
+
body,
|
|
582
|
+
appointmentId: appointment.id,
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
const notificationId = await this.createNotification(
|
|
587
|
+
notificationData as Notification
|
|
588
|
+
);
|
|
589
|
+
console.log(
|
|
590
|
+
`[NotificationsAdmin] Created PAYMENT_UPDATE notification ${notificationId} for patient ${patientUserId}.`
|
|
591
|
+
);
|
|
592
|
+
return notificationId;
|
|
593
|
+
} catch (error) {
|
|
594
|
+
console.error(
|
|
595
|
+
`[NotificationsAdmin] Error creating PAYMENT_UPDATE notification for patient ${patientUserId}:`,
|
|
596
|
+
error
|
|
597
|
+
);
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async sendReviewRequestPush(
|
|
603
|
+
appointment: Appointment,
|
|
604
|
+
patientUserId: string,
|
|
605
|
+
patientExpoTokens: string[]
|
|
606
|
+
): Promise<string | null> {
|
|
607
|
+
if (!patientExpoTokens || patientExpoTokens.length === 0) {
|
|
608
|
+
console.log(
|
|
609
|
+
`[NotificationsAdmin] No expo tokens for patient ${patientUserId} for appointment ${appointment.id} review request. Skipping push.`
|
|
610
|
+
);
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const title = "Leave a Review";
|
|
615
|
+
const body = `How was your recent appointment for ${appointment.procedureInfo.name}? We'd love to hear your feedback!`;
|
|
616
|
+
|
|
617
|
+
const notificationTimestampForDb = admin.firestore.Timestamp.now();
|
|
618
|
+
|
|
619
|
+
const notificationData: Omit<
|
|
620
|
+
Notification,
|
|
621
|
+
"id" | "createdAt" | "updatedAt" | "status" | "isRead"
|
|
622
|
+
> = {
|
|
623
|
+
userId: patientUserId,
|
|
624
|
+
userRole: UserRole.PATIENT,
|
|
625
|
+
notificationType: NotificationType.REVIEW_REQUEST,
|
|
626
|
+
notificationTime: notificationTimestampForDb as any,
|
|
627
|
+
notificationTokens: patientExpoTokens,
|
|
628
|
+
title,
|
|
629
|
+
body,
|
|
630
|
+
appointmentId: appointment.id,
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
try {
|
|
634
|
+
const notificationId = await this.createNotification(
|
|
635
|
+
notificationData as Notification
|
|
636
|
+
);
|
|
637
|
+
console.log(
|
|
638
|
+
`[NotificationsAdmin] Created REVIEW_REQUEST notification ${notificationId} for patient ${patientUserId}.`
|
|
639
|
+
);
|
|
640
|
+
return notificationId;
|
|
641
|
+
} catch (error) {
|
|
642
|
+
console.error(
|
|
643
|
+
`[NotificationsAdmin] Error creating REVIEW_REQUEST notification for patient ${patientUserId}:`,
|
|
644
|
+
error
|
|
645
|
+
);
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async sendReviewAddedPush(
|
|
651
|
+
appointment: Appointment,
|
|
652
|
+
recipientUserId: string,
|
|
653
|
+
recipientExpoTokens: string[],
|
|
654
|
+
recipientRole: UserRole
|
|
655
|
+
): Promise<string | null> {
|
|
656
|
+
if (recipientRole !== UserRole.PRACTITIONER) {
|
|
657
|
+
console.log(
|
|
658
|
+
`[NotificationsAdmin] Only Practitioners receive review added push notifications (Role: ${recipientRole}). Skipping for appointment ${appointment.id}.`
|
|
659
|
+
);
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
if (!recipientExpoTokens || recipientExpoTokens.length === 0) {
|
|
663
|
+
console.log(
|
|
664
|
+
`[NotificationsAdmin] No expo tokens for practitioner ${recipientUserId} for appointment ${appointment.id} review added. Skipping push.`
|
|
665
|
+
);
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const title = "New Review Received";
|
|
670
|
+
const body = `A new review has been added by ${
|
|
671
|
+
appointment.patientInfo.fullName
|
|
672
|
+
} for your appointment on ${appointment.appointmentStartTime
|
|
673
|
+
.toDate()
|
|
674
|
+
.toLocaleDateString()}.`;
|
|
675
|
+
|
|
676
|
+
const notificationTimestampForDb = admin.firestore.Timestamp.now();
|
|
677
|
+
|
|
678
|
+
const tempNotificationType = NotificationType.GENERAL_MESSAGE;
|
|
679
|
+
|
|
680
|
+
const notificationData: Omit<
|
|
681
|
+
Notification,
|
|
682
|
+
"id" | "createdAt" | "updatedAt" | "status" | "isRead"
|
|
683
|
+
> = {
|
|
684
|
+
userId: recipientUserId,
|
|
685
|
+
userRole: UserRole.PRACTITIONER,
|
|
686
|
+
notificationType: tempNotificationType,
|
|
687
|
+
notificationTime: notificationTimestampForDb as any,
|
|
688
|
+
notificationTokens: recipientExpoTokens,
|
|
689
|
+
title,
|
|
690
|
+
body,
|
|
691
|
+
appointmentId: appointment.id,
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
try {
|
|
695
|
+
const notificationId = await this.createNotification(
|
|
696
|
+
notificationData as Notification
|
|
697
|
+
);
|
|
698
|
+
console.log(
|
|
699
|
+
`[NotificationsAdmin] Created REVIEW_ADDED (using temp type ${tempNotificationType}) notification ${notificationId} for practitioner ${recipientUserId}.`
|
|
700
|
+
);
|
|
701
|
+
return notificationId;
|
|
702
|
+
} catch (error) {
|
|
703
|
+
console.error(
|
|
704
|
+
`[NotificationsAdmin] Error creating REVIEW_ADDED (using temp type ${tempNotificationType}) notification for practitioner ${recipientUserId}:`,
|
|
705
|
+
error
|
|
706
|
+
);
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|