import { Injectable } from '@nestjs/common' import { addDays, addHours, addMinutes, startOfMinute } from 'date-fns' import { debug, Debugger } from 'debug' import { UnprocessableEntityError } from '../../../errors/unprocessable_entity.error' import { Appointment, Message, MESSAGE_TAGS, User } from '../../../models' import { Patient } from '../../../models/patient.model' import { AppointmentRepository, MessageRepository } from '../../../repositories' import { PatientRepository } from '../../../repositories/patient.repository' import { MessagingService, PushPayload } from '../../../services/messaging' import { parseTimeToMinutes } from '../../../utils' import { chunkedEach } from '../../../utils/async' import { LanguageCountryCode } from '../../../utils/countries' import { TranslateKey, TranslateParam } from '../../../utils/i18n' import { PatientsTranslations, TranslatePatientsHelper } from '../i18n' const NOTIFY_SCHEDULED_APPOINTMENT_WINDOW = 5 @Injectable() export class Messaging { private debug: Debugger = debug(this.constructor.name) constructor( private readonly patientRepository: PatientRepository, private readonly appointmentRepository: AppointmentRepository, private readonly messageRepository: MessageRepository, private readonly messagingService: MessagingService, private readonly translator: TranslatePatientsHelper, ) {} async incomingAppointmentsChatNofitication(): Promise { try { const baseDate = startOfMinute(new Date()) const appointments = await Promise.all([ this.getAppointments(addDays(baseDate, 1)), this.getAppointments(addHours(baseDate, 1)), ]) const [appointmentsInNext24Hours, appointmentsInNextHour] = appointments const messages = [ ...appointmentsInNext24Hours.map(e => { return this.buildMessage({ userId: e.user_id, patientId: e.patient_id, tag: '24_HOUR_REMAINING', text: this.getAppointmentMessageText({ appointment: e, translationKey: this.translator.keys.INCOMING_APPOINTMENT_24_HOUR, }), }) }), ...appointmentsInNextHour.map(e => { return this.buildMessage({ userId: e.user_id, patientId: e.patient_id, tag: '1_HOUR_REMAINING', text: this.getAppointmentMessageText({ appointment: e, translationKey: this.translator.keys.INCOMING_APPOINTMENT_1_HOUR, }), }) }), ] await chunkedEach(messages, 10, async message => { await this.messageRepository.create(message) }) } catch (error) { this.debug(error.message) } } async nutriReplyNotification(patientId: string): Promise { try { const patient = await this.getPatient(patientId) await this.sendNotification({ patient, title: this.translator.keys.NEW_MESSAGE, body: this.translator.keys.NUTRI_REPLIED, page: '/chat', }) } catch (error) { this.debug(error.message) } } async mealRatedNotification(patientId: string): Promise { try { const patient = await this.getPatient(patientId) await this.sendNotification({ patient, title: this.translator.keys.MEAL_RATED, body: this.translator.keys.MEAL_MESSAGE, page: '/home/diary', }) } catch (error) { this.debug(error.message) } } async foodPlanReadyNotification(patientId: string): Promise { try { const patient = await this.getPatient(patientId) const message = this.buildMessage({ userId: patient.user_id, patientId: patient.id, tag: 'FOOD_PLAN_ACTIVATED', text: this.getTranslation(patient, this.translator.keys.FOOD_PLAN_ACTIVATED), }) await this.messageRepository.create(message) } catch (error) { this.debug(error.message) } } private async sendNotification({ patient, title, body, page, }: { patient: Patient title: TranslateKey body: TranslateKey page: string }): Promise { await this.messagingService.sendPushNotification( patient.device_id, new PushPayload({ notification: { title: this.getTranslation(patient, title), body: this.getTranslation(patient, body), }, data: { click_action: 'FLUTTER_NOTIFICATION_CLICK', page, }, }), ) } private async getPatient(patientId: string): Promise { const patient = await this.patientRepository.get(patientId) if (!patient) throw new UnprocessableEntityError(`Patient ${patientId} not found.`) if (!patient.device_id) throw new UnprocessableEntityError("Patient's device id not found.") return patient } private getLanguageCode(patient: Patient): LanguageCountryCode { return (patient.country_code as LanguageCountryCode) || LanguageCountryCode.PT_BR } private getTranslation( patient: Patient, translation: TranslateKey, params?: TranslateParam, ): string { return this.translator.get(this.getLanguageCode(patient), translation, { params }) } private getAppointments(date: Date): Promise> { return this.appointmentRepository.findThatStartsAtHour({ date: date, windowInMinutes: NOTIFY_SCHEDULED_APPOINTMENT_WINDOW, state: ['scheduled'], }) } private buildMessage({ userId, patientId, tag, text, }: { userId: string patientId: string tag: MESSAGE_TAGS text: string }): Message { return new Message({ user_id: userId, replied_by_user_id: userId, patient_id: patientId, tags: { tags: [tag] }, text: text, }) } private getAppointmentMessageText({ appointment, translationKey, }: { appointment: Appointment translationKey: TranslateKey }) { const offsetInMinutes = parseTimeToMinutes(appointment.timezone) const dateWithOffset = addMinutes(appointment.scheduled_to, offsetInMinutes) return this.getTranslation(appointment.patient, translationKey, { name: appointment.patient.firstName, time: `${dateWithOffset.getHours()}hr ${dateWithOffset.getMinutes()}min`, }) } }