import { Injectable } from '@nestjs/common' import { addDays, addHours, addMinutes, format, isMonday, isSunday, isTuesday, set } from 'date-fns' import { chunk, isNil } from 'lodash' import { UserNotFoundError } from '../../../errors' import { Appointment, Notification, NotificationType, Roles, User } from '../../../models' import { ReleaseNote } from '../../../models/release_note.model' import { AppointmentRepository, AvailabilityTimeRepository, ConfigRepository, MessageRepository, NotificationRepository, UserRepository, } from '../../../repositories' import { MailerSendOptions, MailService } from '../../../services' import { CountryCode, CountryHelper, getOffsetBy, parseWrittenDate } from '../../../utils' import { TranslateNotificationsHelper } from '../i18n' import { UserStatusUpdate } from '../models' const NEW_MESSAGES_TEMPLATE = 'new_messages_nutri' const NEW_RELEASE_NOTE_TEMPLATE = 'new_release_note' const NEW_SIGN_UP_TEMPLATE = 'new_sign_up' const APPOINTMENT_SCHEDULED_NUTRI = 'appointment_scheduled_nutri' const APPOINTMENT_CANCELLED_NUTRI = 'appointment_cancelled_nutri' const APPOINTMENT_TRANSFERRED_NUTRI = 'appointment_transferred_nutri' const APPOINTMENT_LEFT_TO_START_NUTRI = 'appointment_left_to_start_nutri' const PENDING_FOOD_PLAN_NUTRI = 'pending_food_plan_nutri' const MINUTES_TO_CHECK = 5 const NOTIFY_APPOINTMENT_START_MINUTES_WINDOW = 5 const NOTIFY_APPOINTMENT_PENDING_FOOD_PLAN_WINDOW = 5 type TimeExtracted = { hours: number; minutes: number } @Injectable() export class Nutritionists { constructor( private readonly appointmentRepository: AppointmentRepository, private readonly availabilityTimeRepository: AvailabilityTimeRepository, private readonly messageRepository: MessageRepository, private readonly notificationRepository: NotificationRepository, private readonly userRepository: UserRepository, private readonly translator: TranslateNotificationsHelper, private readonly mailService: MailService, private readonly configRepository: ConfigRepository, ) {} async notifyNewRelease(releaseNote: ReleaseNote): Promise { const countryCodes = releaseNote.countryCodes const users = await this.userRepository.findAll(true, countryCodes) await Promise.all( users.map(user => { const notification = this.buildReleaseNotification(releaseNote, user) return this.notificationRepository.create(notification, user.country_code) }), ) } async notifyNewMessage(nutritionistId: string, patientId: string): Promise { const nutritionist = await this.getNutritionist(nutritionistId) const notification = await this.buildMessageNotification(nutritionist, patientId) if (isNil(notification)) return const unreadsMessageNotifications = await this.notificationRepository.findUnreadsByTypeAndUserAndPatient( nutritionistId, patientId, NotificationType.MESSAGE, ) await Promise.all([ ...unreadsMessageNotifications.map(value => this.notificationRepository.delete(value.id)), this.notificationRepository.create(notification, CountryHelper.parseCode(nutritionist.country_code)), ]) } async notifyAppointmentScheduled(appointment: Appointment): Promise { if (appointment.state !== 'scheduled') return const nutritionist = await this.getNutritionist(appointment.user_id) const notification = await this.buildAppointmentNotification(nutritionist, appointment, APPOINTMENT_SCHEDULED_NUTRI) await this.notificationRepository.create(notification, CountryHelper.parseCode(nutritionist.country_code)) } async notifyAppointmentTransferred(appointment: Appointment, oldNutritionistId: string): Promise { if (appointment.state !== 'scheduled') return const oldNutritionist = await this.getNutritionist(oldNutritionistId) const notification = await this.buildAppointmentNotification( oldNutritionist, appointment, APPOINTMENT_TRANSFERRED_NUTRI, ) await Promise.all([ this.notificationRepository.create(notification, CountryHelper.parseCode(oldNutritionist.country_code)), this.notifyAppointmentScheduled(appointment), ]) } async notifyAppointmentCancelled(appointment: Appointment): Promise { const nutritionist = await this.getNutritionist(appointment.user_id) const notification = await this.buildAppointmentNotification(nutritionist, appointment, APPOINTMENT_CANCELLED_NUTRI) await this.notificationRepository.create(notification, CountryHelper.parseCode(nutritionist.country_code)) } async notifyAppointmentsLeftToStart(): Promise { const appointments = await this.appointmentRepository.findThatStartsAtHour({ date: addMinutes(new Date(), 1), windowInMinutes: NOTIFY_APPOINTMENT_START_MINUTES_WINDOW, state: ['scheduled', 'patient_waiting'], }) const chunked = chunk(appointments, 10) for (const appointmentsChunk of chunked) { await Promise.all( appointmentsChunk.map(async appointment => { const notification = await this.buildAppointmentNotification( appointment.user, appointment, APPOINTMENT_LEFT_TO_START_NUTRI, ) return this.notificationRepository.create( notification, CountryHelper.parseCode(appointment.user.country_code), ) }), ) } } async notifyPendingFoodPlanWithOneDayLeft(): Promise { const baseDate = new Date() const businessDay = this.getPreviousTwoBusinessDate(baseDate) if (isNil(businessDay)) return const appointments = await this.appointmentRepository.findThatStartsAtHour({ date: businessDay, windowInMinutes: NOTIFY_APPOINTMENT_PENDING_FOOD_PLAN_WINDOW, state: ['food_plan_pending', 'food_plan_in_construction'], }) const chunked = chunk(appointments, 10) for (const appointmentsChunk of chunked) { await Promise.all( appointmentsChunk.map(async appointment => { const notification = await this.buildFoodPlanNotification( appointment.user, appointment, PENDING_FOOD_PLAN_NUTRI, ) return this.notificationRepository.create( notification, CountryHelper.parseCode(appointment.user.country_code), ) }), ) } } async notifyNewSignUp(user: User): Promise { const countryCode = user.country_code const adminsToNotify = await this.userRepository.findByRoleAndCountryCode({ roles: [Roles.NUTRITIONIST_MANAGER], countryCodes: [CountryHelper.parseLanguageCode(countryCode)], attributes: ['id', 'email'], }) await Promise.all( adminsToNotify.map(async userToNotify => { const templates = await this.buildSignUpAdminNotifications(countryCode, userToNotify, user.id) return Promise.all([ this.notificationRepository.create(templates.notification, countryCode), ...(templates.email ? [this.mailService.send(templates.email)] : []), ]) }), ) } async notifyStatusUpdate(user: User, status: UserStatusUpdate): Promise { const emailId = this.translator.get(user.country_code, `email_templates.${status}` as any) const mynutriUrl = await this.getMynutriUrl() if (emailId) { await this.mailService.send({ to: user.email, templateId: emailId, dynamicTemplateData: { web_link: mynutriUrl, first_name: user.firstName, disapproval_reason: user.reproved_reason, }, }) } } private async buildSignUpAdminNotifications( countryCode: CountryCode, userToNotify: User, resourceId: string, ): Promise<{ notification?: Notification; email?: MailerSendOptions }> { const notification = this.translator.get(countryCode, `pushes.${NEW_SIGN_UP_TEMPLATE}` as any) const emailId = this.translator.get(countryCode, `email_templates.${NEW_SIGN_UP_TEMPLATE}` as any) const mynutriUrl = await this.getMynutriUrl() return { notification: new Notification({ title: notification['title'], message: notification['message'], created_at: new Date(), user_id: userToNotify.id, resource_id: resourceId, type: NotificationType.SIGN_UP, }), ...(emailId && { email: { to: userToNotify.email, templateId: emailId, dynamicTemplateData: { web_link: `${mynutriUrl}/users-management`, }, }, }), } } private buildReleaseNotification(releaseNote: ReleaseNote, user: User): Notification { const template = this.translator.get( CountryHelper.parseCode(user.country_code), `pushes.${NEW_RELEASE_NOTE_TEMPLATE}` as any, ) if (!template) throw new Error('Push notification template not found') return new Notification({ title: template['title'], message: template['message'], created_at: new Date(), resource_id: releaseNote.id, user_id: user.id, type: NotificationType.RELEASE, }) } private async buildMessageNotification(nutritionist: User, patientId: string): Promise { const messagesCount = await this.messageRepository.getUserPatientPendingMessagesCount(nutritionist.id, patientId) if (messagesCount <= 0) return const template = this.translator.get( CountryHelper.parseCode(nutritionist.country_code), `pushes.${NEW_MESSAGES_TEMPLATE}` as any, { params: { messagesCount } }, ) if (!template) throw new Error('Push notification template not found') return new Notification({ title: template['title'], message: template['message'], created_at: new Date(), patient_id: patientId, user_id: nutritionist.id, type: NotificationType.MESSAGE, }) } private async buildAppointmentNotification( nutritionist: User, appointment: Appointment, templateCode: string, ): Promise { const timesWithTimezone = await this.getAppointmentTimeByTimezone(appointment, nutritionist) const scheduledAtByTimezone = appointment.scheduled_to && set(appointment.scheduled_to, timesWithTimezone || {}) const scheduledAt = scheduledAtByTimezone && parseWrittenDate(scheduledAtByTimezone, CountryHelper.parseCode(nutritionist.country_code)) const template = this.translator.get( CountryHelper.parseCode(nutritionist.country_code), `pushes.${templateCode}` as any, { params: { scheduledAt, minutes: MINUTES_TO_CHECK } }, ) if (!template) throw new Error('Push notification template not found') return new Notification({ title: template['title'], message: template['message'], created_at: new Date(), patient_id: appointment.patient_id, user_id: nutritionist.id, type: NotificationType.APPOINTMENT, resource_id: appointment.id, }) } private async buildFoodPlanNotification( nutritionist: User, appointment: Appointment, templateCode: string, ): Promise { const template = this.translator.get( CountryHelper.parseCode(nutritionist.country_code), `pushes.${templateCode}` as any, ) if (!template) throw new Error('Push notification template not found') return new Notification({ title: template['title'], message: template['message'], created_at: new Date(), patient_id: appointment.patient_id, user_id: nutritionist.id, type: NotificationType.FOOD_PLAN, resource_id: appointment.id, }) } private async getNutritionist(nutritionistId: string): Promise { const nutritionist = await this.userRepository.get(nutritionistId) if (isNil(nutritionist)) throw new UserNotFoundError(nutritionistId) return nutritionist } private async getAppointmentTimeByTimezone(appointment: Appointment, nutritionist?: User): Promise { const availabilityTime = await this.availabilityTimeRepository.getByAppointmentId(appointment.id) if (isNil(availabilityTime)) return ( nutritionist && this.extractHoursAndMinutesFromTime( format(this.getDateWithOffset(nutritionist.country_code, appointment.scheduled_to), 'H:mm'), ) ) return this.extractHoursAndMinutesFromTime(availabilityTime.time) } private getDateWithOffset = (countryCode: CountryCode, scheduledTo: Date): Date => { return addHours(scheduledTo, getOffsetBy(countryCode, scheduledTo)) } private extractHoursAndMinutesFromTime = (time: string): TimeExtracted => time .split('-') .shift() .split(':') .map(info => Number(info)) .reduce((current, value, index) => ({ ...current, [Object.keys(current)[index]]: value }), { hours: 0, minutes: 0, }) private getPreviousTwoBusinessDate = (baseDate: Date): Date => { if (isSunday(baseDate)) return if (isMonday(baseDate) || isTuesday(baseDate)) return addDays(baseDate, -3) return addDays(baseDate, -2) } private async getMynutriUrl(): Promise { return this.configRepository.get('mynutri_url') } }