Skip to content

Commit 667c671

Browse files
authored
Merge pull request #218 from Plant-for-the-Planet-org/develop
Merge: notification-fix, env-cleanup
2 parents 2e2c153 + 2d42466 commit 667c671

File tree

24 files changed

+736
-200
lines changed

24 files changed

+736
-200
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ node_modules
66
.DS_Store
77
.turbo
88
.idea
9-
.vercel
9+
.vercel

.prettierignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
**

apps/server/.prettierrc.cjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
arrowParens: "avoid",
3+
bracketSameLine: true,
4+
bracketSpacing: false,
5+
singleQuote: true,
6+
trailingComma: "all",
7+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- AlterTable
2+
ALTER TABLE "Notification" ADD COLUMN "isSkipped" BOOLEAN NOT NULL DEFAULT false,
3+
ADD COLUMN "metadata" JSONB;

apps/server/prisma/schema.prisma

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ model Site {
7979
slices Json? // Will be something like ["1","2"] or ["3"] or ["1"] or ["7","8"]
8080
detectionGeometry Unsupported("geometry")?
8181
originalGeometry Unsupported("geometry")?
82-
detectionArea Float?
82+
detectionArea Float? @default(0)
8383
alerts SiteAlert[]
8484
project Project? @relation(fields: [projectId], references: [id])
8585
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -149,6 +149,8 @@ model Notification {
149149
destination String
150150
sentAt DateTime?
151151
isDelivered Boolean @default(false)
152+
isSkipped Boolean @default(false)
153+
metadata Json?
152154
}
153155

154156
model Stats {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type {NextApiRequest, NextApiResponse} from 'next';
2+
3+
export interface AdditionalOptions {
4+
req?: NextApiRequest;
5+
res?: NextApiResponse;
6+
}

apps/server/src/Interfaces/NotificationParameters.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ export interface NotificationParameters {
66
subject?: string;
77
url?: string;
88
authenticationMessage?: boolean;
9-
otp?: string,
10-
siteName?: string,
9+
otp?: string;
10+
siteName?: string;
1111
alert?: {
1212
id: string;
1313
type: string;
@@ -21,4 +21,10 @@ export interface NotificationParameters {
2121
siteId: string;
2222
siteName: string;
2323
};
24+
site?: {
25+
id: string;
26+
};
27+
user?: {
28+
id: string;
29+
};
2430
}

apps/server/src/Services/Notifications/SendNotifications.ts

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,40 @@
1+
import type {AdditionalOptions} from '../../Interfaces/AdditionalOptions';
12
import {type AlertMethodMethod} from '../../Interfaces/AlertMethod';
2-
import NotifierRegistry from '../Notifier/NotifierRegistry';
3-
import {type NotificationParameters} from '../../Interfaces/NotificationParameters';
43
import type DataRecord from '../../Interfaces/DataRecord';
4+
import {type NotificationParameters} from '../../Interfaces/NotificationParameters';
5+
import {getLocalTime} from '../../../src/utils/date';
6+
import {env} from '../../env.mjs';
57
import {prisma} from '../../server/db';
68
import {logger} from '../../server/logger';
7-
import {getLocalTime} from '../../../src/utils/date';
9+
import NotifierRegistry from '../Notifier/NotifierRegistry';
10+
import {NOTIFICATION_METHOD} from '../Notifier/methodConstants';
11+
12+
const MAX_RETRIES = 0;
13+
const ALERT_SMS_DISABLED = env.ALERT_SMS_DISABLED;
14+
const ALERT_WHATSAPP_DISABLED = env.ALERT_WHATSAPP_DISABLED;
815

916
// get all undelivered Notifications and using relation from SiteAlert, get the data on Site
1017
// for each notification, send the notification to the destination
1118
// After sending notification update the notification table to set isDelivered to true and sentAt to current time
1219
// If notification fails to send, increment the failCount in all alertMethods table where destination and method match.
13-
const sendNotifications = async (): Promise<number> => {
20+
const sendNotifications = async ({req}: AdditionalOptions): Promise<number> => {
21+
const alertMethodsExclusionList = [];
22+
if (ALERT_SMS_DISABLED)
23+
alertMethodsExclusionList.push(NOTIFICATION_METHOD.SMS);
24+
if (ALERT_WHATSAPP_DISABLED)
25+
alertMethodsExclusionList.push(NOTIFICATION_METHOD.WHATSAPP);
26+
1427
const take = 10;
1528
let successCount = 0;
1629
let continueProcessing = true;
30+
let retries = 0;
1731
while (continueProcessing) {
1832
const notifications = await prisma.notification.findMany({
1933
where: {
34+
isSkipped: false,
2035
isDelivered: false,
2136
sentAt: null,
22-
alertMethod: {notIn: ['sms', 'whatsapp']}
37+
alertMethod: {notIn: alertMethodsExclusionList},
2338
},
2439
include: {
2540
siteAlert: {
@@ -40,13 +55,17 @@ const sendNotifications = async (): Promise<number> => {
4055
logger(`Notifications to be sent: ${notifications.length}`, 'info');
4156

4257
const successfulNotificationIds: string[] = [];
43-
const failedAlertMethods: { destination: string; method: AlertMethodMethod }[] = [];
58+
const successfulDestinations: string[] = [];
59+
const failedAlertMethods: {
60+
destination: string;
61+
method: AlertMethodMethod;
62+
}[] = [];
4463

4564
await Promise.all(
4665
notifications.map(async notification => {
4766
try {
4867
const {id, destination, siteAlert} = notification;
49-
const alertMethod = notification.alertMethod as AlertMethodMethod
68+
const alertMethod = notification.alertMethod as AlertMethodMethod;
5069
const {
5170
id: alertId,
5271
confidence,
@@ -59,6 +78,8 @@ const sendNotifications = async (): Promise<number> => {
5978
eventDate,
6079
site,
6180
} = siteAlert;
81+
// const siteId = site.id;
82+
// const userId = site.userId;
6283

6384
// if distance = 0 then the fire is inside the site's original geometry
6485
// if distance > 0 then the fire is outside the site's original geometry
@@ -144,53 +165,64 @@ const sendNotifications = async (): Promise<number> => {
144165
siteName: siteName,
145166
data: data as DataRecord,
146167
},
168+
// site: {id: siteId},
169+
// user: {id: userId!},
147170
};
148171
const notifier = NotifierRegistry.get(alertMethod);
149172

150173
const isDelivered = await notifier.notify(
151174
destination,
152175
notificationParameters,
176+
{req},
153177
);
154178

155179
if (isDelivered === true) {
156180
successfulNotificationIds.push(id);
181+
successfulDestinations.push(destination);
157182
successCount++;
158183
} else {
159-
failedAlertMethods.push({ destination, method: alertMethod });
184+
failedAlertMethods.push({destination, method: alertMethod});
160185
}
161186
} catch (error) {
162-
logger(`Error processing notification ${notification.id}:`, 'error');
187+
logger(
188+
`Error processing notification ${notification.id}: ${
189+
(error as Error)?.message
190+
}`,
191+
'error',
192+
);
163193
}
164-
})
194+
}),
165195
);
166196

167-
// UpdateMany notification
197+
// UpdateMany notification
168198
if (successfulNotificationIds.length > 0) {
169199
await prisma.notification.updateMany({
170-
where: {
171-
id: {
172-
in: successfulNotificationIds,
173-
},
174-
},
175-
data: {
176-
isDelivered: true,
177-
sentAt: new Date(),
178-
},
200+
where: {id: {in: successfulNotificationIds}},
201+
data: {isDelivered: true, sentAt: new Date()},
179202
});
180-
}
181-
// Handle failed notifications
182-
for (const { destination, method } of failedAlertMethods) {
183203
await prisma.alertMethod.updateMany({
184-
where: {
185-
destination: destination,
186-
method: method,
187-
},
188-
data: {
189-
failCount: {
190-
increment: 1,
191-
},
192-
},
204+
where: {destination: {in: successfulDestinations}},
205+
data: {failCount: 0},
206+
});
207+
}
208+
209+
retries += 1;
210+
if (retries >= MAX_RETRIES) {
211+
const unsuccessfulNotifications = notifications.filter(
212+
({id}) => !successfulNotificationIds.includes(id),
213+
);
214+
215+
const unsuccessfulNotificationIds = unsuccessfulNotifications.map(
216+
({id}) => id,
217+
);
218+
219+
await prisma.notification.updateMany({
220+
where: {id: {in: unsuccessfulNotificationIds}},
221+
data: {isSkipped: true, isDelivered: false, sentAt: null},
193222
});
223+
224+
continueProcessing = false;
225+
break;
194226
}
195227

196228
// skip += take; No need to skip take as we update the notifications to isDelivered = true
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
import {type NotificationParameters} from '../../Interfaces/NotificationParameters';
1+
import type {AdditionalOptions} from '../../Interfaces/AdditionalOptions';
2+
import type {NotificationParameters} from '../../Interfaces/NotificationParameters';
3+
import type {HandleFailedNotificationOptions} from './handleFailedNotification';
24

35
interface Notifier {
46
getSupportedMethods: () => string[];
57
notify: (
68
destination: string,
79
parameters: NotificationParameters,
10+
options?: AdditionalOptions,
811
) => Promise<boolean>;
12+
13+
handleFailedNotification?: (
14+
opts: HandleFailedNotificationOptions,
15+
) => Promise<void>;
916
}
1017

1118
export default Notifier;

apps/server/src/Services/Notifier/Notifier/DeviceNotifier.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import {NOTIFICATION_METHOD} from '../methodConstants';
44
import {env} from '../../../env.mjs';
55
import {logger} from '../../../server/logger';
66
import {prisma} from '../../../server/db';
7+
import {handleFailedNotification as genericFailedNotificationHandler} from '../handleFailedNotification';
78

89
class DeviceNotifier implements Notifier {
910
getSupportedMethods(): Array<string> {
1011
return [NOTIFICATION_METHOD.DEVICE];
1112
}
1213

13-
async deleteNotificationAndDevice(destination: string, notificationId: string): Promise<void> {
14+
async deleteNotificationAndDevice(
15+
destination: string,
16+
notificationId: string,
17+
): Promise<void> {
1418
try {
1519
// Delete the notification
1620
await prisma.notification.delete({
@@ -23,11 +27,17 @@ class DeviceNotifier implements Notifier {
2327
where: {
2428
destination: destination,
2529
method: NOTIFICATION_METHOD.DEVICE,
26-
}
30+
},
2731
});
28-
logger(`Notification with ID: ${notificationId} deleted and alertMethod for destination: ${destination} has been unverified and disabled.`, "info");
32+
logger(
33+
`Notification with ID: ${notificationId} deleted and alertMethod for destination: ${destination} has been unverified and disabled.`,
34+
'info',
35+
);
2936
} catch (error) {
30-
logger(`Database Error: Couldn't modify the alertMethod or delete the notification: ${error}`, "error");
37+
logger(
38+
`Database Error: Couldn't modify the alertMethod or delete the notification: ${error}`,
39+
'error',
40+
);
3141
}
3242
}
3343

@@ -41,6 +51,12 @@ class DeviceNotifier implements Notifier {
4151
): Promise<boolean> {
4252
const {message, subject, url, alert} = parameters;
4353

54+
// Check if OneSignal is configured
55+
if (!env.ONESIGNAL_APP_ID || !env.ONESIGNAL_REST_API_KEY) {
56+
logger(`Push notifications are disabled: OneSignal is not configured`, 'warn');
57+
return Promise.resolve(false);
58+
}
59+
4460
// logger(`Sending message ${message} to ${destination}`, "info");
4561

4662
// construct the payload for the OneSignal API
@@ -62,21 +78,29 @@ class DeviceNotifier implements Notifier {
6278
},
6379
body: JSON.stringify(payload),
6480
});
65-
console.log(response);
81+
// console.log(response);
6682
if (!response.ok) {
6783
logger(
6884
`Failed to send device notification. Error: ${response.statusText} for ${parameters.id}`,
6985
'error',
7086
);
87+
88+
await this.handleFailedNotification({
89+
destination: destination,
90+
method: NOTIFICATION_METHOD.DEVICE,
91+
});
92+
7193
// If device not found
72-
if(response.status === 404){
73-
await this.deleteNotificationAndDevice(destination, parameters.id)
74-
}
94+
// if (response.status === 404) {
95+
// await this.deleteNotificationAndDevice(destination, parameters.id);
96+
// }
7597
return false;
7698
}
7799

78100
return true;
79101
}
102+
103+
handleFailedNotification = genericFailedNotificationHandler;
80104
}
81105

82106
export default DeviceNotifier;

0 commit comments

Comments
 (0)