import { Test, TestingModule } from '@nestjs/testing' import { datatype } from 'faker' import { factory } from '../../../../test/factories' import { MockedModule } from '../../../../test/types' import { BusinessExceptionError, NotFoundError } from '../../../errors' import { Patient, PaymentProviders, Plan, Subscription as SubscriptionModel } from '../../../models' import { PatientRepository, PlanRepository, SubscriptionTransactionRepository } from '../../../repositories/' import { SmartfitApi, SubscriptionService, Transaction } from '../../../services' import { N2BPaymentNotification, N2BPaymentRefundNotification } from './types' import { Subscription } from './subscription' import { Person } from './person' describe('Subscription', () => { let subject: Subscription let patientRepository: MockedModule let planRepository: MockedModule let subscriptionService: MockedModule let smartfitAPI: MockedModule let subscriptionTransactionRepository: MockedModule beforeAll(async () => { patientRepository = { get: jest.fn(), } planRepository = { getWithFeatures: jest.fn(), } subscriptionService = { addSubscription: jest.fn(), addInstallments: jest.fn(), addTransaction: jest.fn(), uncancel: jest.fn(), updateCancellationDate: jest.fn(), updateSuspendedDate: jest.fn(), updateInstallmentPayment: jest.fn(), updateCardNumber: jest.fn(), } smartfitAPI = { createIntegration: jest.fn(), getPersonByLogin: jest.fn(), getWalletToken: jest.fn(), } subscriptionTransactionRepository = { findLastBySubscription: jest.fn(), } const app: TestingModule = await Test.createTestingModule({ controllers: [], providers: [ { provide: PatientRepository, useValue: patientRepository, }, { provide: PlanRepository, useValue: planRepository, }, { provide: SubscriptionService, useValue: subscriptionService, }, { provide: SmartfitApi, useValue: smartfitAPI, }, { provide: SubscriptionTransactionRepository, useValue: subscriptionTransactionRepository, }, Subscription, ], }).compile() subject = app.get(Subscription) }) afterEach(() => { jest.resetAllMocks() }) describe('#updateSubscriberStatus', () => { let patient: Patient let plan: Plan let params: N2BPaymentNotification let createIntegrationOnSmartFitSpy: jest.SpyInstance let newSubscription: SubscriptionModel beforeAll(async () => { params = await factory.build('n2b_payment:payment', { invoice_installment: await factory.build('n2b_payment:installment', { installment_number: 1 }), }) patient = await factory.build('patient') plan = await factory.build('plan:n2b') newSubscription = await factory.build('subscription:n2b') createIntegrationOnSmartFitSpy = jest.spyOn(subject, 'createIntegrationOnSmartFit') params.invoice_installment.invoice.subscription.data = { origin: 'smartfit' } }) beforeEach(async () => { patientRepository.get.mockResolvedValue(patient) planRepository.getWithFeatures.mockResolvedValue(plan) createIntegrationOnSmartFitSpy.mockResolvedValue(undefined) smartfitAPI.getWalletToken.mockResolvedValue({ token: datatype.uuid(), kind: 'credit' }) subscriptionService.addSubscription.mockResolvedValue(newSubscription) }) afterAll(() => { createIntegrationOnSmartFitSpy.mockRestore() }) it('notify smartfit about transaction', async () => { await subject.updateSubscriberStatus(params) expect(createIntegrationOnSmartFitSpy).toBeCalled() }) describe('when patient is not found', () => { beforeAll(() => { patient = undefined }) afterAll(async () => { patient = await factory.build('patient') }) it('throws an error', async () => { await expect(subject.updateSubscriberStatus(params)).rejects.toThrow(NotFoundError) }) }) describe(`when webhook doesn't provide card data`, () => { let person: Person let transaction: Transaction beforeEach(() => { transaction = { ...params.transaction, card: undefined } smartfitAPI.getPersonByLogin.mockResolvedValue(person) }) describe('when person is not found', () => { beforeAll(() => { person = undefined }) it('throws an error', async () => { await expect(subject.updateSubscriberStatus({ ...params, transaction })).rejects.toThrow(NotFoundError) }) }) describe('when person exists', () => { beforeAll(async () => { person = await factory.build('smartfit:person') }) beforeEach(() => { smartfitAPI.getPersonByLogin.mockResolvedValue({ ...person, lastDigitsCardNumber: params.transaction.card.last4, }) smartfitAPI.getWalletToken.mockResolvedValue({ kind: params.transaction.card.kind }) }) it('get card data from SmartFit api and add it to transaction', async () => { await subject.updateSubscriberStatus({ ...params, transaction }) expect(subscriptionService.addTransaction).toHaveBeenCalledWith( expect.objectContaining({ card_number: params.transaction.card.last4, card_type: params.transaction.card.kind, }), ) }) }) }) describe('when is the first installments', () => { it('logs subscription transaction', async () => { await subject.updateSubscriberStatus(params) expect(subscriptionService.addTransaction).toBeCalledWith( expect.objectContaining({ subscription: newSubscription }), ) }) describe('when patient is already a subscriber', () => { beforeAll(async () => { patient = await factory.build('patient:subscriber') }) afterAll(async () => { patient = await factory.build('patient') }) it('throws an error', async () => { await expect(subject.updateSubscriberStatus(params)).rejects.toThrow(BusinessExceptionError) }) }) describe('when payment is success', () => { it('creates a subscription', async () => { await subject.updateSubscriberStatus(params) expect(subscriptionService.addSubscription).toBeCalledWith( expect.objectContaining({ options: expect.objectContaining({ cardNumber: params.transaction.card.last4, originalTransaction: params.invoice_installment.invoice.subscription_id.toString(), }), }), ) }) }) describe('when payment is failed', () => { beforeAll(() => { params.amount = 0 params.paid_at = null }) it('does not create subscription', async () => { await subject.updateSubscriberStatus(params) expect(subscriptionService.addSubscription).not.toBeCalled() }) it('logs subscription transaction with no subscription', async () => { await subject.updateSubscriberStatus(params) expect(subscriptionService.addTransaction).toBeCalledWith( expect.objectContaining({ subscription: undefined }), ) }) }) }) describe('when is not the first installments', () => { beforeAll(async () => { patient = await factory.build('patient:subscriber') params.invoice_installment.installment_number = 2 }) it('logs subscription transaction', async () => { await subject.updateSubscriberStatus(params) expect(subscriptionService.addTransaction).toBeCalledWith( expect.objectContaining({ subscription: patient.subscription }), ) }) describe('when payment is success', () => { beforeAll(() => { params.amount = datatype.number({ min: 1 }) params.paid_at = new Date() }) describe(`when subscription doesn't has the card number`, () => { it('add and update subscription', async () => { await subject.updateSubscriberStatus(params) expect(subscriptionService.updateCardNumber).toHaveBeenCalledWith( patient.subscription, params.transaction.card.last4, ) }) }) describe('when the subscription is not suspended', () => { it('does not suspend the subscription', async () => { await subject.updateSubscriberStatus(params) expect(subscriptionService.updateSuspendedDate).not.toBeCalledWith( expect.objectContaining({ id: patient.subscription.id, }), expect.any(Date), ) }) }) describe('when the subscription is suspended', () => { beforeAll(() => { patient.subscription.suspendedAt = new Date() }) it('cancels suspention of the subscription', async () => { await subject.updateSubscriberStatus(params) expect(subscriptionService.updateSuspendedDate).toBeCalledWith( expect.objectContaining({ id: patient.subscription.id, }), null, ) }) }) describe(`when webhook provides card data`, () => { it('add it to transaction', async () => { await subject.updateSubscriberStatus(params) expect(subscriptionService.addTransaction).toHaveBeenCalledWith( expect.objectContaining({ card_number: params.transaction.card.last4, card_type: params.transaction.card.kind, }), ) }) }) }) describe('when payment is failed', () => { beforeAll(() => { params.amount = 0 params.paid_at = null patient.subscription.suspendedAt = null }) describe('when the subscription is not suspended', () => { it('suspends the subscription', async () => { await subject.updateSubscriberStatus(params) expect(subscriptionService.updateSuspendedDate).toBeCalledWith( expect.objectContaining({ id: patient.subscription.id, }), expect.any(Date), ) }) }) describe('when the subscription is suspended', () => { beforeAll(() => { patient.subscription.suspendedAt = new Date() }) it('does not suspend the subscription', async () => { await subject.updateSubscriberStatus(params) expect(subscriptionService.updateSuspendedDate).not.toBeCalled() }) }) }) }) }) describe('#expiresSubscription', () => { let patient: Patient let person: Person let createIntegrationOnSmartFitSpy: jest.SpyInstance let params: N2BPaymentRefundNotification beforeAll(async () => { params = await factory.build('n2b_payment:refund') patient = await factory.build('patient:subscriber') person = await factory.build('smartfit:person') createIntegrationOnSmartFitSpy = jest.spyOn(subject, 'createIntegrationOnSmartFit') params.invoice_pay.invoice_installment.invoice.subscription.data = { origin: 'smartfit' } patient.subscription.paymentProvider = PaymentProviders.N2B }) beforeEach(() => { patientRepository.get.mockResolvedValue(patient) smartfitAPI.getPersonByLogin.mockResolvedValue(person) createIntegrationOnSmartFitSpy.mockResolvedValue(undefined) smartfitAPI.getWalletToken.mockResolvedValue({ token: datatype.uuid(), kind: 'credit' }) }) afterAll(() => { createIntegrationOnSmartFitSpy.mockRestore() }) describe('when patient is not found', () => { beforeAll(() => { patient = undefined }) afterAll(async () => { patient = await factory.build('patient:subscriber') patient.subscription.paymentProvider = PaymentProviders.N2B }) it('throws an error', async () => { await expect(subject.expiresSubscription(params)).rejects.toThrow(NotFoundError) }) }) describe('when refund is failed', () => { beforeAll(() => { params.amount = 0 }) it('does not cancel the subscription', async () => { await subject.expiresSubscription(params) expect(subscriptionService.updateCancellationDate).not.toBeCalled() }) it('uncancel the subscription', async () => { await subject.expiresSubscription(params) expect(subscriptionService.uncancel).toBeCalled() }) it('notify smartfit about fail', async () => { await subject.expiresSubscription(params) expect(createIntegrationOnSmartFitSpy).toBeCalledWith( patient, params.invoice_pay.invoice_installment.invoice.subscription.data.origin, patient.subscription.id.toString(), 'failed', ) }) describe('when subscription already cancelled', () => { beforeAll(() => { patient.subscription.cancellationAt = new Date() }) afterAll(() => { delete patient.subscription.cancellationAt }) it('does not uncancel subscription', async () => { await subject.expiresSubscription(params) expect(subscriptionService.uncancel).not.toBeCalled() }) }) describe('when subscription is not N2B', () => { beforeAll(() => { patient.subscription.paymentProvider = PaymentProviders.SMARTFIT }) afterAll(() => { patient.subscription.paymentProvider = PaymentProviders.N2B }) it('logs subscription transaction with no subscription id', async () => { await subject.expiresSubscription(params) expect(subscriptionService.addTransaction).not.toBeCalledWith( expect.objectContaining({ subscription: patient.subscription }), ) }) it('does not cancel the subscription', async () => { await subject.expiresSubscription(params) expect(subscriptionService.updateCancellationDate).not.toBeCalled() }) it('does not uncancel the subscription', async () => { await subject.expiresSubscription(params) expect(subscriptionService.uncancel).not.toBeCalled() }) it('does not notify smartfit about fail', async () => { await subject.expiresSubscription(params) expect(createIntegrationOnSmartFitSpy).not.toBeCalledWith( patient, PaymentProviders.N2B, params.invoice_pay.invoice_installment.invoice.subscription.data.origin, patient.subscription.id.toString(), 'failed', ) }) }) }) describe('when refund is success', () => { beforeAll(() => { params.amount = datatype.number({ min: 1 }) }) it('cancels the subscription', async () => { await subject.expiresSubscription(params) expect(subscriptionService.updateCancellationDate).toBeCalled() }) it('does not uncancel the subscription', async () => { await subject.expiresSubscription(params) expect(subscriptionService.uncancel).not.toBeCalled() }) it('notify smartfit about cancel', async () => { await subject.expiresSubscription(params) expect(createIntegrationOnSmartFitSpy).toBeCalledWith( patient, params.invoice_pay.invoice_installment.invoice.subscription.data.origin, patient.subscription.id.toString(), 'cancelled', ) }) describe('when the subscription already cancelled', () => { beforeAll(() => { patient.subscription.cancellationAt = new Date() }) it('does not cancel the subscription', async () => { await subject.expiresSubscription(params) expect(subscriptionService.updateCancellationDate).not.toBeCalled() }) }) describe('when subscription is not N2B', () => { beforeAll(() => { patient.subscription.paymentProvider = PaymentProviders.SMARTFIT }) afterAll(() => { patient.subscription.paymentProvider = PaymentProviders.N2B }) it('logs subscription transaction with no subscription id', async () => { await subject.expiresSubscription(params) expect(subscriptionService.addTransaction).not.toBeCalledWith( expect.objectContaining({ subscription: patient.subscription }), ) }) it('does not cancels the subscription', async () => { await subject.expiresSubscription(params) expect(subscriptionService.updateCancellationDate).not.toBeCalled() }) it('does not uncancel the subscription', async () => { await subject.expiresSubscription(params) expect(subscriptionService.uncancel).not.toBeCalled() }) it('does not notify smartfit about fail', async () => { await subject.expiresSubscription(params) expect(createIntegrationOnSmartFitSpy).not.toBeCalledWith( patient, PaymentProviders.N2B, params.invoice_pay.invoice_installment.invoice.subscription.data.origin, patient.subscription.id.toString(), 'failed', ) }) }) }) describe(`when webhook doesn't provide card data`, () => { let transaction: Transaction beforeEach(() => { transaction = { ...params.transaction, card: undefined } }) beforeEach(() => { subscriptionTransactionRepository.findLastBySubscription.mockResolvedValue({ cardData: params.transaction.card, }) }) it('get card data from the last transaction', async () => { await subject.expiresSubscription({ ...params, transaction }) expect(subscriptionTransactionRepository.findLastBySubscription).toHaveBeenCalledWith( patient.subscription.id.toString(), ) }) }) it('logs subscription transaction', async () => { await subject.expiresSubscription(params) expect(subscriptionService.addTransaction).toBeCalled() }) }) })