import { TestingModule, Test } from '@nestjs/testing' import { addMinutes, format, parseISO } from 'date-fns' import { datatype } from 'faker' import { cloneDeep } from 'lodash' import { Appointment, AppointmentCancelReason, AvailabilityTime, Patient, User } from '../../models' import { AppointmentRepository, AppointmentCancelReasonRepository, AvailabilityTimeRepository, AVAILABLE_TIMES_SIMILAR_USERS, MessageRepository, PatientRepository, UserRepository, } from '../../repositories' import { factory } from '../../../test/factories' import { MockedModule } from '../../../test/types' import { parseTimeToMinutes } from '../../utils' import { TransferService } from './transfer.service' describe('TransferService', () => { let subject: TransferService let appointmentRepository: MockedModule let appointmentCancelReasonRepository: MockedModule let availabilityTimeRepository: MockedModule let messageRepository: MockedModule let patientRepository: MockedModule let userRepository: MockedModule beforeAll(async () => { appointmentRepository = { save: jest.fn(), } appointmentCancelReasonRepository = { get: jest.fn(), } availabilityTimeRepository = { save: jest.fn(), get: jest.fn(), getByAppointmentId: jest.fn(), update: jest.fn(), } messageRepository = { transferPatientPendingMessages: jest.fn(), } patientRepository = { save: jest.fn(), get: jest.fn(), } userRepository = { findUsersWithAvailableDateByGoalAndRestrictions: jest.fn(), get: jest.fn(), } const app: TestingModule = await Test.createTestingModule({ controllers: [], providers: [ { provide: AppointmentRepository, useValue: appointmentRepository, }, { provide: AppointmentCancelReasonRepository, useValue: appointmentCancelReasonRepository, }, { provide: AvailabilityTimeRepository, useValue: availabilityTimeRepository, }, { provide: MessageRepository, useValue: messageRepository, }, { provide: PatientRepository, useValue: patientRepository, }, { provide: UserRepository, useValue: userRepository, }, TransferService, ], }).compile() subject = app.get(TransferService) }) afterEach(() => { jest.resetAllMocks() }) afterAll(() => { jest.restoreAllMocks() }) describe('#transfer', () => { const date = new Date() const similarUsers: AVAILABLE_TIMES_SIMILAR_USERS[] = [] let appointment: Appointment let availabilityTime: AvailabilityTime let appointmentCancelReason: AppointmentCancelReason let getSimilarUsersWithAvailableDateSpy: jest.SpyInstance let patient: Patient let user: User beforeAll(async () => { patient = await factory.build('patient') user = await factory.build('user') appointment = await factory.build('appointment', { patient_id: patient.id, user_id: user.id }) availabilityTime = await factory.build('availability_time', { status: 'scheduled', appointment_id: appointment.id, }) appointmentCancelReason = await factory.build('appointment_cancel_reason', { block_when_transfer: true }) }) beforeEach(() => { patientRepository.get.mockResolvedValue(patient) userRepository.get.mockResolvedValue(user) userRepository.findUsersWithAvailableDateByGoalAndRestrictions.mockResolvedValue(cloneDeep(similarUsers)) availabilityTimeRepository.getByAppointmentId.mockResolvedValue(availabilityTime) appointmentCancelReasonRepository.get.mockResolvedValue(appointmentCancelReason) }) afterEach(() => { subject['similarNutritionistsByDate'] = {} }) describe('when appointment has not timezone offset', () => { beforeAll(() => { appointment.timezone = null }) beforeEach(() => { getSimilarUsersWithAvailableDateSpy = jest.spyOn(subject, 'getSimilarUsersWithAvailableDate') }) afterAll(() => { getSimilarUsersWithAvailableDateSpy.mockRestore() }) it('uses availability timezone offset', async () => { await subject.transfer(appointment) expect(getSimilarUsersWithAvailableDateSpy).toBeCalledWith( appointment.user_id, appointment.patient_id, addMinutes(appointment.scheduled_to, parseTimeToMinutes(availabilityTime.offset)), ) }) }) describe('when appointment has timezone offset', () => { beforeAll(() => { appointment.timezone = '-10' }) beforeEach(() => { getSimilarUsersWithAvailableDateSpy = jest.spyOn(subject, 'getSimilarUsersWithAvailableDate') }) afterAll(() => { getSimilarUsersWithAvailableDateSpy.mockRestore() }) it('uses appointment timezone offset', async () => { await subject.transfer(appointment) expect(getSimilarUsersWithAvailableDateSpy).toBeCalledWith( appointment.user_id, appointment.patient_id, addMinutes(appointment.scheduled_to, parseTimeToMinutes(appointment.timezone)), ) }) }) describe('when does not have similar users', () => { it('blocks appointment that have not same date and time', async () => { await subject.transfer(appointment) expect(appointmentRepository.save).toBeCalledWith( expect.objectContaining({ id: appointment.id, state: 'blocked' }), expect.arrayContaining(['state']), ) }) describe('when cancel reasons is not given', () => { it('free old availability time', async () => { await subject.transfer(appointment) expect(availabilityTimeRepository.update).toBeCalledWith( expect.objectContaining({ id: availabilityTime.id, status: 'available' }), ) }) }) describe('when cancel reasons is given', () => { it('block old availability time', async () => { await subject.transfer(appointment, { value: appointmentCancelReason.value }) expect(availabilityTimeRepository.update).toBeCalledWith( expect.objectContaining({ id: availabilityTime.id, status: 'blocked' }), ) }) }) }) describe('when has similar users', () => { let freeAvailabilityTime: AvailabilityTime beforeAll(async () => { similarUsers.push({ id: datatype.uuid(), available_times: [ AvailabilityTime.toInstance({ ...(await factory.build('availability_time', { time: '09:00:00-03', })), date: new Date(), }) as any, ], }) similarUsers.push({ id: datatype.uuid(), available_times: [ AvailabilityTime.toInstance({ ...(await factory.build('availability_time', { time: '10:00:00-03', })), date, }) as any, AvailabilityTime.toInstance({ ...(await factory.build('availability_time', { time: '11:00:00-03', })), date, }) as any, ], }) }) beforeEach(async () => { freeAvailabilityTime = await factory.build('availability_time', { id: similarUsers[1].available_times[0].id, status: 'available', appointment_id: null, }) availabilityTimeRepository.get.mockResolvedValue(freeAvailabilityTime) }) describe('when has the same date time', () => { beforeAll(() => { appointment.scheduled_to = parseISO(`${format(date, 'yyyy-MM-dd')}T13:00:00+00:00`) }) it('block old availability time', async () => { await subject.transfer(appointment, { value: appointmentCancelReason.value }) expect(availabilityTimeRepository.update).toBeCalledWith( expect.objectContaining({ id: availabilityTime.id, status: 'blocked' }), ) }) it('schedule new availability time', async () => { await subject.transfer(appointment, { value: appointmentCancelReason.value }) expect(availabilityTimeRepository.update).toBeCalledWith( expect.objectContaining({ id: freeAvailabilityTime.id, status: 'scheduled', appointment_id: appointment.id, }), ) }) it('just change appointment user', async () => { await subject.transfer(appointment, { value: appointmentCancelReason.value }) expect(appointmentRepository.save).toBeCalledWith( expect.objectContaining({ id: appointment.id, user_id: similarUsers[1].id, }), expect.arrayContaining([{ field: 'user_id', type: 'String' }]), ) }) it("changes patient's user", async () => { await subject.transfer(appointment, { value: appointmentCancelReason.value }) expect(patientRepository.save).toBeCalledWith( expect.objectContaining({ id: appointment.patient_id, user_id: similarUsers[1].id }), expect.arrayContaining([{ field: 'user_id', type: 'String' }]), ) }) it('redirect pending messages', async () => { await subject.transfer(appointment, { value: appointmentCancelReason.value }) expect(messageRepository.transferPatientPendingMessages).toBeCalledWith( appointment.patient_id, similarUsers[1].id, ) }) }) describe('when has not the same date time', () => { beforeAll(() => { appointment.scheduled_to = parseISO(`${format(date, 'yyyy-MM-dd')}T07:00`) }) it('blocks appointment that have not same date and time', async () => { await subject.transfer(appointment, { value: appointmentCancelReason.value }) expect(appointmentRepository.save).toBeCalledWith( expect.objectContaining({ id: appointment.id, state: 'blocked' }), expect.arrayContaining(['state']), ) }) it('block old availability time', async () => { await subject.transfer(appointment, { value: appointmentCancelReason.value }) expect(availabilityTimeRepository.update).toBeCalledWith( expect.objectContaining({ id: availabilityTime.id, status: 'blocked' }), ) }) }) }) describe('when cancellation reason is given', () => { describe('when cancellation reason block would block the availability time', () => { beforeAll(() => { appointmentCancelReason.block_when_transfer = true }) it('block availability time', async () => { await subject.transfer(appointment, { value: appointmentCancelReason.value }) expect(availabilityTimeRepository.update).toBeCalledWith( expect.objectContaining({ id: availabilityTime.id, status: 'blocked' }), ) }) }) describe('when cancellation reason block would not block the availability time', () => { beforeAll(() => { appointmentCancelReason.block_when_transfer = false }) it('free availability time', async () => { await subject.transfer(appointment, { value: appointmentCancelReason.value }) expect(availabilityTimeRepository.update).toBeCalledWith( expect.objectContaining({ id: availabilityTime.id, status: 'available' }), ) }) }) }) }) })