import { TestingModule, Test } from '@nestjs/testing' import { addHours, addMinutes, format, parseISO, subMinutes } from 'date-fns' import { datatype } from 'faker' import MockDate from 'mockdate' import { BusinessExceptionError, NotFoundError, UnprocessableEntityError } from '../../../errors' import { Appointment, AppointmentCancellationReasons, AppointmentCancelReason, AppointmentState, Availability, AvailabilityTime, Patient, TeleconsultationsCredit, User, } from '../../../models' import { MockedModule } from '../../../../test/types' import { AppointmentRepository, AppointmentCancelReasonRepository, AvailabilityRepository, AvailabilityTimeRepository, PatientRepository, UserRepository, } from '../../../repositories' import { factory } from '../../../../test/factories' import { BookingConditions } from '../types/booking-conditions.type' import { TeleconsultationsCreditService, TransferService } from '../../../services' import { APPOINTMENT_STATE_CARRIED_OUT, Booking, LIMIT_MINUTES_TO_TRANSFER } from './booking' describe('Booking', () => { let subject: Booking let availabilityTimeRepository: MockedModule let availabilityRepository: MockedModule let patientRepository: MockedModule let appointmentRepository: MockedModule let teleconsultationsCreditService: MockedModule let appointmentCancelReasonRepository: MockedModule let userRepository: MockedModule let transferService: MockedModule let nextValidCreditToUse: Partial beforeAll(async () => { availabilityTimeRepository = { update: jest.fn(), updateWhenStatusEqual: jest.fn(), get: jest.fn(), getByAppointmentId: jest.fn(), } availabilityRepository = { getByUserAndDate: jest.fn(), createWithNestedAvailabilityTimes: jest.fn(), get: jest.fn(), } teleconsultationsCreditService = { getBalance: jest.fn(), createTransaction: jest.fn(), getSeparateCredits: jest.fn(), getNextValidCreditToUse: jest.fn(), getByAppointmentId: jest.fn(), save: jest.fn(), } patientRepository = { get: jest.fn(), updateUser: jest.fn(), create: jest.fn(), findByDocumentNumberAndTenant: jest.fn(), } appointmentRepository = { get: jest.fn(), create: jest.fn(), update: jest.fn(), reallocate: jest.fn(), hasScheduledAppointment: jest.fn(), getLastAppointment: jest.fn(), delete: jest.fn(), getCountUsedAppointments: jest.fn(), findAppointmentsWithNutriNoArrived: jest.fn(), getLastValidUsedAppointment: jest.fn(), getScheduledAppointmentsWithCancelledSubscription: jest.fn(), findScheduledAppointmentsByPatient: jest.fn(), } appointmentCancelReasonRepository = { get: jest.fn(), } userRepository = { getCountryCode: jest.fn(), } transferService = { transfer: jest.fn(), } const app: TestingModule = await Test.createTestingModule({ controllers: [], providers: [ { provide: TeleconsultationsCreditService, useValue: teleconsultationsCreditService, }, { provide: AvailabilityTimeRepository, useValue: availabilityTimeRepository, }, { provide: AvailabilityRepository, useValue: availabilityRepository, }, { provide: PatientRepository, useValue: patientRepository, }, { provide: AppointmentRepository, useValue: appointmentRepository, }, { provide: AppointmentCancelReasonRepository, useValue: appointmentCancelReasonRepository, }, { provide: UserRepository, useValue: userRepository, }, { provide: TransferService, useValue: transferService, }, Booking, ], }).compile() subject = app.get(Booking) }) afterEach(() => { jest.clearAllMocks() }) afterAll(() => { jest.restoreAllMocks() }) describe('#book', () => { const patientId = datatype.uuid() const availabilityTimeId = datatype.uuid() const now = new Date() let patient: Patient let availabilityTime: AvailabilityTime let availability: Availability let userCountryCode: string const credits = { teleconsultation: 0, } beforeAll(() => { MockDate.set(now) }) beforeEach(() => { if (patient) patient.id = patientId patientRepository.get.mockResolvedValue(patient) availabilityTimeRepository.get.mockResolvedValue(availabilityTime) availabilityRepository.get.mockResolvedValue(availability) appointmentRepository.getLastAppointment.mockResolvedValue(null) appointmentRepository.getCountUsedAppointments.mockResolvedValue(0) userRepository.getCountryCode.mockResolvedValue(userCountryCode) teleconsultationsCreditService.getSeparateCredits.mockResolvedValue(credits) }) afterAll(() => { MockDate.reset() }) describe('when patient not exists', () => { beforeAll(() => { patient = null }) it('throw an error', async () => { await expect(subject.book(patientId, availabilityTimeId)).rejects.toThrowError(NotFoundError) }) }) describe('when patient exists', () => { beforeAll(async () => { patient = await factory.build('patient', { id: patientId }) }) describe('when patient does not have teleconsultation credit', () => { beforeAll(async () => { patient = await factory.build('patient', { id: patientId, collaborator: false, internal_collaborator: false }) credits.teleconsultation = 0 }) it('throws an error', async () => { await expect(subject.book(patientId, availabilityTimeId)).rejects.toThrowError(BusinessExceptionError) }) }) describe('when patient has teleconsultation credit', () => { beforeAll(() => { credits.teleconsultation = 1 }) describe('and availability_time not exists', () => { beforeAll(() => { availabilityTime = null }) it('throw an error', async () => { await expect(subject.book(patientId, availabilityTimeId)).rejects.toThrowError(NotFoundError) }) }) describe('and availability_time exists', () => { describe('and availability_time is blocked', () => { beforeAll(async () => { availabilityTime = await factory.build('availability_time', { status: 'blocked' }) }) it('throw an error', async () => { await expect(subject.book(patientId, availabilityTimeId)).rejects.toThrowError(BusinessExceptionError) }) }) describe('and availability_time is scheduled', () => { beforeAll(async () => { availabilityTime = await factory.build('availability_time', { status: 'scheduled' }) }) it('throw an error', async () => { await expect(subject.book(patientId, availabilityTimeId)).rejects.toThrowError(BusinessExceptionError) }) }) describe('and availability_time is available', () => { beforeEach(async () => { availabilityTime = await factory.build('availability_time', { status: 'available' }) availabilityTimeRepository.get.mockResolvedValue(availabilityTime) }) describe('and availability not exists', () => { beforeAll(() => { availability = null }) it('throw an error', async () => { await expect(subject.book(patientId, availabilityTimeId)).rejects.toThrowError(NotFoundError) }) }) describe('and availability exists', () => { beforeAll(async () => { availability = await factory.build('availability') }) describe(`and availability time it's in the past`, () => { beforeEach(async () => { availabilityTime = await factory.build('availability_time', { status: 'available', time: format(addHours(now, -12), 'HH:mm'), }) availabilityTimeRepository.get.mockResolvedValue(availabilityTime) }) beforeAll(() => { availability.date = addHours(now, -12) }) afterAll(async () => { availability = await factory.build('availability') }) it('throw an error', async () => { await expect(subject.book(patientId, availabilityTimeId)).rejects.toThrowError(BusinessExceptionError) }) }) describe('and user does not exist', () => { beforeAll(async () => { userCountryCode = null }) it('throw an error', async () => { await expect(subject.book(patientId, availabilityTimeId)).rejects.toThrowError(NotFoundError) }) }) describe('and user exists', () => { const appointmentId = datatype.uuid() beforeAll(async () => { appointmentRepository.create.mockResolvedValue(appointmentId) }) describe('when user is from a country different than the patient', () => { beforeAll(() => { patient.country_code = 'pt_BR' userCountryCode = 'es_MX' }) it('throw an error', async () => { await expect(subject.book(patientId, availabilityTimeId)).rejects.toThrowError( BusinessExceptionError, ) }) }) describe('when user and patient are from the same country', () => { beforeAll(() => { userCountryCode = patient.country_code }) describe('when patient already has scheduled appointment', () => { beforeEach(() => { appointmentRepository.hasScheduledAppointment.mockResolvedValue(true) }) it('throw an error', async () => { await expect(subject.book(patientId, availabilityTimeId)).rejects.toThrowError( UnprocessableEntityError, ) }) }) describe('when patient has no scheduled appointment', () => { beforeEach(() => { appointmentRepository.hasScheduledAppointment.mockResolvedValue(false) }) it('set the availability_time as scheduled', async () => { await subject.book(patientId, availabilityTimeId) availabilityTime.setScheduled() expect(availabilityTimeRepository.updateWhenStatusEqual).toHaveBeenCalledWith( availabilityTime, 'available', ) }) it('creates the appointment', async () => { await subject.book(patientId, availabilityTimeId) expect(appointmentRepository.create).toHaveBeenCalledWith( Appointment.build({ availability, availabilityTime, patient, }), ) }) describe('and creates appointment throws an error', () => { beforeAll(() => { const error = new Error() appointmentRepository.create.mockRejectedValue(error) }) afterAll(() => { appointmentRepository.create.mockResolvedValue(appointmentId) }) it('set availability_time as free', async () => { try { await subject.book(patientId, availabilityTimeId) } catch (error) { } finally { availabilityTime.free() expect(availabilityTimeRepository.update).toHaveBeenCalledWith(availabilityTime) } }) it('throws an error', async () => { await expect(subject.book(patientId, availabilityTimeId)).rejects.toThrowError() }) }) it('sets the apppointment id on availability_time', async () => { await subject.book(patientId, availabilityTimeId) availabilityTime.setScheduled(appointmentId) expect(availabilityTimeRepository.update).toHaveBeenCalledWith(availabilityTime) }) describe('and sets appointment id on availability_time throws an error', () => { beforeEach(() => { const error = new Error() availabilityTimeRepository.update.mockRejectedValueOnce(error) }) afterAll(() => { availabilityTimeRepository.update.mockResolvedValue(null) }) it('sets availability_time as free', async () => { try { await subject.book(patientId, availabilityTimeId) } catch (error) { } finally { availabilityTime.free() expect(availabilityTimeRepository.update).toHaveBeenNthCalledWith(2, availabilityTime) } }) it('deletes the appointment', async () => { try { await subject.book(patientId, availabilityTimeId) } catch (error) { } finally { expect(appointmentRepository.delete).toHaveBeenCalledWith(appointmentId) } }) it('throws an error', async () => { await expect(subject.book(patientId, availabilityTimeId)).rejects.toThrowError() }) }) it('sets the user_id on patient', async () => { await subject.book(patientId, availabilityTimeId) expect(patientRepository.updateUser).toHaveBeenCalledWith(patient, availability.user_id) }) it('returns the apppointment id', async () => { const response = await subject.book(patientId, availabilityTimeId) expect(response).toBeTruthy() }) describe('when appointmentIdRescheduled is gived', () => { it('create appointment with appointment_id_rescheduled', async () => { const appointmentIdRescheduled = datatype.uuid() await subject.book(patientId, availabilityTimeId, appointmentIdRescheduled) expect(appointmentRepository.create).toBeCalledWith( expect.objectContaining({ appointment_id_rescheduled: appointmentIdRescheduled }), ) }) }) describe('when patient is a collaborator', () => { beforeAll(async () => { patient = await factory.build('patient:collaborator', { id: patientId, internal_collaborator: false, }) subject['patientCached'][patientId] = patient nextValidCreditToUse = { id: datatype.uuid(), subscription_id: datatype.uuid(), } teleconsultationsCreditService.getNextValidCreditToUse.mockResolvedValue(nextValidCreditToUse) }) afterAll(() => { teleconsultationsCreditService.getNextValidCreditToUse.mockResolvedValue(null) }) it(`discount a teleconsultation credit`, async () => { await subject.book(patientId, availabilityTimeId) expect(teleconsultationsCreditService.createTransaction).toHaveBeenCalledWith({ patientId, reason: 'appointment_use', value: -1, creditId: nextValidCreditToUse.id, subscriptionId: nextValidCreditToUse.subscription_id, objectModel: expect.objectContaining({ id: appointmentId }), author: patient, }) }) }) describe('when patient is an internal collaborator', () => { beforeAll(async () => { patient = await factory.build('patient', { id: patientId, collaborator: false, internal_collaborator: true, }) subject['patientCached'][patientId] = patient }) it(`doesn't discount a teleconsultation credit`, async () => { await subject.book(patientId, availabilityTimeId) expect(teleconsultationsCreditService.createTransaction).not.toHaveBeenCalled() }) }) describe('when patient has subscription', () => { beforeAll(async () => { patient = await factory.build('patient:subscriber', { id: patientId }) subject['patientCached'][patientId] = patient nextValidCreditToUse = { id: datatype.uuid(), subscription_id: datatype.uuid(), } teleconsultationsCreditService.getNextValidCreditToUse.mockResolvedValue(nextValidCreditToUse) }) afterAll(() => { teleconsultationsCreditService.getNextValidCreditToUse.mockResolvedValue(null) }) it('discount a teleconsultation credit', async () => { await subject.book(patientId, availabilityTimeId) expect(teleconsultationsCreditService.createTransaction).toHaveBeenCalledWith({ patientId, reason: 'appointment_use', value: -1, objectModel: expect.objectContaining({ id: appointmentId }), author: patient, creditId: nextValidCreditToUse.id, subscriptionId: nextValidCreditToUse.subscription_id, }) }) }) describe('when appointment is scheduled for more than 48 hours', () => { beforeEach(() => { const date = addHours(new Date(), 72) availability.date = date availabilityTime.time = format(date, 'HH:mm') }) it('does not confirm the appointment', async () => { await subject.book(patientId, availabilityTimeId) expect(appointmentRepository.create).toBeCalledWith( expect.objectContaining({ confirmed_at: null }), ) }) }) describe('when appointment is scheduled for less than 48 hours', () => { beforeEach(() => { const date = addHours(new Date(), 36) availability.date = date availabilityTime.time = format(date, 'HH:mm') }) it('confirms the appointment', async () => { await subject.book(patientId, availabilityTimeId) expect(appointmentRepository.create).toBeCalledWith( expect.objectContaining({ confirmed_at: expect.any(Date) }), ) }) }) describe('when appointment is scheduled for equal to 48 hours', () => { beforeEach(() => { const date = addHours(new Date(), 48) availability.date = date availabilityTime.time = format(date, 'HH:mm') }) it('confirms the appointment', async () => { await subject.book(patientId, availabilityTimeId) expect(appointmentRepository.create).toBeCalledWith( expect.objectContaining({ confirmed_at: expect.any(Date) }), ) }) }) }) }) }) }) }) }) }) }) }) describe('#cancel', () => { const appointmentId = datatype.uuid() let appointment: Appointment let availabilityTime: AvailabilityTime let initialState: AppointmentState let requester: User | Patient let appointmentCancelReason: AppointmentCancelReason beforeAll(async () => { availabilityTime = await factory.build('availability_time') requester = await factory.build('user') }) beforeEach(async () => { appointment = await factory.build('appointment', { is_return: false }) initialState = appointment.state appointmentCancelReasonRepository.get.mockResolvedValue(appointmentCancelReason) }) describe('when appointment not exists', () => { beforeEach(() => { appointmentRepository.get.mockResolvedValue(null) }) it('throw an error', async () => { await expect(subject.cancel(appointmentId, requester)).rejects.toThrowError(NotFoundError) }) }) describe('when appointment exists', () => { beforeEach(() => { appointmentRepository.get.mockResolvedValue(appointment) }) describe('and availability time exists', () => { beforeEach(() => { availabilityTimeRepository.getByAppointmentId.mockResolvedValue(availabilityTime) appointmentRepository.update.mockResolvedValue(null) }) describe('and cancel reason is invalid', () => { beforeAll(() => { appointmentCancelReason = null }) it('throw an error', async () => { await expect(subject.cancel(appointmentId, requester)).rejects.toThrowError(NotFoundError) }) }) describe('and cancel reason is valid', () => { beforeAll(async () => { appointmentCancelReason = await factory.build('appointment_cancel_reason', { value: AppointmentCancellationReasons.PATIENT_CANCELLED, decrease_credit: true, }) }) const cancelFlow = () => { beforeAll(() => { appointmentCancelReason.decrease_credit = true }) it('cancels the appointment', async () => { await subject.cancel(appointmentId, requester, { value: appointmentCancelReason.value }) expect(appointmentRepository.update).toBeCalledWith( expect.objectContaining({ state: 'cancelled', canceled_at: expect.any(Date), cancellation_reason: appointmentCancelReason.value, }), ) }) describe('and appointment cancel throws error', () => { beforeEach(() => { const error = new Error() appointmentRepository.update.mockRejectedValue(error) }) it('not updated the availability time', async () => { try { await subject.cancel(appointmentId, requester) } catch (error) { } finally { expect(availabilityTimeRepository.update).not.toBeCalled() } }) }) describe('and availability time is free', () => { let lastAppointment: Appointment let patient: User beforeAll(async () => { patient = await factory.build('patient') lastAppointment = await factory.build('appointment', { is_return: false }) appointmentRepository.update.mockResolvedValue(null) availabilityTimeRepository.update.mockResolvedValue(null) }) beforeEach(() => { appointment.scheduled_to = addHours(new Date(), 3) appointmentRepository.getLastAppointment.mockResolvedValue(lastAppointment) patientRepository.get.mockResolvedValue(patient) }) it('updated the availability time', async () => { await subject.cancel(appointmentId, requester) expect(availabilityTimeRepository.update).toBeCalled() }) describe('and does have not reschedule credit', () => { describe('when cancellation reason decrease credit', () => { describe('when 2 hours left for consultation', () => { beforeEach(async () => { appointment.scheduled_to = addHours(new Date(), 2) }) it('does not update teleconsultationsCredit', async () => { await subject.cancel(appointmentId, requester) expect(teleconsultationsCreditService.createTransaction).not.toBeCalled() }) }) describe('when more than 2 hours left for consultation', () => { beforeEach(async () => { appointment.scheduled_to = addHours(new Date(), 3) }) it('does not update teleconsultationsCredit', async () => { await subject.cancel(appointmentId, requester) expect(teleconsultationsCreditService.createTransaction).not.toBeCalled() }) }) }) }) describe('and availability time update throws error', () => { beforeAll(() => { availabilityTimeRepository.update.mockRejectedValue(new Error()) }) afterAll(() => { availabilityTimeRepository.update.mockReset() }) it('rollbacks the appointment cancel', async () => { try { await subject.cancel(appointmentId, requester) } catch (error) { } finally { expect(appointmentRepository.update).toBeCalledWith( expect.objectContaining({ state: initialState, canceled_at: null }), ) } }) }) }) } describe('when the requester is an user', () => { describe('when is a return', () => { beforeEach(() => { appointment.is_return = true }) cancelFlow() }) describe('when is not a return', () => { beforeEach(() => { appointment.is_return = false }) describe('when the appointment was not started', () => { beforeEach(() => { appointment.state = 'scheduled' }) describe('when the reason is requested by patient', () => { it(`doesn't transfers the appointment`, async () => { await subject.cancel(appointmentId, requester, { value: AppointmentCancellationReasons.NUTRI_CANCELLED_BY_PATIENT_REQUEST, }) expect(transferService.transfer).not.toBeCalled() }) }) describe(`when the reason isn't requested by patient`, () => { describe('when the appointment is less then 10 minutes from now', () => { it('cancels the appointment', async () => { await subject.cancel(appointmentId, requester, { value: AppointmentCancellationReasons.PERSONAL_UNFORESEEN, }) expect(transferService.transfer).not.toBeCalled() }) }) describe(`when the appointment is more then ${LIMIT_MINUTES_TO_TRANSFER} minutes from now`, () => { beforeEach(() => { appointment.scheduled_to = addMinutes(new Date(), LIMIT_MINUTES_TO_TRANSFER + 2) }) it('transfers the appointment', async () => { await subject.cancel(appointmentId, requester, { value: AppointmentCancellationReasons.PERSONAL_UNFORESEEN, }) expect(transferService.transfer).toBeCalled() }) }) }) }) describe('when the appointment was started', () => { beforeEach(() => { initialState = appointment.state = 'user_waiting' }) cancelFlow() describe('when cancellation reason does not decrease credit', () => { let usedCredit: TeleconsultationsCredit beforeAll(async () => { appointmentCancelReason.decrease_credit = false usedCredit = await factory.build('teleconsultations_credit') }) beforeEach(() => { teleconsultationsCreditService.getByAppointmentId.mockResolvedValue(usedCredit) }) it('add new teleconsultation credit', async () => { await subject.cancel(appointmentId, requester) expect(teleconsultationsCreditService.createTransaction).toBeCalledWith({ patientId: appointment.patient_id, reason: 'appointment_cancellation', value: 1, author: requester, objectModel: appointment, creditId: usedCredit.credit_id, }) }) }) }) }) }) describe('when the requester is an patient', () => { beforeAll(async () => { requester = await factory.build('patient') }) describe('when is a return', () => { beforeEach(() => { appointment.is_return = true }) cancelFlow() }) describe('when is not a return', () => { beforeEach(() => { appointment.is_return = false }) cancelFlow() describe('when cancellation reason does not decrease credit', () => { let usedCredit: TeleconsultationsCredit beforeAll(async () => { appointmentCancelReason.decrease_credit = false usedCredit = await factory.build('teleconsultations_credit') }) beforeEach(() => { teleconsultationsCreditService.getByAppointmentId.mockResolvedValue(usedCredit) }) it('updates teleconsultationsCredit', async () => { await subject.cancel(appointmentId, requester) expect(teleconsultationsCreditService.createTransaction).toBeCalledWith({ patientId: appointment.patient_id, reason: 'appointment_cancellation', value: 1, author: requester, objectModel: appointment, creditId: usedCredit.credit_id, }) }) }) }) }) }) }) }) }) describe('#reallocateNutri', () => { let appointment: Appointment let availability: Availability let fromAvailabilityTime: AvailabilityTime let toAvailabilityTime: AvailabilityTime let promiseBook: Promise beforeAll(async () => { fromAvailabilityTime = await factory.build('availability_time') toAvailabilityTime = await factory.build('availability_time') availability = await factory.build('availability', { id: toAvailabilityTime.availability_id }) appointment = await factory.build('appointment', { id: fromAvailabilityTime.appointment_id, is_return: false }) promiseBook = Promise.resolve() }) beforeEach(async () => { subject.book = jest.fn().mockImplementation(() => promiseBook) availabilityTimeRepository.get.mockReturnValueOnce(fromAvailabilityTime) availabilityTimeRepository.get.mockReturnValueOnce(toAvailabilityTime) availabilityTimeRepository.update.mockResolvedValue(null) availabilityRepository.get.mockResolvedValue(availability) appointmentRepository.get.mockResolvedValue(appointment) appointmentRepository.update.mockResolvedValue(null) patientRepository.updateUser.mockResolvedValue(null) await subject.reallocateNutri(fromAvailabilityTime.id, toAvailabilityTime.id) }) it('moves the appoint to destiny availability time', () => { expect(availabilityTimeRepository.update).toBeCalledWith( expect.objectContaining({ id: toAvailabilityTime.id, status: 'scheduled', appointment_id: appointment.id, }), ) }) it('makes free the appoint origin availability time', () => { expect(availabilityTimeRepository.update).toBeCalledWith( expect.objectContaining({ id: fromAvailabilityTime.id, status: 'available', appointment_id: null }), ) }) it('updates the appointment with the destiny user', () => { expect(appointmentRepository.reallocate).toBeCalledWith( expect.objectContaining({ id: appointment.id, user_id: availability.user_id, scheduled_to: parseISO(`${availability.formatedDate}T${toAvailabilityTime.time}`), }), ) }) it('updates the patient with the destiny user', () => { expect(patientRepository.updateUser).toBeCalledWith( expect.objectContaining({ id: appointment.patient_id }), availability.user_id, ) }) }) describe('#reschedule', () => { let appointmentId: string let availabilityTimeId: string let requester: User | Patient let appointmentCancelReason: AppointmentCancelReason beforeAll(async () => { appointmentId = datatype.uuid() availabilityTimeId = datatype.uuid() requester = await factory.build('user') appointmentCancelReason = await factory.build('appointment_cancel_reason', { value: AppointmentCancellationReasons.PATIENT_RESCHEDULED, decrease_credit: true, }) }) beforeEach(async () => { appointmentCancelReasonRepository.get.mockResolvedValue(appointmentCancelReason) }) describe('when appointment is not found', () => { beforeEach(() => { appointmentRepository.get.mockResolvedValue(null) }) it('throws error', async () => { await expect(subject.reschedule(appointmentId, availabilityTimeId, requester)).rejects.toThrowError( NotFoundError, ) }) }) describe('when appointment is found', () => { let appointment: Appointment beforeAll(async () => { appointment = await factory.build('appointment', { id: appointmentId, is_return: false }) }) beforeEach(() => { appointmentRepository.get.mockResolvedValue(appointment) }) describe('when avaibilityTime is not found', () => { beforeEach(() => { availabilityTimeRepository.get.mockResolvedValue(null) }) it('throws error', async () => { await expect(subject.reschedule(appointmentId, availabilityTimeId, requester)).rejects.toThrowError( NotFoundError, ) }) }) describe('when avaibilityTime is found', () => { let availabilityTime: AvailabilityTime beforeAll(async () => { availabilityTime = await factory.build('availability_time', { id: availabilityTimeId }) }) beforeEach(() => { availabilityTimeRepository.get.mockResolvedValue(availabilityTime) }) describe('when availabilityTime is not available', () => { beforeAll(() => { availabilityTime.status = 'scheduled' }) it('throws error', async () => { await expect(subject.reschedule(appointmentId, availabilityTimeId, requester)).rejects.toThrowError( BusinessExceptionError, ) }) }) describe('when availabilityTime is available', () => { let teleconsultationsCredit: number let lastAppointment: Appointment let patient: Patient let cancelSpy: jest.SpyInstance let bookSpy: jest.SpyInstance beforeAll(async () => { availabilityTime.status = 'available' lastAppointment = await factory.build('appointment') }) beforeEach(() => { appointmentRepository.getLastAppointment.mockResolvedValue(lastAppointment) patientRepository.get.mockResolvedValue(patient) teleconsultationsCreditService.getBalance.mockResolvedValue(teleconsultationsCredit) cancelSpy = jest.spyOn(subject, 'cancel') bookSpy = jest.spyOn(subject, 'book') }) describe('when cancellation reason decrease credit', () => { beforeAll(() => { appointmentCancelReason.decrease_credit = true }) describe('when does not have teleconsultations credit', () => { beforeAll(async () => { patient = await factory.build('patient:subscriber') teleconsultationsCredit = 0 }) describe('when more than 2 hours left for consultation', () => { beforeAll(() => { appointment.scheduled_to = addHours(new Date(), 3) }) it('cancel current appointment', async () => { await subject.reschedule(appointmentId, availabilityTimeId, requester) expect(cancelSpy).toBeCalled() }) it('creates new appointment', async () => { await subject.reschedule(appointmentId, availabilityTimeId, requester) expect(bookSpy).toBeCalledWith(appointment.patient_id, availabilityTimeId, appointment.id, requester) }) }) describe('when 2 hours left for consultation', () => { beforeAll(() => { appointment.scheduled_to = addHours(new Date(), 2) }) it('cancel current appointment', async () => { await subject.reschedule(appointmentId, availabilityTimeId, requester) expect(cancelSpy).toBeCalled() }) it('creates new appointment', async () => { await subject.reschedule(appointmentId, availabilityTimeId, requester) expect(bookSpy).toBeCalledWith(appointment.patient_id, availabilityTimeId, appointment.id, requester) }) }) }) describe('when has teleconsultations credit', () => { beforeAll(async () => { patient = await factory.build('patient:subscriber') teleconsultationsCredit = 1 }) it('cancel current appointment', async () => { await subject.reschedule(appointmentId, availabilityTimeId, requester) expect(cancelSpy).toBeCalled() }) it('creates new appointment', async () => { await subject.reschedule(appointmentId, availabilityTimeId, requester) expect(bookSpy).toBeCalledWith(appointment.patient_id, availabilityTimeId, appointment.id, requester) }) }) }) describe('when cancellation reason does not decrease credit', () => { beforeAll(() => { appointmentCancelReason.decrease_credit = false }) afterAll(() => { appointmentCancelReason.decrease_credit = true }) describe('when more than 2 hours left for consultation', () => { beforeAll(() => { appointment.scheduled_to = addHours(new Date(), 3) }) it('cancel current appointment', async () => { await subject.reschedule(appointmentId, availabilityTimeId, requester) expect(cancelSpy).toBeCalled() }) it('creates new appointment', async () => { await subject.reschedule(appointmentId, availabilityTimeId, requester) expect(bookSpy).toBeCalledWith(appointment.patient_id, availabilityTimeId, appointment.id, requester) }) }) describe('when 2 hours left for consultation', () => { beforeAll(() => { appointment.scheduled_to = addHours(new Date(), 2) }) describe('when does not have teleconsultations credit', () => { beforeAll(async () => { patient = await factory.build('patient:subscriber') teleconsultationsCredit = 0 }) it('cancel current appointment', async () => { await subject.reschedule(appointmentId, availabilityTimeId, requester) expect(cancelSpy).toBeCalled() }) it('creates new appointment', async () => { await subject.reschedule(appointmentId, availabilityTimeId, requester) expect(bookSpy).toBeCalledWith(appointment.patient_id, availabilityTimeId, appointment.id, requester) }) }) describe('when has teleconsultations credit', () => { beforeAll(async () => { patient = await factory.build('patient:subscriber') teleconsultationsCredit = 1 }) it('cancel current appointment', async () => { await subject.reschedule(appointmentId, availabilityTimeId, requester) expect(cancelSpy).toBeCalled() }) it('creates new appointment', async () => { await subject.reschedule(appointmentId, availabilityTimeId, requester) expect(bookSpy).toBeCalledWith(appointment.patient_id, availabilityTimeId, appointment.id, requester) }) }) }) }) }) }) }) }) describe('#getTeleconsultationsCredit', () => { let patient: Patient beforeAll(async () => { patient = await factory.build('patient') }) beforeEach(() => { patientRepository.get.mockResolvedValue(patient) }) describe('when patient is a internal collaborator', () => { beforeAll(() => { patient.internal_collaborator = true }) it('returns 1', async () => { await expect(subject['getTeleconsultationsCredit'](patient)).resolves.toEqual({ teleconsultation: 1, }) }) }) describe('when patient is subscriber or unsubscriber', () => { beforeAll(async () => { patient = await factory.build('patient:subscriber') teleconsultationsCreditService.getSeparateCredits.mockResolvedValue({ teleconsultation: 1, }) }) it('returns the teleconsultations credit balance', async () => { await expect(subject['getTeleconsultationsCredit'](patient)).resolves.toEqual({ teleconsultation: 1, }) }) }) }) describe('#checkBookingConditions', () => { let patientId: string let patient: Patient let teleconsultationsCredit: number let result: BookingConditions beforeAll(() => { patientId = datatype.uuid() teleconsultationsCredit = 1 }) beforeEach(() => { teleconsultationsCreditService.getSeparateCredits.mockResolvedValue({ teleconsultation: teleconsultationsCredit, }) }) describe('when did not have an appointment yet', () => { beforeEach(async () => { appointmentRepository.getLastAppointment.mockResolvedValue(null) patient = await factory.build('patient:subscriber', { id: patientId }) patientRepository.get.mockResolvedValue(patient) result = await subject.checkBookingConditions(patientId) }) afterEach(() => { subject['patientCached'][patientId] = null }) it(`loses_credit_if_cancel returns false`, () => { expect(result).toEqual(expect.objectContaining({ loses_credit_if_cancel: false })) }) }) describe.each` state | hoursBeforeSchedule | losesCreditIfCancel ${'food_plan_pending'} | ${null} | ${false} ${'food_plan_in_construction'} | ${null} | ${false} ${'finished'} | ${null} | ${false} ${'scheduled'} | ${30} | ${false} ${'scheduled'} | ${24} | ${false} ${'scheduled'} | ${22} | ${true} ${'scheduled'} | ${2} | ${true} ${'cancelled'} | ${30} | ${false} ${'cancelled'} | ${24} | ${false} ${'cancelled'} | ${22} | ${true} `( 'when the last appointment has state "$state" and "$hoursBeforeSchedule" hours before', ({ state, hoursBeforeSchedule, losesCreditIfCancel }) => { let rescheduleCredit: number beforeEach(async () => { const now = new Date() const canceled_at = hoursBeforeSchedule ? now : null const scheduled_to = addHours(now, hoursBeforeSchedule || 3) const lastAppointment = await factory.build('appointment', { state, canceled_at, scheduled_to, reschedule_credit: rescheduleCredit, is_return: false, }) appointmentRepository.getLastAppointment.mockResolvedValue(lastAppointment) patient = await factory.build('patient:subscriber', { id: patientId }) patientRepository.get.mockResolvedValue(patient) teleconsultationsCreditService.getBalance.mockResolvedValue(teleconsultationsCredit) result = await subject.checkBookingConditions(patientId) }) afterEach(() => { subject['patientCached'][patientId] = null subject['lastAppointmentCached'][patientId] = null }) it(`loses_credit_if_cancel returns ${losesCreditIfCancel}`, () => { expect(result).toEqual(expect.objectContaining({ loses_credit_if_cancel: losesCreditIfCancel })) }) if (!APPOINTMENT_STATE_CARRIED_OUT.includes(state) && losesCreditIfCancel) { describe('when does not have rescheduleCredit', () => { beforeAll(() => { rescheduleCredit = 0 }) describe('when has teleconsultationsCredit', () => { beforeAll(() => { teleconsultationsCredit = 1 }) it('allow_reschedule returns false', () => { expect(result).toEqual(expect.objectContaining({ allow_reschedule: false })) }) it('allow_schedule returns true', () => { expect(result).toEqual(expect.objectContaining({ allow_schedule: true })) }) }) describe('when does not have teleconsultationsCredit', () => { beforeAll(() => { teleconsultationsCredit = 0 }) it('allow_reschedule returns false', () => { expect(result).toEqual(expect.objectContaining({ allow_reschedule: false })) }) it('allow_schedule returns false', () => { expect(result).toEqual(expect.objectContaining({ allow_schedule: false })) }) }) }) describe('when has rescheduleCredit', () => { beforeAll(() => { rescheduleCredit = 1 }) if (hoursBeforeSchedule <= 2) { it('allow_reschedule returns false', () => { expect(result).toEqual(expect.objectContaining({ allow_reschedule: false })) }) } if (hoursBeforeSchedule > 2) { it('allow_reschedule returns true', () => { expect(result).toEqual(expect.objectContaining({ allow_reschedule: true })) }) } }) } }, ) }) describe('#cancellationReasonDecreasesCredit', () => { let value: AppointmentCancellationReasons let appointmentCancelReason: AppointmentCancelReason beforeAll(async () => { appointmentCancelReason = await factory.build('appointment_cancel_reason', { value: AppointmentCancellationReasons.PATIENT_CANCELLED, }) value = appointmentCancelReason.value appointmentCancelReasonRepository.get.mockResolvedValue(null) }) describe('when cancellation reason is not found', () => { it('throws error', async () => { await expect(subject['cancellationReasonDecreasesCredit'](value)).rejects.toThrowError(NotFoundError) }) }) describe('when cancellation reason is found', () => { beforeEach(() => { appointmentCancelReasonRepository.get.mockResolvedValue(appointmentCancelReason) }) it('returns descrease_credit value', async () => { await expect(subject['cancellationReasonDecreasesCredit'](value)).resolves.toEqual( appointmentCancelReason.decrease_credit, ) }) }) }) describe('#cancelAllAppointmentsWithNutriNoArrived', () => { let appointments: Appointment[] let now: Date let spyCancelAppointment: jest.SpyInstance beforeAll(() => { now = new Date() MockDate.set(now) }) beforeEach(() => { spyCancelAppointment = jest.spyOn(subject, 'cancel') spyCancelAppointment.mockResolvedValue(null) }) afterAll(() => { MockDate.reset() }) it('get appointments 10 minutes ago', async () => { await subject.cancelAllAppointmentsWithNutriNoArrived() expect(appointmentRepository.findAppointmentsWithNutriNoArrived).toBeCalledWith(subMinutes(now, 10)) }) describe('when there are no appointments with 10 minutes late', () => { it('does not cancel any appointment', async () => { await subject.cancelAllAppointmentsWithNutriNoArrived() expect(spyCancelAppointment).not.toBeCalled() }) }) describe('when there are appointments with 10 minutes late', () => { const count = datatype.number({ min: 1, max: 10 }) beforeAll(async () => { appointments = await factory.buildMany('appointment', count) }) beforeEach(() => { appointmentRepository.findAppointmentsWithNutriNoArrived.mockResolvedValue(appointments) }) it('cancel all appointments', async () => { await subject.cancelAllAppointmentsWithNutriNoArrived() expect(spyCancelAppointment).toBeCalledTimes(count) }) }) }) describe('#cancelScheduledAppointmentsBySubscriptionCancellation', () => { const count = datatype.number({ min: 1, max: 10 }) let appointments: Appointment[] let spyCancelAppointment: jest.SpyInstance beforeAll(async () => { appointments = await factory.buildMany('appointment', count) }) beforeEach(() => { spyCancelAppointment = jest.spyOn(subject, 'cancel').mockResolvedValue(null) appointmentRepository.getScheduledAppointmentsWithCancelledSubscription.mockResolvedValue(appointments) }) it('cancel all scheduled appointments with cancelled subscription', async () => { await subject.cancelBySubscriptionCancellation() expect(appointmentRepository.getScheduledAppointmentsWithCancelledSubscription).toBeCalled() expect(spyCancelAppointment).toBeCalledTimes(count) }) }) describe('#cancelBySubscriptionSuspension', () => { const count = datatype.number({ min: 1, max: 10 }) let appointments: Appointment[] let spyCancelAppointment: jest.SpyInstance beforeAll(async () => { appointments = await factory.buildMany('appointment', count) }) beforeEach(() => { spyCancelAppointment = jest.spyOn(subject, 'cancel').mockResolvedValue(null) appointmentRepository.findScheduledAppointmentsByPatient.mockResolvedValue(appointments) }) it('cancel all scheduled appointments with cancelled subscription', async () => { const id = datatype.uuid() await subject.cancelBySubscriptionSuspension(id) expect(appointmentRepository.findScheduledAppointmentsByPatient).toBeCalledWith(id) expect(spyCancelAppointment).toBeCalledTimes(count) }) }) })