eletrotupi / tcc/ api/src/services/notification.service.tsf6425d4096f950efeafb0cfb4405841fd35f2f8e
3.7 KB | 150 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
    notificationId: notification.id,
87
    ...(rule.screen ? { screen: rule.screen } : {}),
88
    ...(regimenId !== null ? { regimenId } : {}),
89
  };
90

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

104
    return notification;
105
  }
106

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

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

118
const DEFAULT_NOTIFICATIONS_LIMIT = 30;
119

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

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

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

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

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