import { Test, TestingModule } from '@nestjs/testing' import { addDays, format, isSunday, set, startOfWeek } from 'date-fns' import { datatype } from 'faker' import { sample } from 'lodash' import MockDate from 'mockdate' import { Appointment, AvailabilityTime, NotificationType, Roles, User } from '../../../models' import { AppointmentRepository, AvailabilityTimeRepository, ConfigRepository, MessageRepository, NotificationRepository, UserRepository, } from '../../../repositories' import { factory } from '../../../../test/factories' import { CountryCode, CountryHelper, LanguageCountryCode } from '../../../utils/countries' import { MockedModule } from '../../../../test/types' import { parseWrittenDate } from '../../../utils/date/parse-written-date' import { UserNotFoundError } from '../../../errors/user_not_found.error' import { ReleaseNote } from '../../../models/release_note.model' import { TranslateNotificationsHelper } from '../i18n' import { MailService, PushNotificationTemplate } from '../../../services' import { UserStatusUpdate } from '../models' import { Nutritionists } from './nutritionists' describe('Nutritionists', () => { let subject: Nutritionists let messageRepository: MockedModule let notificationRepository: MockedModule let userRepository: MockedModule let appointmentRepository: MockedModule let availabilityTimeRepository: MockedModule let mailService: MockedModule let configRepository: MockedModule const translateNotificationsHelper = new TranslateNotificationsHelper() const getMessageAndTitle = (countryCode: CountryCode, template: string): PushNotificationTemplate => translateNotificationsHelper.get(countryCode, `pushes.${template}` as any) const getEmailTemplate = (countryCode: CountryCode, template: string): string => translateNotificationsHelper.get(countryCode, `email_templates.${template}` as any) beforeAll(async () => { messageRepository = { getUserPatientPendingMessagesCount: jest.fn(), } notificationRepository = { findUnreadsByTypeAndUserAndPatient: jest.fn(), delete: jest.fn(), create: jest.fn(), } userRepository = { get: jest.fn(), findAll: jest.fn(), findByRoleAndCountryCode: jest.fn(), } appointmentRepository = { findThatStartsAtHour: jest.fn(), } availabilityTimeRepository = { getByAppointmentId: jest.fn(), } mailService = { send: jest.fn(), } configRepository = { get: jest.fn(), } const app: TestingModule = await Test.createTestingModule({ controllers: [], providers: [ { provide: MailService, useValue: mailService, }, { provide: AppointmentRepository, useValue: appointmentRepository, }, { provide: AvailabilityTimeRepository, useValue: availabilityTimeRepository, }, { provide: MessageRepository, useValue: messageRepository, }, { provide: NotificationRepository, useValue: notificationRepository, }, { provide: UserRepository, useValue: userRepository, }, { provide: TranslateNotificationsHelper, useValue: translateNotificationsHelper, }, { provide: ConfigRepository, useValue: configRepository, }, Nutritionists, ], }).compile() subject = app.get(Nutritionists) }) afterEach(() => { jest.resetAllMocks() }) describe('#notifyNewRelease', () => { let releaseNote: ReleaseNote let brUser: User let mxUser: User beforeAll(async () => { brUser = await factory.build('user:br') mxUser = await factory.build('user:mx') }) describe('when release is only in pt_BR', () => { beforeEach(async () => { releaseNote = await factory.build('release_note') userRepository.findAll.mockReturnValue([brUser]) await subject.notifyNewRelease(releaseNote) }) it('searches for br users only', async () => { expect(userRepository.findAll).toBeCalledWith(true, [LanguageCountryCode.PT_BR]) }) it('creates a notification', async () => { expect(notificationRepository.create).toBeCalledWith( expect.objectContaining({ ...getMessageAndTitle(brUser.country_code, 'new_release_note'), type: NotificationType.RELEASE, user_id: brUser.id, resource_id: releaseNote.id, }), brUser.country_code, ) }) }) describe('when release is also in es_MX', () => { beforeEach(async () => { releaseNote = await factory.build('release_note:es') userRepository.findAll.mockReturnValue([brUser, mxUser]) await subject.notifyNewRelease(releaseNote) }) it('searches for br and mx users', async () => { expect(userRepository.findAll).toBeCalledWith(true, [LanguageCountryCode.PT_BR, LanguageCountryCode.ES_MX]) }) it('creates a notification for both users', async () => { expect(notificationRepository.create).toBeCalledTimes(2) expect(notificationRepository.create).toBeCalledWith( expect.objectContaining({ ...getMessageAndTitle(brUser.country_code, 'new_release_note'), type: NotificationType.RELEASE, user_id: brUser.id, resource_id: releaseNote.id, }), brUser.country_code, ) expect(notificationRepository.create).toBeCalledWith( expect.objectContaining({ ...getMessageAndTitle(mxUser.country_code, 'new_release_note'), type: NotificationType.RELEASE, user_id: mxUser.id, resource_id: releaseNote.id, }), mxUser.country_code, ) }) }) }) describe('#notifyNewMessage', () => { let nutritionist: User let patientId: string beforeAll(async () => { nutritionist = await factory.build('user') patientId = datatype.uuid() }) beforeEach(() => { notificationRepository.findUnreadsByTypeAndUserAndPatient.mockResolvedValue([]) }) describe('when nutritionist does not exist', () => { it('throws an error', async () => { await expect(subject.notifyNewMessage(nutritionist.id, patientId)).rejects.toThrowError(UserNotFoundError) }) }) describe('when nutritionist exists', () => { beforeEach(() => { userRepository.get.mockReturnValue(nutritionist) }) describe('when has not pending messages', () => { beforeEach(() => { messageRepository.getUserPatientPendingMessagesCount.mockResolvedValue(0) }) it('does not create a notification', async () => { await subject.notifyNewMessage(nutritionist.id, patientId) expect(notificationRepository.create).not.toBeCalled() }) }) describe('when has pending messages', () => { beforeEach(() => { messageRepository.getUserPatientPendingMessagesCount.mockResolvedValue(datatype.number({ min: 1, max: 10 })) }) it('creates a notification', async () => { await subject.notifyNewMessage(nutritionist.id, patientId) expect(notificationRepository.create).toBeCalledWith( expect.objectContaining({ ...getMessageAndTitle(nutritionist.country_code, 'new_messages_nutri'), title: expect.any(String), type: NotificationType.MESSAGE, user_id: nutritionist.id, patient_id: patientId, }), nutritionist.country_code, ) }) describe('when has same type unread notifications', () => { let unreadNotifications: any[] beforeAll(() => { unreadNotifications = [...Array(1)].map(() => ({ id: datatype.uuid() })) }) beforeEach(() => { messageRepository.getUserPatientPendingMessagesCount.mockResolvedValue({ min: 1, max: 10 }) notificationRepository.findUnreadsByTypeAndUserAndPatient.mockResolvedValue(unreadNotifications) }) it('deletes all messages', async () => { await subject.notifyNewMessage(nutritionist.id, patientId) expect(notificationRepository.delete).toBeCalledTimes(unreadNotifications.length) }) }) }) }) }) describe('#notifyNewSignUp', () => { let admin: User let newUser: User beforeEach(() => { userRepository.findByRoleAndCountryCode.mockReturnValue([admin]) configRepository.get.mockReturnValue('') }) describe('when user is brazilian', () => { beforeAll(async () => { admin = await factory.build('user:br', { role: Roles.NUTRITIONIST_MANAGER }) newUser = await factory.build('user:br') }) it('notifies by notification and email', async () => { await subject.notifyNewSignUp(newUser) expect(notificationRepository.create).toBeCalledWith( expect.objectContaining({ title: getMessageAndTitle(admin.country_code, 'new_sign_up').title, message: getMessageAndTitle(admin.country_code, 'new_sign_up').message, user_id: admin.id, resource_id: newUser.id, type: NotificationType.SIGN_UP, }), admin.country_code, ) expect(mailService.send).toBeCalledWith( expect.objectContaining({ to: admin.email, templateId: getEmailTemplate(admin.country_code, 'new_sign_up'), dynamicTemplateData: { web_link: `/users-management`, }, }), ) }) }) describe('when admin is not brazilian', () => { beforeAll(async () => { admin = await factory.build('user:mx', { role: Roles.NUTRITIONIST_MANAGER }) newUser = await factory.build('user:mx') }) it('notifies by notification only', async () => { await subject.notifyNewSignUp(newUser) expect(mailService.send).toBeCalledTimes(0) }) }) }) describe('#notifyStatusUpdate', () => { let user: User let status: UserStatusUpdate beforeEach(() => { configRepository.get.mockReturnValue('') status = sample(Object.values(UserStatusUpdate)) }) describe('when user is brazilian', () => { beforeAll(async () => { user = await factory.build('user:br') }) it('notifies by email with correct template', async () => { await subject.notifyStatusUpdate(user, status) expect(mailService.send).toBeCalledWith( expect.objectContaining({ to: user.email, templateId: getEmailTemplate(user.country_code, status), dynamicTemplateData: { web_link: '', first_name: user.firstName, disapproval_reason: user.reproved_reason, }, }), ) }) }) describe('when user is not brazilian', () => { beforeAll(async () => { user = await factory.build('user:mx') }) it('does nothing', async () => { await subject.notifyStatusUpdate(user, status) expect(mailService.send).toBeCalledTimes(0) }) }) }) describe('#notifyAppointmentScheduled', () => { let appointment: Appointment let nutritionist: User beforeAll(async () => { appointment = await factory.build('appointment') nutritionist = await factory.build('user') }) describe('when nutritionist does not exist', () => { it('throws an error', async () => { await expect(subject.notifyAppointmentScheduled(appointment)).rejects.toThrowError(UserNotFoundError) }) }) describe('when nutritionist exists', () => { let availabilityTime: AvailabilityTime beforeAll(async () => { availabilityTime = await factory.build('availability_time', { appointment_id: appointment.id }) }) beforeEach(() => { userRepository.get.mockReturnValue(nutritionist) availabilityTimeRepository.getByAppointmentId.mockResolvedValue(availabilityTime) }) it('creates a notification', async () => { await subject.notifyAppointmentScheduled(appointment) expect(notificationRepository.create).toBeCalledWith( expect.objectContaining({ message: expect.any(String), title: expect.any(String), type: NotificationType.APPOINTMENT, user_id: nutritionist.id, patient_id: appointment.patient_id, resource_id: appointment.id, }), nutritionist.country_code, ) }) }) describe('when appointment state is different to scheduled', () => { beforeAll(() => { appointment.state = 'blocked' }) it('does not create a notification', async () => { await subject.notifyAppointmentScheduled(appointment) expect(notificationRepository.create).not.toBeCalled() }) }) }) describe('#notifyAppointmentCancelled', () => { let appointment: Appointment let nutritionist: User beforeAll(async () => { appointment = await factory.build('appointment:cancelled') nutritionist = await factory.build('user') }) describe('when nutritionist does not exist', () => { it('throws an error', async () => { await expect(subject.notifyAppointmentCancelled(appointment)).rejects.toThrowError(UserNotFoundError) }) }) describe('when nutritionist exists', () => { let scheduledTo: Date let message: string beforeEach(() => { scheduledTo = set( new Date(appointment.scheduled_to), subject['extractHoursAndMinutesFromTime']( format(subject['getDateWithOffset'](nutritionist.country_code, appointment.scheduled_to), 'H:mm'), ), ) message = parseWrittenDate(scheduledTo, CountryHelper.parseCode(nutritionist.country_code)) userRepository.get.mockReturnValue(nutritionist) availabilityTimeRepository.getByAppointmentId.mockResolvedValue(null) }) it('creates a notification', async () => { await subject.notifyAppointmentCancelled(appointment) expect(notificationRepository.create).toBeCalledWith( expect.objectContaining({ title: expect.any(String), message: expect.stringContaining(message), type: NotificationType.APPOINTMENT, user_id: nutritionist.id, patient_id: appointment.patient_id, resource_id: appointment.id, }), nutritionist.country_code, ) }) }) }) describe('#notifyAppointmentsLeftToStart', () => { describe('when has not appointments', () => { beforeEach(() => { appointmentRepository.findThatStartsAtHour.mockResolvedValue([]) }) it('does not create any notification', async () => { await subject.notifyAppointmentsLeftToStart() expect(notificationRepository.create).not.toBeCalled() }) }) describe('when has appointments', () => { let appointments: Appointment[] let availabilityTime: AvailabilityTime beforeAll(async () => { appointments = await factory.buildMany('appointment', datatype.number({ min: 1, max: 10 })) availabilityTime = await factory.build('availability_time') appointments.map( appointment => (appointment['user'] = { id: appointment.user_id, country_code: sample(Object.values(LanguageCountryCode)), }), ) }) beforeEach(() => { appointmentRepository.findThatStartsAtHour.mockResolvedValue(appointments) availabilityTimeRepository.getByAppointmentId.mockResolvedValue(availabilityTime) }) it('creates a notification', async () => { await subject.notifyAppointmentsLeftToStart() expect(notificationRepository.create).toBeCalledTimes(appointments.length) }) }) }) describe('#notifyPendingFoodPlanWithOneDayLeft', () => { beforeAll(() => { MockDate.set(startOfWeek(new Date(), { weekStartsOn: 1 })) }) afterAll(() => { MockDate.reset() }) describe('when has not appointments', () => { beforeEach(() => { appointmentRepository.findThatStartsAtHour.mockResolvedValue([]) }) it('does not create any notification', async () => { await subject.notifyPendingFoodPlanWithOneDayLeft() expect(notificationRepository.create).not.toBeCalled() }) }) describe('when has appointments', () => { let appointments: Appointment[] let availabilityTime: AvailabilityTime beforeAll(async () => { appointments = await factory.buildMany('appointment', datatype.number({ min: 1, max: 10 })) availabilityTime = await factory.build('availability_time') appointments = appointments.map(appointment => { appointment['user'] = { id: appointment.user_id, country_code: sample(Object.values(LanguageCountryCode)), } return appointment }) }) beforeEach(() => { appointmentRepository.findThatStartsAtHour.mockResolvedValue(appointments) availabilityTimeRepository.getByAppointmentId.mockResolvedValue(availabilityTime) }) it('creates a notification', async () => { await subject.notifyPendingFoodPlanWithOneDayLeft() expect(notificationRepository.create).toBeCalledTimes(appointments.length) }) }) describe.each` day | subDays ${'2021-07-18'} | ${0} ${'2021-07-19'} | ${3} ${'2021-07-20'} | ${3} ${'2021-07-21'} | ${2} ${'2021-07-22'} | ${2} ${'2021-07-23'} | ${2} ${'2021-07-24'} | ${2} `('when find appointments from "$day"', ({ day, subDays }) => { const baseDate = new Date(`${day}T00:00:00`) let businessDay: Date beforeAll(() => { businessDay = addDays(baseDate, -subDays) }) beforeEach(() => { MockDate.set(baseDate) }) if (isSunday(baseDate)) { it('does not execute the service', async () => { await subject.notifyPendingFoodPlanWithOneDayLeft() expect(appointmentRepository.findThatStartsAtHour).not.toBeCalled() }) return } it('find appointments with from correct business day', async () => { await subject.notifyPendingFoodPlanWithOneDayLeft() expect(appointmentRepository.findThatStartsAtHour).toBeCalledWith( expect.objectContaining({ date: businessDay }), ) }) }) }) describe('#notifyAppointmentTransferred', () => { let appointment: Appointment let oldNutritionist: User let spyNotifyAppointmentScheduled: jest.SpyInstance beforeAll(async () => { appointment = await factory.build('appointment') oldNutritionist = await factory.build('user') }) describe('when old nutritionist does not exist', () => { it('throws an error', async () => { await expect(subject.notifyAppointmentTransferred(appointment, oldNutritionist.id)).rejects.toThrowError( UserNotFoundError, ) }) }) describe('when old nutritionist exists', () => { beforeEach(() => { userRepository.get.mockReturnValue(oldNutritionist) spyNotifyAppointmentScheduled = jest.spyOn(subject, 'notifyAppointmentScheduled').mockResolvedValue() }) it('notify the new nutritionist', async () => { await subject.notifyAppointmentTransferred(appointment, oldNutritionist.id) expect(spyNotifyAppointmentScheduled).toBeCalledWith(appointment) }) it('creates a transfer notification to the old nutrionist', async () => { await subject.notifyAppointmentTransferred(appointment, oldNutritionist.id) expect(notificationRepository.create).toBeCalledWith( expect.objectContaining({ ...getMessageAndTitle(oldNutritionist.country_code, 'appointment_transferred_nutri'), message: expect.any(String), type: NotificationType.APPOINTMENT, user_id: oldNutritionist.id, patient_id: appointment.patient_id, resource_id: appointment.id, }), oldNutritionist.country_code, ) }) }) }) })