import { Test, TestingModule } from '@nestjs/testing' import { subDays } from 'date-fns' import { date, internet, datatype, random } from 'faker' import { sample } from 'lodash' import MockDate from 'mockdate' import { factory } from '../../../../test/factories' import { MockedModule } from '../../../../test/types' import { BusinessExceptionError, NotFoundError } from '../../../errors' import { CardType, Patient, PaymentProviders, Plan, Subscription, SubscriptionTransaction } from '../../../models' import { PlanRepository, SubscriptionRepository, SubscriptionTransactionRepository } from '../../../repositories/' import { AccountService, SalesChannel, SubscriptionService, TokenType } from '../../../services' import { Person } from './person' describe('Person', () => { let subject: Person let patient: Patient let planRepository: MockedModule let subscriptionRepository: MockedModule let subscriptionService: MockedModule let subscriptionTransactionRepository: MockedModule let accountService: MockedModule beforeAll(async () => { planRepository = { get: jest.fn(), } subscriptionRepository = { getByPatient: jest.fn(), } subscriptionService = { subscribe: jest.fn(), cancelSubscription: jest.fn(), getSubscriptionAmountToRefund: jest.fn(), } subscriptionTransactionRepository = { findBySubscription: jest.fn(), } accountService = { createOrUpdatePatient: jest.fn(), } const app: TestingModule = await Test.createTestingModule({ controllers: [], providers: [ { provide: PlanRepository, useValue: planRepository, }, { provide: SubscriptionRepository, useValue: subscriptionRepository, }, { provide: SubscriptionService, useValue: subscriptionService, }, { provide: SubscriptionTransactionRepository, useValue: subscriptionTransactionRepository, }, { provide: AccountService, useValue: accountService, }, Person, ], }).compile() subject = app.get(Person) }) beforeEach(() => { accountService.createOrUpdatePatient.mockResolvedValue(patient) }) afterEach(() => { jest.resetAllMocks() }) describe('#get', () => { const login = internet.userName() let getSubscriptionResultSpy: jest.SpyInstance beforeAll(() => { getSubscriptionResultSpy = jest.spyOn(subject, 'getSubscriptionResult') }) afterAll(() => { getSubscriptionResultSpy.mockRestore() }) describe('when patient is not found', () => { it('throws an error', async () => { await expect(subject.get(login)).rejects.toThrow(NotFoundError) }) }) describe('when patient is found', () => { beforeAll(async () => { patient = await factory.build('patient') }) beforeEach(() => { getSubscriptionResultSpy.mockResolvedValue({}) }) it(`returns patient's data`, async () => { await expect(subject.get(login)).resolves.toEqual( expect.objectContaining({ id: patient.id, name: patient.name, documentNumber: patient.document_number, email: patient.email, countryCode: patient.country_code, }), ) }) describe('when patient does not have any subscription', () => { it('returns subscription null', async () => { await expect(subject.get(login)).resolves.toEqual( expect.objectContaining({ subscription: null, }), ) }) }) describe('when patient has any subscription', () => { beforeAll(async () => { patient = await factory.build('patient:subscriber') }) it('returns subscription data', async () => { await expect(subject.get(login)).resolves.toEqual( expect.objectContaining({ subscription: expect.any(Object), }), ) }) }) }) }) describe('#getSubscriptions', () => { const login = internet.userName() let getSubscriptionResultSpy: jest.SpyInstance beforeAll(() => { getSubscriptionResultSpy = jest.spyOn(subject, 'getSubscriptionResult') patient = undefined }) afterAll(() => { getSubscriptionResultSpy.mockRestore() }) describe('when patient is not found', () => { it('throws an error', async () => { await expect(subject.getSubscriptions(login)).rejects.toThrow(NotFoundError) }) }) describe('when patient is found', () => { beforeAll(async () => { patient = await factory.build('patient') }) describe('when patient does not have subscriptions', () => { beforeEach(() => { subscriptionRepository.getByPatient.mockResolvedValue([]) }) it('returns empty array', async () => { await expect(subject.getSubscriptions(login)).resolves.toEqual([]) }) }) describe('when patient has subscriptions', () => { let subscriptions: Subscription[] beforeEach(async () => { subscriptions = await factory.buildMany('subscription', datatype.number({ min: 1, max: 5 })) subscriptionRepository.getByPatient.mockResolvedValue(subscriptions) getSubscriptionResultSpy.mockResolvedValue({}) }) it('returns subscriptions found', async () => { await expect(subject.getSubscriptions(login)).resolves.toHaveLength(subscriptions.length) }) }) }) }) describe('#subscribe', () => { const login = internet.userName() const token = datatype.uuid() const kind = 'credit' as CardType const source = random.word() const planId = datatype.uuid() const unitId = datatype.number() const discount = datatype.number({ min: 0 }) const salesChannel = 'smart_system' const params: [string, string, CardType, string, string, number, number, SalesChannel] = [ login, token, kind, source, planId, unitId, discount, salesChannel, ] let plan: Plan beforeEach(() => { planRepository.get.mockResolvedValue(plan) }) describe('when patient is not found', () => { it('throws an error', async () => { await expect(subject.subscribe(...params)).rejects.toThrow(NotFoundError) }) }) describe('when patient is a collaborator', () => { beforeEach(() => { patient.collaborator = true }) afterEach(() => { patient.collaborator = false }) it('throws an error', async () => { await expect(subject.subscribe(...params)).rejects.toThrow(BusinessExceptionError) }) }) describe('when plan is not found', () => { it('throws an error', async () => { await expect(subject.subscribe(...params)).rejects.toThrow(NotFoundError) }) }) describe('when the data is valid', () => { beforeAll(async () => { plan = await factory.build('plan:n2b') }) beforeEach(() => { subscriptionService.subscribe.mockResolvedValue({}) }) it('subscribes patient', async () => { await subject.subscribe(...params) expect(subscriptionService.subscribe).toBeCalledWith({ patient, plan, paymentProvider: { provider: PaymentProviders.N2B, data: { card: { token, kind, tokenType: TokenType.PAYMENT_METHOD }, discount, unitId, source }, }, data: { origin: 'smartfit', sales_channel: salesChannel, smartfit_unit_id: unitId }, }) }) }) }) describe('#unsubscribe', () => { const login = internet.userName() let plan: Plan beforeAll(() => { patient = undefined }) beforeEach(() => { planRepository.get.mockResolvedValue(plan) }) describe('when patient is not found', () => { it('throws an error', async () => { await expect(subject.unsubscribe(login)).rejects.toThrow(NotFoundError) }) }) describe('when patient is found', () => { beforeAll(async () => { patient = await factory.build('patient:subscriber') }) beforeEach(() => { subscriptionService.cancelSubscription.mockResolvedValue({}) }) describe('when current subscription is trial', () => { const amount = 19.9 beforeEach(() => { patient.subscription.trialExpiresAt = date.future() subscriptionService.getSubscriptionAmountToRefund.mockResolvedValue(amount) }) it('requests subscription cancellation refunding plan price', async () => { await subject.unsubscribe(login) expect(subscriptionService.cancelSubscription).toBeCalledWith({ patient, cancellationReason: expect.anything(), cancellationBy: expect.anything(), amount: amount, }) }) }) describe('when current subscription is not trial', () => { beforeAll(() => { patient.subscription.trialExpiresAt = date.past() }) it('requests subscription cancellation with no refund', async () => { await subject.unsubscribe(login) expect(subscriptionService.cancelSubscription).toBeCalledWith({ patient, cancellationReason: expect.anything(), cancellationBy: expect.anything(), amount: 0, }) }) }) }) }) describe('#getSubscriptionResult', () => { const unitAcronym = internet.userName() const paymentProvider: PaymentProviders = sample([PaymentProviders.N2B, PaymentProviders.SMARTFIT]) let subscription: Subscription let patient: Patient let plan: Plan let transactions: SubscriptionTransaction[] = [] beforeAll(async () => { subscription = await factory.build('subscription', { paymentProvider }) patient = await factory.build('patient:subscriber', { subscription }) MockDate.set(new Date()) }) beforeEach(() => { planRepository.get.mockResolvedValue(plan) subscriptionTransactionRepository.findBySubscription.mockResolvedValue(transactions) }) afterAll(() => { MockDate.reset() }) describe('when plan is not found', () => { it('throws an error', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).rejects.toThrow( NotFoundError, ) }) }) describe('when plan is found', () => { beforeAll(async () => { plan = await factory.build(`plan:${paymentProvider}`) }) describe('when subscription is active', () => { it('does not return expiredAt attribute', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.not.toEqual( expect.objectContaining({ expiredAt: subscription.expiresAt, }), ) }) it('does not return cancelledRequestedAt attribute', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.not.toEqual( expect.objectContaining({ cancelledRequestedAt: subscription.expiresAt, }), ) }) }) describe('when subscriptions is in cancellation', () => { beforeAll(() => { subscription.cancellationRequestAt = date.past() }) afterAll(() => { delete subscription.cancellationRequestAt }) it('does not return cancellationRequestAt attribute', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.not.toEqual( expect.objectContaining({ cancellationRequestAt: subscription.cancellationRequestAt, }), ) }) it('returns cancelledRequestedAt attribute', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.toEqual( expect.objectContaining({ cancelledRequestedAt: subscription.cancellationRequestAt, }), ) }) }) describe(`when subscription's subscribedAt is empty`, () => { beforeAll(() => { subscription.subscribedAt = null }) it('returns subscribedAt with createdAt date', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.toEqual( expect.objectContaining({ subscribedAt: subscription.createdAt, }), ) }) }) describe('when subscriptions is expired', () => { beforeAll(() => { subscription.expiresAt = date.past() }) afterAll(() => { delete subscription.expiresAt }) it('does not return expiresAt attribute', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.not.toEqual( expect.objectContaining({ expiresAt: subscription.expiresAt, }), ) }) it('returns expiredAt attribute', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.toEqual( expect.objectContaining({ expiredAt: subscription.expiresAt, }), ) }) }) describe('when subscriptions is cancelled', () => { beforeAll(() => { subscription.cancellationAt = date.past() }) describe('when expires at is greater than cancellationAt', () => { beforeAll(() => { subscription.expiresAt = date.future() }) it('does not return expiresAt attribute', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.not.toEqual( expect.objectContaining({ expiresAt: subscription.expiresAt, }), ) }) it('returns expiredAt attribute', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.toEqual( expect.objectContaining({ expiredAt: subscription.cancellationAt, }), ) }) }) describe('when expires at is less than cancellationAt', () => { beforeAll(() => { subscription.expiresAt = subDays(subscription.cancellationAt, 1) }) it('does not return expiresAt attribute', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.not.toEqual( expect.objectContaining({ expiresAt: subscription.expiresAt, }), ) }) it('returns expiredAt attribute', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.toEqual( expect.objectContaining({ expiredAt: subscription.cancellationAt, }), ) }) }) }) describe('when does not have transactions', () => { it('returns transactions values empty', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.toEqual( expect.objectContaining({ id: subscription.id, totalPaid: 0, notPaidYet: plan.getPaymentProvider(paymentProvider).amount, transactions, }), ) }) }) describe('when has transactions', () => { const amount = datatype.number({ min: 1 }) beforeAll(async () => { transactions = [ await factory.build('subscription_transaction:failed'), await factory.build('subscription_transaction:success', { amount }), await factory.build('subscription_transaction:refund', { amount: -amount }), ] }) it('returns array with all transactions', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.toEqual( expect.objectContaining({ transactions: expect.arrayContaining( transactions.map(transaction => expect.objectContaining({ id: transaction.id, }), ), ), }), ) }) it('calcules totalPaid', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.toEqual( expect.objectContaining({ totalPaid: transactions[1].amount + transactions[2].amount, }), ) }) it('calcules notPaidYet', async () => { await expect(subject['getSubscriptionResult'](patient, subscription, unitAcronym)).resolves.toEqual( expect.objectContaining({ notPaidYet: plan.getPaymentProvider(paymentProvider).amount - transactions[1].amount, }), ) }) }) }) }) })