eletrotupi / tcc/ api/src/services/notification.service.ts1b8d5d79f03d61a20e683b0da33bbd8abe43bf86
3.7 KB | 149 lines Raw
1
import { Notification, NotificationKind } from "@prisma/client";
2
import { endOfDay } from "date-fns";
3

4
import { prisma } from "@app/lib/prisma";
5
import { APP_TIMEZONE } from "@app/lib/appTime";
6
import { logger } from "@app/lib/logger";
7
import { sendPushNotification } from "@app/lib/sendPushNotification";
8
import {
9
  publicNotificationSelect,
10
  PublicNotification,
11
} from "@app/lib/serializers/notification";
12

13
export type CreateNotificationInput = {
14
  userId: number;
15
  kind: NotificationKind;
16
  title: string;
17
  body: string;
18
  scheduledFor: Date;
19
  regimenId?: number | null;
20
};
21

22
const endOfAppDay = (scheduledFor: Date): Date =>
23
  endOfDay(scheduledFor, { in: APP_TIMEZONE });
24

25
// The only place a kind's behaviour is described: how long it stays relevant,
26
// and where tapping it should lead
27
type NotificationRule = {
28
  expiresAt: (scheduledFor: Date) => Date | null;
29
  screen?: string;
30
};
31

32
const notificationRules: Record<NotificationKind, NotificationRule> = {
33
  [NotificationKind.DAILY_REMINDER]: {
34
    expiresAt: endOfAppDay,
35
    screen: "moods",
36
  },
37
  [NotificationKind.MEDICINE_REMINDER]: {
38
    expiresAt: endOfAppDay,
39
    screen: "medicine-regimens",
40
  },
41
  [NotificationKind.INSIGHT_ALERT]: {
42
    expiresAt: () => null,
43
  },
44
};
45

46
export const createNotification = async (
47
  input: CreateNotificationInput
48
): Promise<Notification | null> => {
49
  const { userId, kind, title, body, scheduledFor, regimenId = null } = input;
50

51
  const user = await prisma.user.findUnique({
52
    where: { id: userId },
53
    select: { pushToken: true, pushEnabled: true },
54
  });
55

56
  if (!user) {
57
    logger.warn({ userId, kind }, "[notification] user not found, skipping");
58

59
    return null;
60
  }
61

62
  const rule = notificationRules[kind];
63

64
  const notification = await prisma.notification.create({
65
    data: {
66
      userId,
67
      kind,
68
      title,
69
      body,
70
      scheduledFor,
71
      regimenId,
72
      expiresAt: rule.expiresAt(scheduledFor),
73
    },
74
  });
75

76
  if (!user.pushEnabled || !user.pushToken) {
77
    logger.info(
78
      { notificationId: notification.id, userId, kind },
79
      "[notification] stored without push"
80
    );
81

82
    return notification;
83
  }
84

85
  const data = {
86
    ...(rule.screen ? { screen: rule.screen } : {}),
87
    ...(regimenId !== null ? { regimenId } : {}),
88
  };
89

90
  try {
91
    await sendPushNotification({
92
      token: user.pushToken,
93
      title,
94
      body,
95
      data,
96
    });
97
  } catch (error) {
98
    logger.error(
99
      { notificationId: notification.id, userId, err: error },
100
      "[notification] push failed"
101
    );
102

103
    return notification;
104
  }
105

106
  logger.info(
107
    { notificationId: notification.id, userId, kind },
108
    "[notification] push sent"
109
  );
110

111
  return prisma.notification.update({
112
    where: { id: notification.id },
113
    data: { pushedAt: new Date() },
114
  });
115
};
116

117
const DEFAULT_NOTIFICATIONS_LIMIT = 30;
118

119
export type NotificationsPage = {
120
  notifications: PublicNotification[];
121
  nextCursor: number | null;
122
};
123

124
// Ids are monotonic, so ordering by id is newest-first and gives the cursor a
125
// stable, unique key. One extra row is fetched to know whether more remain
126
export const getNotificationsByUserId = async (
127
  userId: number,
128
  params?: { limit?: number; cursor?: number }
129
): Promise<NotificationsPage> => {
130
  const limit = params?.limit ?? DEFAULT_NOTIFICATIONS_LIMIT;
131

132
  const rows = await prisma.notification.findMany({
133
    where: {
134
      userId,
135
      ...(params?.cursor !== undefined && { id: { lt: params.cursor } }),
136
    },
137
    orderBy: { id: "desc" },
138
    take: limit + 1,
139
    select: publicNotificationSelect,
140
  });
141

142
  const hasMore = rows.length > limit;
143
  const notifications = hasMore ? rows.slice(0, limit) : rows;
144

145
  return {
146
    notifications,
147
    nextCursor: hasMore ? notifications[notifications.length - 1].id : null,
148
  };
149
};