@blackcode_sa/metaestetics-api 1.6.5 → 1.6.6

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.
@@ -1,6 +1,13 @@
1
- import { Notification, NotificationStatus } from "../../types/notifications";
1
+ import {
2
+ Notification,
3
+ NotificationStatus,
4
+ NotificationType,
5
+ } from "../../types/notifications";
2
6
  import * as admin from "firebase-admin";
3
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";
4
11
 
5
12
  export class NotificationsAdmin {
6
13
  private expo: Expo;
@@ -222,4 +229,393 @@ export class NotificationsAdmin {
222
229
  );
223
230
  }
224
231
  }
232
+
233
+ // --- Business Specific Notification Methods (Revised) ---
234
+
235
+ /**
236
+ * Creates and potentially sends a push notification for a confirmed appointment.
237
+ * @param appointment The confirmed appointment object.
238
+ * @param recipientUserId The ID of the user receiving the notification.
239
+ * @param recipientExpoTokens Array of Expo push tokens for the recipient.
240
+ * @param recipientRole The role of the recipient (e.g., PATIENT, PRACTITIONER).
241
+ */
242
+ async sendAppointmentConfirmedPush(
243
+ appointment: Appointment,
244
+ recipientUserId: string,
245
+ recipientExpoTokens: string[],
246
+ recipientRole: UserRole
247
+ ): Promise<string | null> {
248
+ if (recipientRole === UserRole.CLINIC_ADMIN) {
249
+ console.log(
250
+ `[NotificationsAdmin] Clinic admin roles do not receive push notifications for appointment ${appointment.id} confirmation. Skipping.`
251
+ );
252
+ return null;
253
+ }
254
+ if (!recipientExpoTokens || recipientExpoTokens.length === 0) {
255
+ console.log(
256
+ `[NotificationsAdmin] No expo tokens for ${recipientRole} ${recipientUserId} for appointment ${appointment.id} confirmation. Skipping push.`
257
+ );
258
+ return null;
259
+ }
260
+
261
+ let title = "Appointment Confirmed!";
262
+ let body = `Your appointment for ${
263
+ appointment.procedureInfo.name
264
+ } on ${appointment.appointmentStartTime
265
+ .toDate()
266
+ .toLocaleDateString()} at ${appointment.appointmentStartTime
267
+ .toDate()
268
+ .toLocaleTimeString()} has been confirmed.`;
269
+
270
+ if (recipientRole === UserRole.PRACTITIONER) {
271
+ title = "New Appointment Confirmed";
272
+ body = `Appointment for ${appointment.procedureInfo.name} with ${
273
+ appointment.patientInfo.fullName
274
+ } on ${appointment.appointmentStartTime
275
+ .toDate()
276
+ .toLocaleDateString()} is confirmed.`;
277
+ }
278
+
279
+ const adminTsNow = admin.firestore.Timestamp.now();
280
+ const clientCompatibleNotificationTime = new FirebaseClientTimestamp(
281
+ adminTsNow.seconds,
282
+ adminTsNow.nanoseconds
283
+ );
284
+
285
+ const notificationData: Omit<
286
+ Notification,
287
+ "id" | "createdAt" | "updatedAt" | "status" | "isRead"
288
+ > = {
289
+ userId: recipientUserId,
290
+ userRole: recipientRole,
291
+ notificationType: NotificationType.APPOINTMENT_STATUS_CHANGE,
292
+ notificationTime: clientCompatibleNotificationTime,
293
+ notificationTokens: recipientExpoTokens,
294
+ title,
295
+ body,
296
+ appointmentId: appointment.id,
297
+ };
298
+
299
+ try {
300
+ const notificationId = await this.createNotification(
301
+ notificationData as Notification
302
+ );
303
+ console.log(
304
+ `[NotificationsAdmin] Created APPOINTMENT_STATUS_CHANGE (Confirmed) notification ${notificationId} for ${recipientRole} ${recipientUserId}.`
305
+ );
306
+ return notificationId;
307
+ } catch (error) {
308
+ console.error(
309
+ `[NotificationsAdmin] Error creating APPOINTMENT_STATUS_CHANGE (Confirmed) notification for ${recipientRole} ${recipientUserId}:`,
310
+ error
311
+ );
312
+ return null;
313
+ }
314
+ }
315
+
316
+ async sendAppointmentCancelledPush(
317
+ appointment: Appointment,
318
+ recipientUserId: string,
319
+ recipientExpoTokens: string[],
320
+ recipientRole: UserRole
321
+ ): Promise<string | null> {
322
+ if (recipientRole === UserRole.CLINIC_ADMIN) {
323
+ console.log(
324
+ `[NotificationsAdmin] Clinic admin roles do not receive push notifications for appointment ${appointment.id} cancellation. Skipping.`
325
+ );
326
+ return null;
327
+ }
328
+ if (!recipientExpoTokens || recipientExpoTokens.length === 0) {
329
+ console.log(
330
+ `[NotificationsAdmin] No expo tokens for ${recipientRole} ${recipientUserId} for appointment ${appointment.id} cancellation. Skipping push.`
331
+ );
332
+ return null;
333
+ }
334
+
335
+ let title = "Appointment Cancelled";
336
+ let body = `Your appointment for ${
337
+ appointment.procedureInfo.name
338
+ } on ${appointment.appointmentStartTime
339
+ .toDate()
340
+ .toLocaleDateString()} has been cancelled.`;
341
+ if (appointment.cancellationReason) {
342
+ body += ` Reason: ${appointment.cancellationReason}`;
343
+ }
344
+
345
+ if (recipientRole === UserRole.PRACTITIONER) {
346
+ body = `The appointment for ${appointment.procedureInfo.name} with ${
347
+ appointment.patientInfo.fullName
348
+ } on ${appointment.appointmentStartTime
349
+ .toDate()
350
+ .toLocaleDateString()} has been cancelled.`;
351
+ if (appointment.cancellationReason) {
352
+ body += ` Reason: ${appointment.cancellationReason}`;
353
+ }
354
+ }
355
+
356
+ const adminTsNow = admin.firestore.Timestamp.now();
357
+ const clientCompatibleNotificationTime = new FirebaseClientTimestamp(
358
+ adminTsNow.seconds,
359
+ adminTsNow.nanoseconds
360
+ );
361
+
362
+ const notificationData: Omit<
363
+ Notification,
364
+ "id" | "createdAt" | "updatedAt" | "status" | "isRead"
365
+ > = {
366
+ userId: recipientUserId,
367
+ userRole: recipientRole,
368
+ notificationType: NotificationType.APPOINTMENT_CANCELLED,
369
+ notificationTime: clientCompatibleNotificationTime,
370
+ notificationTokens: recipientExpoTokens,
371
+ title,
372
+ body,
373
+ appointmentId: appointment.id,
374
+ };
375
+
376
+ try {
377
+ const notificationId = await this.createNotification(
378
+ notificationData as Notification
379
+ );
380
+ console.log(
381
+ `[NotificationsAdmin] Created APPOINTMENT_CANCELLED notification ${notificationId} for ${recipientRole} ${recipientUserId}.`
382
+ );
383
+ return notificationId;
384
+ } catch (error) {
385
+ console.error(
386
+ `[NotificationsAdmin] Error creating APPOINTMENT_CANCELLED notification for ${recipientRole} ${recipientUserId}:`,
387
+ error
388
+ );
389
+ return null;
390
+ }
391
+ }
392
+
393
+ async sendAppointmentRescheduledProposalPush(
394
+ appointment: Appointment,
395
+ patientUserId: string,
396
+ patientExpoTokens: string[]
397
+ ): Promise<string | null> {
398
+ if (!patientExpoTokens || patientExpoTokens.length === 0) {
399
+ console.log(
400
+ `[NotificationsAdmin] No expo tokens for patient ${patientUserId} for appointment ${appointment.id} reschedule proposal. Skipping push.`
401
+ );
402
+ return null;
403
+ }
404
+
405
+ const title = "Appointment Reschedule Proposed";
406
+ const body = `Action Required: A new time has been proposed for your appointment for ${appointment.procedureInfo.name}. Please review in the app.`;
407
+
408
+ const adminTsNow = admin.firestore.Timestamp.now();
409
+ const clientCompatibleNotificationTime = new FirebaseClientTimestamp(
410
+ adminTsNow.seconds,
411
+ adminTsNow.nanoseconds
412
+ );
413
+
414
+ const notificationData: Omit<
415
+ Notification,
416
+ "id" | "createdAt" | "updatedAt" | "status" | "isRead"
417
+ > = {
418
+ userId: patientUserId,
419
+ userRole: UserRole.PATIENT,
420
+ notificationType: NotificationType.APPOINTMENT_RESCHEDULED_PROPOSAL,
421
+ notificationTime: clientCompatibleNotificationTime,
422
+ notificationTokens: patientExpoTokens,
423
+ title,
424
+ body,
425
+ appointmentId: appointment.id,
426
+ };
427
+
428
+ try {
429
+ const notificationId = await this.createNotification(
430
+ notificationData as Notification
431
+ );
432
+ console.log(
433
+ `[NotificationsAdmin] Created APPOINTMENT_RESCHEDULED_PROPOSAL notification ${notificationId} for patient ${patientUserId}.`
434
+ );
435
+ return notificationId;
436
+ } catch (error) {
437
+ console.error(
438
+ `[NotificationsAdmin] Error creating APPOINTMENT_RESCHEDULED_PROPOSAL notification for patient ${patientUserId}:`,
439
+ error
440
+ );
441
+ return null;
442
+ }
443
+ }
444
+
445
+ async sendPaymentUpdatePush(
446
+ appointment: Appointment,
447
+ patientUserId: string,
448
+ patientExpoTokens: string[]
449
+ ): Promise<string | null> {
450
+ if (!patientExpoTokens || patientExpoTokens.length === 0) {
451
+ console.log(
452
+ `[NotificationsAdmin] No expo tokens for patient ${patientUserId} for appointment ${appointment.id} payment update. Skipping push.`
453
+ );
454
+ return null;
455
+ }
456
+ const title = "Payment Updated";
457
+ const body = `Your payment status for the appointment (${
458
+ appointment.procedureInfo.name
459
+ }) on ${appointment.appointmentStartTime
460
+ .toDate()
461
+ .toLocaleDateString()} is now ${appointment.paymentStatus}.`;
462
+
463
+ const adminTsNow = admin.firestore.Timestamp.now();
464
+ const clientCompatibleNotificationTime = new FirebaseClientTimestamp(
465
+ adminTsNow.seconds,
466
+ adminTsNow.nanoseconds
467
+ );
468
+
469
+ const notificationType =
470
+ appointment.paymentStatus === PaymentStatus.PAID
471
+ ? NotificationType.PAYMENT_CONFIRMATION
472
+ : NotificationType.GENERAL_MESSAGE;
473
+
474
+ const notificationData: Omit<
475
+ Notification,
476
+ "id" | "createdAt" | "updatedAt" | "status" | "isRead"
477
+ > = {
478
+ userId: patientUserId,
479
+ userRole: UserRole.PATIENT,
480
+ notificationType: notificationType,
481
+ notificationTime: clientCompatibleNotificationTime,
482
+ notificationTokens: patientExpoTokens,
483
+ title,
484
+ body,
485
+ appointmentId: appointment.id,
486
+ };
487
+
488
+ try {
489
+ const notificationId = await this.createNotification(
490
+ notificationData as Notification
491
+ );
492
+ console.log(
493
+ `[NotificationsAdmin] Created PAYMENT_UPDATE notification ${notificationId} for patient ${patientUserId}.`
494
+ );
495
+ return notificationId;
496
+ } catch (error) {
497
+ console.error(
498
+ `[NotificationsAdmin] Error creating PAYMENT_UPDATE notification for patient ${patientUserId}:`,
499
+ error
500
+ );
501
+ return null;
502
+ }
503
+ }
504
+
505
+ async sendReviewRequestPush(
506
+ appointment: Appointment,
507
+ patientUserId: string,
508
+ patientExpoTokens: string[]
509
+ ): Promise<string | null> {
510
+ if (!patientExpoTokens || patientExpoTokens.length === 0) {
511
+ console.log(
512
+ `[NotificationsAdmin] No expo tokens for patient ${patientUserId} for appointment ${appointment.id} review request. Skipping push.`
513
+ );
514
+ return null;
515
+ }
516
+
517
+ const title = "Leave a Review";
518
+ const body = `How was your recent appointment for ${appointment.procedureInfo.name}? We'd love to hear your feedback!`;
519
+
520
+ const adminTsNow = admin.firestore.Timestamp.now();
521
+ const clientCompatibleNotificationTime = new FirebaseClientTimestamp(
522
+ adminTsNow.seconds,
523
+ adminTsNow.nanoseconds
524
+ );
525
+
526
+ const notificationData: Omit<
527
+ Notification,
528
+ "id" | "createdAt" | "updatedAt" | "status" | "isRead"
529
+ > = {
530
+ userId: patientUserId,
531
+ userRole: UserRole.PATIENT,
532
+ notificationType: NotificationType.REVIEW_REQUEST,
533
+ notificationTime: clientCompatibleNotificationTime,
534
+ notificationTokens: patientExpoTokens,
535
+ title,
536
+ body,
537
+ appointmentId: appointment.id,
538
+ };
539
+
540
+ try {
541
+ const notificationId = await this.createNotification(
542
+ notificationData as Notification
543
+ );
544
+ console.log(
545
+ `[NotificationsAdmin] Created REVIEW_REQUEST notification ${notificationId} for patient ${patientUserId}.`
546
+ );
547
+ return notificationId;
548
+ } catch (error) {
549
+ console.error(
550
+ `[NotificationsAdmin] Error creating REVIEW_REQUEST notification for patient ${patientUserId}:`,
551
+ error
552
+ );
553
+ return null;
554
+ }
555
+ }
556
+
557
+ async sendReviewAddedPush(
558
+ appointment: Appointment,
559
+ recipientUserId: string,
560
+ recipientExpoTokens: string[],
561
+ recipientRole: UserRole
562
+ ): Promise<string | null> {
563
+ if (recipientRole !== UserRole.PRACTITIONER) {
564
+ console.log(
565
+ `[NotificationsAdmin] Only Practitioners receive review added push notifications (Role: ${recipientRole}). Skipping for appointment ${appointment.id}.`
566
+ );
567
+ return null;
568
+ }
569
+ if (!recipientExpoTokens || recipientExpoTokens.length === 0) {
570
+ console.log(
571
+ `[NotificationsAdmin] No expo tokens for practitioner ${recipientUserId} for appointment ${appointment.id} review added. Skipping push.`
572
+ );
573
+ return null;
574
+ }
575
+
576
+ const title = "New Review Received";
577
+ const body = `A new review has been added by ${
578
+ appointment.patientInfo.fullName
579
+ } for your appointment on ${appointment.appointmentStartTime
580
+ .toDate()
581
+ .toLocaleDateString()}.`;
582
+
583
+ const adminTsNow = admin.firestore.Timestamp.now();
584
+ const clientCompatibleNotificationTime = new FirebaseClientTimestamp(
585
+ adminTsNow.seconds,
586
+ adminTsNow.nanoseconds
587
+ );
588
+
589
+ const tempNotificationType = NotificationType.GENERAL_MESSAGE;
590
+
591
+ const notificationData: Omit<
592
+ Notification,
593
+ "id" | "createdAt" | "updatedAt" | "status" | "isRead"
594
+ > = {
595
+ userId: recipientUserId,
596
+ userRole: UserRole.PRACTITIONER,
597
+ notificationType: tempNotificationType,
598
+ notificationTime: clientCompatibleNotificationTime,
599
+ notificationTokens: recipientExpoTokens,
600
+ title,
601
+ body,
602
+ appointmentId: appointment.id,
603
+ };
604
+
605
+ try {
606
+ const notificationId = await this.createNotification(
607
+ notificationData as Notification
608
+ );
609
+ console.log(
610
+ `[NotificationsAdmin] Created REVIEW_ADDED (using temp type ${tempNotificationType}) notification ${notificationId} for practitioner ${recipientUserId}.`
611
+ );
612
+ return notificationId;
613
+ } catch (error) {
614
+ console.error(
615
+ `[NotificationsAdmin] Error creating REVIEW_ADDED (using temp type ${tempNotificationType}) notification for practitioner ${recipientUserId}:`,
616
+ error
617
+ );
618
+ return null;
619
+ }
620
+ }
225
621
  }
@@ -93,6 +93,7 @@ export interface LinkedFormInfo {
93
93
  templateVersion: number;
94
94
  title: string; // For display, usually from DocumentTemplate.title
95
95
  isUserForm: boolean;
96
+ isRequired?: boolean;
96
97
  status: FilledDocumentStatus; // Status of the filled form (e.g., draft, completed, signed)
97
98
  path: string; // Full Firestore path to the filled document (e.g., appointments/{aid}/user-forms/{fid})
98
99
  submittedAt?: Timestamp;
@@ -11,6 +11,8 @@ export enum NotificationType {
11
11
  APPOINTMENT_CANCELLED = "appointmentCancelled", // When an appointment is cancelled
12
12
 
13
13
  // --- Requirement-Driven Instructions ---
14
+ PRE_REQUIREMENT_INSTRUCTION_DUE = "preRequirementInstructionDue",
15
+ POST_REQUIREMENT_INSTRUCTION_DUE = "postRequirementInstructionDue",
14
16
  REQUIREMENT_INSTRUCTION_DUE = "requirementInstructionDue", // For specific pre/post care instructions
15
17
 
16
18
  // --- Form Related ---
@@ -106,7 +108,7 @@ export enum NotificationStatus {
106
108
  * Notifikacija za pre-requirement
107
109
  */
108
110
  export interface PreRequirementNotification extends BaseNotification {
109
- notificationType: NotificationType.REQUIREMENT_INSTRUCTION_DUE;
111
+ notificationType: NotificationType.PRE_REQUIREMENT_INSTRUCTION_DUE;
110
112
  /** ID tretmana za koji je vezan pre-requirement */
111
113
  treatmentId: string;
112
114
  /** Lista pre-requirements koji treba da se ispune */
@@ -119,7 +121,7 @@ export interface PreRequirementNotification extends BaseNotification {
119
121
  * Notifikacija za post-requirement
120
122
  */
121
123
  export interface PostRequirementNotification extends BaseNotification {
122
- notificationType: NotificationType.REQUIREMENT_INSTRUCTION_DUE;
124
+ notificationType: NotificationType.POST_REQUIREMENT_INSTRUCTION_DUE;
123
125
  /** ID tretmana za koji je vezan post-requirement */
124
126
  treatmentId: string;
125
127
  /** Lista post-requirements koji treba da se ispune */
@@ -70,6 +70,7 @@ export const linkedFormInfoSchema = z.object({
70
70
  .positive("Template version must be a positive integer"),
71
71
  title: z.string().min(MIN_STRING_LENGTH, "Form title is required"),
72
72
  isUserForm: z.boolean(),
73
+ isRequired: z.boolean().optional(),
73
74
  status: filledDocumentStatusSchema,
74
75
  path: z.string().min(MIN_STRING_LENGTH, "Form path is required"),
75
76
  submittedAt: z