import htmlToText from 'html-to-text' import moment from 'moment' import { NotificationGetPayload, NotificationPreference, NOTIFICATION_DELIVERY_PLATFORM, NOTIFICATION_TYPE, } from 'nexus-plugin-prisma/client' import { performance } from 'perf_hooks' import Constants from '../../constants/general' import { generateUnsubscribeToken } from '../../controllers/email/helpers' import * as UserController from '../../controllers/user' import { prismaClient as prisma } from '../../prismaClient' import * as EmailUtils from '../../utils/email' import ExpoNotification from '../../utils/expoNotification' import GeneralUtils from '../../utils/general' import { notificationLogger } from '../../utils/logger' import { MANAGER_NOTIFICATION_TYPES, NotificationType } from '../../utils/notification' import * as PusherUtils from '../../utils/pusher' import Roles from '../../utils/roles' import * as Twilio from '../../utils/twilio' const NOTIFICATION_SELECT = { id: true, sent: true, } // Not sure of this and the name... // Had to do this because below we initialize the notification before using prisma // "let notification" will have the type any which is not what we want... // Need to find a cleaner way to do this later to not have // the "NOTIFICATION_SELECT" above + this "NotificationSelectType"... type NotificationSelectType = NotificationGetPayload<{ select: { id: true sent: true } }> function userHasEnableNotificationForPlatform( user: { hasAppNotificationEnable: boolean hasEmailNotificationEnable: boolean hasSMSNotificationEnable: boolean }, platform: NOTIFICATION_DELIVERY_PLATFORM ) { if (platform === 'APP') return user.hasAppNotificationEnable if (platform === 'EMAIL') return user.hasEmailNotificationEnable if (platform === 'SMS') return user.hasSMSNotificationEnable } function managerEnabledNotificationType( user: { id: string notificationPreferences: NotificationPreference[] hasAppNotificationEnable: boolean hasEmailNotificationEnable: boolean hasSMSNotificationEnable: boolean }, type: NOTIFICATION_TYPE, platform: NOTIFICATION_DELIVERY_PLATFORM ) { const notificationTypeIsValid = MANAGER_NOTIFICATION_TYPES.includes(type) if (!notificationTypeIsValid) { notificationLogger.error( `Notification type: ${type} is not included in the default notification types` ) return userHasEnableNotificationForPlatform(user, platform) } const notificationPreference = user.notificationPreferences.find( (notificationPreference) => notificationPreference.type === type && notificationPreference.platform === platform ) if (!notificationPreference) { notificationLogger.error( `Can't find notification preference for type ${type} and platform ${platform} and user id ${user.id}` ) return userHasEnableNotificationForPlatform(user, platform) } return notificationPreference.isEnable } // Probably can import something from @types/Bull instead... type JobDataType = { data: NotificationType & { userId: string } } export default async ({ data: jobData }: JobDataType) => { const startTime = performance.now() const { userId, title, titleFr, subtitle, subtitleFr, message, messageFr, data, serviceProjectId, projectId, noAppNotification, forceSMS, noSMS, smsData, forceEmail, forceApp, noEmail, emailData, type, forceSend, sender, isFromWalter, skipValidating, // Use with precaution } = jobData try { const project = projectId ? await prisma.project.findOne({ where: { id: projectId }, select: { id: true, name: true, colorHex: true, logo: { select: { url: true, }, }, managingCompany: { select: { id: true, longName: true, shortName: true, logo: { select: { url: true } }, phone: { select: { number: true, nationalFormat: true } }, address: { select: { address1: true, apartmentNumber: true, zip: true, city: true, state: true, country: true, countryCode: true, provinceCode: true, }, }, }, }, }, }) : null const serviceProject = serviceProjectId ? await prisma.serviceProject.findOne({ where: { id: serviceProjectId } }) : null // const notificationLogs = { // title, // titleFr, // subtitle, // subtitleFr, // message, // messageFr, // notificationType: type, // app: null, // email: null, // sms: null, // error: null, // pusher: null, // userId: null, // userName: null, // } const notificationErrors = { errorMessage: null, sentErrorMessage: null, sentByAppErrorMessage: null, sentByEmailErrorMessage: null, sentBySMSErrorMessage: null, } let notification: NotificationSelectType try { let user = await prisma.user.findOne({ where: { id: userId }, include: { phone: true, homePhone: true, officePhone: true, managingCompanyRole: { include: { projects: { select: { id: true, }, }, }, }, currentProject: { select: { id: true, name: true, }, }, notificationPreferences: true, }, }) // notificationLogs.userId = user.id // notificationLogs.userName = GeneralUtils.getUserName(user) // Format because Property manager might put user information at the wrong place in Condo Manager user = { ...user, email: user.email || user.secondaryEmail, phone: user.phone || user.homePhone || user.officePhone, } const userIsFrench = user.preferedLanguage === 'fr' // Did that in case we want to resend a post for example // We will send notification but we want to take the same notifcaitions created and just update them // Did that to fix a bug where Tommy from Griffin sent a post const existingNotification = process.env.NODE_ENV === 'development' ? ( await prisma.notification.findMany({ take: 1, where: { user: { id: user.id, }, post: { id: data.id, }, }, select: NOTIFICATION_SELECT, }) )[0] : null // Create the Walter notification for each user notification = existingNotification || (await prisma.notification // @ts-ignore .create({ data: { // @ts-ignore type, title, subtitle, message, forceSend, noEmail, noSMS, forceEmail, forceApp, noApp: noAppNotification, forceSMS, user: { connect: { id: user.id } }, ...(data && { data: { id: data.id } }), // Just the id, passing all data is useless for now. ...(project?.id && { project: { connect: { id: project.id } } }), ...(serviceProject?.id && { serviceProject: { connect: { id: serviceProject.id } }, }), // Link notification with post ...((type === 'NEW_POST' || type === 'NEW_PROXY') && data?.id && { post: { connect: { id: data.id, }, }, }), }, select: NOTIFICATION_SELECT, })) // Only resend if it wasn't sent... we don't want to spam the person too if (existingNotification?.sent) { return } // Make sure user can receive notification if (!skipValidating) { // ACTIVE if (!user.active) { notificationErrors.errorMessage = "User can't receive notification because he/she is not active." notificationErrors.sentErrorMessage = "User can't receive notification because he/she is not active." // notificationLogs.error = "❌ Can't receive notification because user is not active" } // WAITING FOR APPROVAL if (user.waitingForApproval) { notificationErrors.errorMessage = "User can't receive notification because he/she is waiting for approval." notificationErrors.sentErrorMessage = "User can't receive notification because he/she is waiting for approval." // notificationLogs.error = "❌ Can't receive notification: needs to be approved" } // RECEIVED INVITATION / INSTALLED if (user.role === 'RESIDENT' && !user.hasReceivedInvitation && !user.hasInstalledApp) { notificationErrors.errorMessage = "User can't receive notification because he/she didn't receive the invitation to use Walter yet." notificationErrors.sentErrorMessage = "User can't receive notification because he/she didn't receive the invitation to use Walter yet." // notificationLogs.error = // "❌ Can't receive notification: Haven't received invitation or installed the app" } // MANAGER ROLE if (user.role === 'MANAGER' && user.managingCompanyRole) { if (!Roles.userCanReceiveNotificationForNotificationType(user, type)) { notificationErrors.errorMessage = "User can't receive notification because he/she doesn't have the right role." notificationErrors.sentErrorMessage = "User can't receive notification because he/she doesn't have the right role." // notificationLogs.error = "❌ Can't receive notification: Doesn't have the right role" } else if (!Roles.hasAccessToProject(project?.id, user)) { notificationErrors.errorMessage = 'User role is not associated with this project' notificationErrors.sentErrorMessage = 'User role is not associated with this project' // notificationLogs.error = // "❌ Can't receive notification: Role is not associated to this project" } } } if (!notificationErrors.errorMessage) { // PUSHER // Only send to managers for now because resident show local notification when new notification created if (user.hasInstalledApp && (user.role === 'MANAGER' || user.role === 'PROVIDER')) { try { await PusherUtils.sendEvent( 'notification-channel', `${user.role.toLowerCase()}-${user.id}`, { id: notification.id, title: user.preferedLanguage === 'fr' && titleFr ? titleFr : title, subtitle: user.preferedLanguage === 'fr' && subtitleFr ? subtitleFr : subtitle, message: htmlToText.fromString( user.preferedLanguage === 'fr' && messageFr ? messageFr : message ), type, ...(project?.id && { project: { id: project.id, }, }), ...(serviceProject?.id && { serviceProject: { id: serviceProject.id, }, }), ...(data && { data: { id: data.id, // For sendbird notification ...(data.channel && { channel_url: data.channel.channel_url, channel: { channel_url: data.channel.channel_url, }, }), }, }), } ) // notificationLogs.pusher = '✅ Pusher notification sent' } catch (error) { notificationLogger.error(error) // notificationLogs.pusher = error?.message || error } } // Will be use later to send email/sms in case we absolutely need a notifciation to be sent let receivedNotification = false const userHasAppNotification = user.role === 'MANAGER' && type ? managerEnabledNotificationType(user, type, 'APP') : user.hasAppNotificationEnable // APP notifications if ( process.env.SEND_NOTIFICATION === 'true' && (userHasAppNotification || forceApp) && (user.oneSignalUserId || user.expoNotificationToken) && !noAppNotification ) { try { const newNumberOfBadges = user.appNotificationsBadgesNumber ? user.appNotificationsBadgesNumber + 1 : 1 if ( (user.role === 'MANAGER' || user.role === 'PROVIDER') && user.expoNotificationToken ) { await ExpoNotification.sendNotification({ title: htmlToText.fromString(title || 'New notification'), subtitle: htmlToText.fromString( project?.name ? subtitle ? `${project.name} - ${subtitle}` : project.name : subtitle || '' ), body: htmlToText.fromString( GeneralUtils.truncate({ str: message, }) ), token: user.expoNotificationToken, badgeNumber: user.appNotificationsBadgesNumber ? user.appNotificationsBadgesNumber + 1 : 1, data, }) } // RESIDENT else if (user.role === 'RESIDENT') { let residentIsUsingExpoNotification = false if (user.role === 'RESIDENT') { if (user.operatingSystem === 'ANDROID') { residentIsUsingExpoNotification = parseInt(user.currentAppVersion) >= 29 } else if (!user.currentAppVersion) { residentIsUsingExpoNotification = true } else { const [major, minor, revision] = user.currentAppVersion.split('.') residentIsUsingExpoNotification = parseInt(major) >= 3 && parseInt(minor) >= 1 && parseInt(revision) >= 1 } } if (residentIsUsingExpoNotification && user.expoNotificationToken) { await ExpoNotification.sendNotification({ title: userIsFrench ? htmlToText.fromString(titleFr || title || 'Notification') : htmlToText.fromString(title || 'Notification'), ...(!!subtitle && { subtitle: userIsFrench ? htmlToText.fromString(subtitleFr || subtitle || 'Notification') : htmlToText.fromString(subtitle || 'Notification'), }), ...(message && { body: htmlToText.fromString( GeneralUtils.truncate({ str: userIsFrench ? messageFr || message : message, }) ), }), token: user.expoNotificationToken, badgeNumber: user.appNotificationsBadgesNumber ? user.appNotificationsBadgesNumber + 1 : 1, data, }) } } // notificationLogs.app = '✅ APP notification sent' receivedNotification = true await prisma.notification.update({ where: { id: notification.id, }, data: { sent: true, sentAt: moment().toDate(), sentByApp: true, sentByAppAt: moment().toDate(), delivered: true, deliveredAt: moment().toDate(), deliveredByApp: true, deliveredByAppAt: moment().toDate(), }, select: NOTIFICATION_SELECT, }) await prisma.user.update({ where: { id: user.id, }, data: { appNotificationsBadgesNumber: newNumberOfBadges, }, }) } catch (error) { notificationLogger.error(error) // notificationLogs.app = error?.message || error notificationErrors.sentByAppErrorMessage = "Couldn't send app push notification. Permission might be refused." } } else if (process.env.SEND_NOTIFICATION !== 'true') { // notificationLogs.app = // '❌ Couldn\'t send because "process.env.SEND_NOTIFICATION" is not true' } else if (!userHasAppNotification) { // notificationLogs.app = "❌ Couldn't send because user has not enabled it" notificationErrors.sentByAppErrorMessage = "Couldn't send because user has not enabled app notifications" } else if (!user.oneSignalUserId && !user.expoNotificationToken && user.hasInstalledApp) { // notificationLogs.app = // "❌ Couldn't send because user doesn't have oneSignal or expo notification token" notificationErrors.sentByAppErrorMessage = "Couldn't send because user didn't accept notifications permissions" } else if (noAppNotification) { // notificationLogs.app = // "❌ Couldn't send because we said to not send this notification by app" notificationErrors.sentByAppErrorMessage = "Couldn't send because we said to not send this notification by app" } let receivedEmail = false const userHasEmailNotification = user.role === 'MANAGER' && type ? managerEnabledNotificationType(user, type, 'EMAIL') : user.hasEmailNotificationEnable const needToSendEmail = forceSend && !receivedNotification // EMAIL notification if (user.email && !noEmail && (userHasEmailNotification || forceEmail || needToSendEmail)) { try { const template: EmailUtils.TemplateNameTypes = emailData?.template || (user.role === 'MANAGER' ? 'MANAGER_GENERAL_NOTIFICATION' : user.preferedLanguage === 'fr' ? 'RESIDENT_GENERAL_NOTIFICATION_FR' : 'RESIDENT_GENERAL_NOTIFICATION') const from = emailData?.from || (isFromWalter || serviceProject ? { email: Constants.EMAIL_COMMUNICATION_WALTER_FROM_EMAIL, name: Constants.EMAIL_COMMUNICATION_WALTER_FROM_NAME, } : { email: Constants.EMAIL_COMMUNICATION_PARSE_FROM_EMAIL, name: project ? project.managingCompany.shortName || project.managingCompany.longName : Constants.EMAIL_COMMUNICATION_WALTER_FROM_NAME, }) const customArgs = { notificationId: notification.id, emailTemplate: template, ...(emailData?.customArgs || {}), } const subject = emailData?.subject || (isFromWalter || serviceProject || user.role === 'MANAGER' ? `${title || ''}${project && project.name ? ` - ${project.name}` : ''}` : `${project && project.name ? `${project.name} - ` : ''}${ !user.preferedLanguage ? titleFr && title ? `${titleFr} / ${title}` : title : user.preferedLanguage === 'fr' ? titleFr || title : title }`) const customVariables = { unsubscribeToken: generateUnsubscribeToken(user.id), preferedLanguage: user.preferedLanguage, hasInstalledApp: user.hasInstalledApp, isResident: user.role === 'RESIDENT', title: !user.preferedLanguage ? titleFr && title ? `${titleFr} / ${title}` : title : user.preferedLanguage === 'fr' ? titleFr || title : title, subtitle: !user.preferedLanguage ? subtitleFr && subtitle ? `${subtitleFr} / ${subtitle}` : subtitle : user.preferedLanguage === 'fr' ? subtitleFr || subtitle : subtitle, body: !user.preferedLanguage ? messageFr && message ? `${messageFr} / ${message}` : message : user.preferedLanguage === 'fr' ? messageFr || message : message, bodySplitted: !user.preferedLanguage ? messageFr && message ? [ ...GeneralUtils.splitTextWithLineBreak(`${messageFr}\n\n`), ...GeneralUtils.splitTextWithLineBreak(message), ] : GeneralUtils.splitTextWithLineBreak(message) : user.preferedLanguage === 'fr' && messageFr ? GeneralUtils.splitTextWithLineBreak(messageFr) : GeneralUtils.splitTextWithLineBreak(message), // NOT USED ANYMORE logoText: isFromWalter || serviceProject || user.role === 'MANAGER' ? Constants.WALTER_LOGO_BLACK : project && project.managingCompany ? project.managingCompany.shortName || project.managingCompany.longName || 'Walter' : 'Walter', // NOT USED ANYMORE logo: isFromWalter || serviceProject || user.role === 'MANAGER' ? Constants.WALTER_LOGO_BLACK : project && project.managingCompany && project.managingCompany.logo ? project.managingCompany.logo.url : null, managingCompanyLogoUrl: isFromWalter || serviceProject || user.role === 'MANAGER' ? Constants.WALTER_LOGO_BLACK : project && project.managingCompany && project.managingCompany.logo ? project.managingCompany.logo.url : null, projectLogoUrl: project && project.logo ? project.logo.url : null, projectColor: project && project.colorHex ? project.colorHex : '#000000', projectName: project ? project.name || '' : '', buildingName: project ? project.name || '' : '', preheader: user.preferedLanguage === 'fr' ? messageFr || subtitleFr || titleFr : message || subtitle || title, ...(isFromWalter || serviceProject ? { senderName: 'Walter', senderTitle: 'Smart building technology', senderEmail: 'info@usewalter.com', senderPhoneNumber: '1 800-881-2629', senderAddress: '2365 St-Patrick street, H3K 1B3, Montreal, Quebec, Canada', projectName: user.currentProject ? user.currentProject.name : null, } : { senderName: project.managingCompany.longName || project.managingCompany.shortName, senderTitle: '', // senderEmail: project.managingCompany.email, // FOR NOW, DON'T PUT EMAIL senderPhoneNumber: project.managingCompany.phone ? project.managingCompany.phone.nationalFormat || GeneralUtils.formatPhoneNumber(project.managingCompany.phone.number) : '', senderAddress: GeneralUtils.getFormattedAddress( project.managingCompany.address ), projectName: project.name, }), // In case we send sender data and want to overwrite default value // ...(sender && sender.name && { senderName: sender.name }), ...(sender && sender.title && { senderTitle: sender.title }), // ...(sender && sender.email && { senderEmail: sender.email }), // FOR NOW, DON'T PUT EMAIL ...(sender && sender.phoneNumber && { senderPhoneNumber: sender.phoneNumber }), ...(sender && sender.address && { senderAddress: sender.address }), // Install the app message info ...(!user.hasInstalledApp && user.role === 'RESIDENT' ? { showEnglishInstallText: user.preferedLanguage === 'en' || user.preferedLanguage === null, showFrenchInstallText: user.preferedLanguage === 'fr', userPhoneNumber: user.phone ? user.phone.nationalFormat || GeneralUtils.formatPhoneNumber(user.phone.number) : null, userEmail: user.email, } : null), ...(emailData?.customVariables || {}), } const data = { ...emailData, to: [{ email: user.email }], template, from, customArgs, subject, customVariables, } await EmailUtils.send(data) // notificationLogs.email = '✅ Email notification sent' await prisma.notification.update({ where: { id: notification.id, }, data: { sent: true, sentAt: moment().toDate(), sentByEmail: true, sentByEmailAt: moment().toDate(), }, select: NOTIFICATION_SELECT, }) receivedEmail = true } catch (error) { notificationLogger.error(error) // notificationLogs.email = error?.message || error notificationErrors.sentByEmailErrorMessage = "Couldn't send email notification." } } // else if (user.cantDeliverEmail) { // notificationLogs.email = `❌ Couldn't send because user cant be delivered emails` // notificationErrors.sentByEmailErrorMessage = `Couldn't send because user has an invalid email` // } else if (noEmail) { // notificationLogs.email = "❌ Couldn't send because we don't want to" notificationErrors.sentByEmailErrorMessage = "Couldn't send by email because notification was not allowed by Walter to be sent by email" } else if (!user.email) { // notificationLogs.email = "❌ Couldn't send because user has no email" notificationErrors.sentByEmailErrorMessage = "Couldn't send by email because user doesn't have one." } else if (!userHasEmailNotification) { // notificationLogs.email = "❌ Couldn't send because user has not enabled it" notificationErrors.sentByEmailErrorMessage = "Couldn't send because user has not enabled email notifications" } else if (user.role === 'RESIDENT' && receivedNotification) { // notificationLogs.email = // "❌ Couldn't send because it was already send by app notification" notificationErrors.sentByEmailErrorMessage = "Couldn't send because it was already sent by app notification and user don't want to receive an email if the app notification was delivered" } const userHasSMSNotification = user.role === 'MANAGER' && type ? managerEnabledNotificationType(user, type, 'SMS') : user.hasSMSNotificationEnable const needToSendSMS = forceSend && !receivedNotification && !receivedEmail // SMS notification if ((forceSMS || userHasSMSNotification || needToSendSMS) && user.phone?.number && !noSMS) { try { let bodySMSMessage = '' if (project && project.name) { bodySMSMessage += `${project.name}` } if (title) { if (!user.preferedLanguage && titleFr) { bodySMSMessage += `\n\n${titleFr} / ${title}` } else if (user.preferedLanguage === 'fr') { bodySMSMessage += `\n\n${titleFr || title}` } else { bodySMSMessage += `\n\n${title}` } } if (subtitle) { if (!user.preferedLanguage && subtitleFr) { bodySMSMessage += `\n\n${subtitleFr} / ${subtitle}` } else if (user.preferedLanguage === 'fr') { bodySMSMessage += `\n\n${subtitleFr || subtitle}` } else { bodySMSMessage += `\n\n${subtitle}` } } if (message) { if (!user.preferedLanguage && messageFr) { bodySMSMessage += `\n\n${GeneralUtils.truncate({ str: messageFr, maxChars: 30, })} / ${GeneralUtils.truncate({ str: htmlToText.fromString(message), maxChars: 30, })}` } else if (user.preferedLanguage === 'fr') { bodySMSMessage += `\n\n${ GeneralUtils.truncate({ str: messageFr, maxChars: 30, }) || GeneralUtils.truncate({ str: htmlToText.fromString(message), maxChars: 30, }) }` } else { bodySMSMessage += `\n\n${GeneralUtils.truncate({ str: htmlToText.fromString(message), maxChars: 30, })}` } // ? `*Le message contient trop de charactère. Veuillez utilisez l'application Walter pour avoir plus de détails.*` // : `*The message has too many characters. Please use Walter's application to see more the details.*` } if (sender) { bodySMSMessage += `\n\n-\n${GeneralUtils.renderWithLineBreak(Object.values(sender))}` } if (user.role === 'RESIDENT') { if (user.hasInstalledApp) { bodySMSMessage += `\n\n${UserController.getMoreDetailsInWalterAppMessage({ language: user.preferedLanguage, })}` } else { bodySMSMessage += `\n\n${await UserController.getDownloadWalterMessage({ user, projectId: project ? project.id : null, })}` } } if (forceSMS && user.role === 'RESIDENT' && !user.hasSMSNotificationEnable) { bodySMSMessage += `\n\n${ user.preferedLanguage === 'fr' ? "Vous recevez ce SMS car votre gestionnaire a forcé l'envoie par message texte afin de s'assurer que vous receviez bien le message." : "You've received this SMS because your manager forced it to make sure you receive the message." }` } const result = await Twilio.sendSMS({ to: user.phone.number, body: bodySMSMessage, itemId: notification.id, ...smsData, }) // notificationLogs.sms = '✅ SMS notifications sent' if (result?.sid) { await prisma.notification.update({ where: { id: notification.id, }, data: { twilioMessageSid: result.sid, sent: true, sentAt: moment().toDate(), sentBySMS: true, sentBySMSAt: moment().toDate(), }, select: NOTIFICATION_SELECT, }) } } catch (error) { notificationLogger.error(error) // notificationLogs.sms = error?.message || error notificationErrors.sentBySMSErrorMessage = "Couldn't send SMS notification." } } else if (process.env.SEND_SMS !== 'true') { notificationErrors.sentBySMSErrorMessage = "Couldn't send because there's no more SMS credit for this month" // notificationLogs.sms = "❌ Couldn't send because SEND_SMS is not true" } else if (!user.phone) { // notificationLogs.sms = "❌ Couldn't send because user has no mobile phone number" } // else if (user.cantDeliverSMS) { // notificationLogs.sms = `❌ Couldn't send because user cant be delivered SMS` // notificationErrors.sentBySMSErrorMessage = `Couldn't send because user has an invalid mobile phone number` // } else if (noSMS) { // notificationLogs.sms = "❌ Couldn't send because we don't want to" notificationErrors.sentBySMSErrorMessage = "Couldn't send by SMS because notification was not allowed to be sent by SMS" } else if (!userHasSMSNotification) { // notificationLogs.sms = "❌ Couldn't send because user has not enabled it" notificationErrors.sentBySMSErrorMessage = "Couldn't send because user has not enabled SMS notifications" } else if (user.role === 'RESIDENT' && (receivedNotification || receivedEmail)) { // notificationLogs.sms = // "❌ Couldn't send because it was already sent with app notification OR email notification" notificationErrors.sentBySMSErrorMessage = "Couldn't send because it was already sent by app notification or email and user don't want to receive an email or SMS if the app notification was delivered" } } } catch (error) { notificationLogger.error(error) if (!notificationErrors.errorMessage) { notificationErrors.errorMessage = 'Something went wrong' notificationErrors.sentErrorMessage = 'Something went wrong' } // notificationLogs.error = error?.message || error } if (notification?.id) { await prisma.notification.update({ where: { id: notification.id }, data: notificationErrors, }) } const seconds = moment.duration(Math.floor(performance.now() - startTime)).asSeconds() // notificationLogger.info(notificationLogs) notificationLogger.info(`Notification processed in ${seconds} seconds`) } catch (error) { notificationLogger.error(error) } }