import { Test, TestingModule } from '@nestjs/testing' import { add, addMonths } from 'date-fns' import { datatype, date } from 'faker' import { isEmpty, sample } from 'lodash' import MockDate from 'mockdate' import { factory } from '../../../test/factories' import { MockedModule } from '../../../test/types' import { DiscountCouponInvalidError, DiscountCouponNotFoundError, IsAlreadySubscriberError, SubscribeError, UnprocessableEntityError, } from '../../errors' import { CancellationReason, CancellationsReasons, DiscountCoupon, Intervals, Patient, PaymentProviders, Plan, Subscription, SubscriptionInstallment, } from '../../models' import { DiscountCouponRepository, PatientRepository, PlanRepository, SubscriptionInstallmentRepository, SubscriptionRepository, SubscriptionTransactionRepository, } from '../../repositories' import { CountryHelper } from '../../utils/countries' import { TeleconsultationsCreditService } from '../credits/teleconsultations_credit.service' import { BillingPeriod, CardParams, Currency, Installment, N2BPaymentsService } from '../n2b-payments' import { Addon, Person, SmartfitApi } from '../smartfit' import { TokenKindDisallowedError } from './errors' import { IntervalsHelper } from './helpers' import { N2BPaymentProviderData, SubscriptionService } from './subscription.service' import { CancellationUser, OriginRequest, SubscriptionData } from './types' describe('SubscriptionService', () => { let subscriptionService: SubscriptionService let patientRepository: MockedModule let planRepository: MockedModule let subscriptionRepository: MockedModule let smartfitApi: MockedModule let teleconsultationsCreditService: MockedModule let n2bPaymentsService: MockedModule let subscriptionTransactionRepository: MockedModule let subscriptionInstallmentRepository: MockedModule let discountCouponRepository: MockedModule beforeAll(async () => { patientRepository = { updateSubscription: jest.fn(), } planRepository = { get: jest.fn(), findEnabled: jest.fn(), } subscriptionRepository = { create: jest.fn(), cancel: jest.fn(), getToken: jest.fn(), } smartfitApi = { addAddon: jest.fn(), findAddons: jest.fn(), getByLogin: jest.fn(), getPersonByLogin: jest.fn(), getWalletToken: jest.fn(), } teleconsultationsCreditService = { createTransaction: jest.fn(), } n2bPaymentsService = { generateSubscription: jest.fn(), generateSubscriptionAndChargePayment: jest.fn(), askRefund: jest.fn(), askCancelSubscription: jest.fn(), } subscriptionTransactionRepository = { create: jest.fn(), } subscriptionInstallmentRepository = { create: jest.fn(), findBySubscription: jest.fn(), } discountCouponRepository = { getByCode: jest.fn(), } const app: TestingModule = await Test.createTestingModule({ providers: [ { provide: PatientRepository, useValue: patientRepository, }, { provide: PlanRepository, useValue: planRepository, }, { provide: SubscriptionRepository, useValue: subscriptionRepository, }, { provide: SmartfitApi, useValue: smartfitApi, }, { provide: TeleconsultationsCreditService, useValue: teleconsultationsCreditService, }, { provide: N2BPaymentsService, useValue: n2bPaymentsService, }, { provide: SubscriptionTransactionRepository, useValue: subscriptionTransactionRepository, }, { provide: SubscriptionInstallmentRepository, useValue: subscriptionInstallmentRepository, }, { provide: DiscountCouponRepository, useValue: discountCouponRepository, }, SubscriptionService, ], }).compile() subscriptionService = app.get(SubscriptionService) }) afterEach(() => { jest.clearAllMocks() }) afterAll(() => { jest.restoreAllMocks() }) describe('#subscribe()', () => { let patient: Patient let plan: Plan let paymentProvider: { provider: PaymentProviders } let addSubscriptionWithAddonSpy: jest.SpyInstance let addSubscriptionSpy: jest.SpyInstance let addSubscriptionWithN2BPaymentsSpy: jest.SpyInstance let data: SubscriptionData beforeAll(async () => { plan = await factory.build('plan') paymentProvider = { provider: plan.payment_providers[0].provider } }) beforeEach(async () => { addSubscriptionWithAddonSpy = jest .spyOn(subscriptionService, 'addSubscriptionWithAddon') .mockResolvedValue(null) addSubscriptionSpy = jest.spyOn(subscriptionService, 'addSubscription').mockResolvedValue(null) addSubscriptionWithN2BPaymentsSpy = jest .spyOn(subscriptionService, 'addSubscriptionWithN2BPayments') .mockResolvedValue(null) data = { origin: sample(['n2b', 'smartfit']) } }) afterAll(() => { addSubscriptionWithAddonSpy.mockRestore() addSubscriptionSpy.mockRestore() addSubscriptionWithN2BPaymentsSpy.mockRestore() }) describe('when patient is subscriber', () => { beforeAll(async () => { patient = await factory.build('patient:subscriber') }) it('throws an error', async () => { await expect(subscriptionService.subscribe({ patient, plan, paymentProvider, data })).rejects.toThrowError( IsAlreadySubscriberError, ) }) }) describe('when patient isn`t a subscriber', () => { beforeAll(async () => { patient = await factory.build('patient') }) describe('and the paymentProvider is SMARTFIT', () => { beforeAll(() => { paymentProvider = { provider: PaymentProviders.SMARTFIT } }) it('creates a new subscription', async () => { await subscriptionService.subscribe({ patient, plan, paymentProvider, data }) expect(addSubscriptionWithAddonSpy).toBeCalledWith({ patient, plan, paymentProvider, data }) }) }) describe('and the paymentProvider is N2B', () => { beforeAll(() => { paymentProvider = { provider: PaymentProviders.N2B } }) it('creates a new subscription', async () => { await subscriptionService.subscribe({ patient, plan, paymentProvider, data: { origin: 'n2b' } }) expect(addSubscriptionWithN2BPaymentsSpy).toBeCalledWith({ patient, plan, paymentProvider, data: { origin: 'n2b' }, }) }) describe('and the transaction is not authorized', () => { describe('without a discount coupon', () => { beforeEach(() => { addSubscriptionWithN2BPaymentsSpy = jest .spyOn(subscriptionService, 'addSubscriptionWithN2BPayments') .mockRejectedValueOnce('Transação não autorizada!') }) it('creates a new subscription with SMARTFIT Payment Provider', async () => { await subscriptionService.subscribe({ patient, plan, paymentProvider, data: { origin: 'n2b' } }) expect(addSubscriptionWithN2BPaymentsSpy).toBeCalledWith({ patient, plan, paymentProvider, data: { origin: 'n2b' }, }) expect(addSubscriptionWithAddonSpy).toBeCalledWith({ patient, plan, paymentProvider: { provider: PaymentProviders.SMARTFIT, }, data: { origin: 'n2b' }, }) }) }) describe('with a discount coupon', () => { beforeEach(() => { addSubscriptionWithN2BPaymentsSpy = jest .spyOn(subscriptionService, 'addSubscriptionWithN2BPayments') .mockRejectedValueOnce(new Error('Transação não autorizada!')) }) it('does not create a new subscription with SMARTFIT Payment Provider', async () => { await expect( subscriptionService.subscribe({ patient, plan, paymentProvider, discountCouponCode: 'CINQUENTAO', data: { origin: 'n2b' }, }), ).rejects.toThrowError(Error) expect(addSubscriptionWithAddonSpy).not.toBeCalled() }) }) }) }) describe('when paymentProvider is different from SMARTFIT', () => { beforeAll(() => { paymentProvider = { provider: sample( Object.values(PaymentProviders).filter( provider => provider !== PaymentProviders.SMARTFIT && provider !== PaymentProviders.N2B && provider !== PaymentProviders.WIX, ), ), } }) describe('when has token', () => { let token: string beforeAll(async () => { token = datatype.uuid() }) it('saves the token', async () => { await subscriptionService.subscribe({ patient, plan, paymentProvider, options: { token }, data }) expect(addSubscriptionSpy).toBeCalledWith( expect.objectContaining({ options: expect.objectContaining({ token }) }), ) }) }) it('creates a new subscription', async () => { await subscriptionService.subscribe({ patient, plan, paymentProvider, data }) expect(addSubscriptionSpy).toBeCalledWith({ patient, plan, paymentProvider, data }) }) }) }) }) describe('#getActiveAddon()', () => { let person: Person let canBeInCancellation: boolean let addons: Array beforeAll(async () => { person = await factory.build('smartfit:person') }) beforeEach(async () => { jest.spyOn(subscriptionService, 'getAllowAddons').mockResolvedValue(addons) }) afterAll(() => { jest.restoreAllMocks() }) describe('when does not find allowed addons', () => { beforeAll(() => { addons = [] }) it('returns undefined', async () => { expect(await subscriptionService.getActiveAddon(person, canBeInCancellation)).toBeUndefined() }) }) describe('when finds allowed addons', () => { describe('and doesn`t want addon in cancellation', () => { beforeAll(() => { canBeInCancellation = false }) describe('and has an addon in cancellation', () => { beforeAll(async () => { addons = [await factory.build('addon:in_cancellation')] }) it('doesn`t return the addon', async () => { expect(await subscriptionService.getActiveAddon(person, canBeInCancellation)).toBeUndefined() }) }) describe('and has a valid addon', () => { beforeAll(async () => { addons = [await factory.build('addon:active')] }) it('returns the addon', async () => { expect(await subscriptionService.getActiveAddon(person, canBeInCancellation)).toBeInstanceOf(Addon) }) }) }) describe('and also wants addons in cancellation', () => { beforeAll(() => { canBeInCancellation = true }) describe.each` status | returns ${'active'} | ${true} ${'trial'} | ${true} ${'in_cancellation'} | ${true} ${'trial_expired'} | ${true} ${'cancelled'} | ${false} `('and addon status is "$status"', ({ status, returns }) => { beforeAll(async () => { addons = [await factory.build(`addon:${status}`)] }) const assertion = returns ? 'returns' : 'does not return' it(assertion, async () => { const result = await subscriptionService.getActiveAddon(person, canBeInCancellation) if (returns) return expect(result).not.toBeUndefined() return expect(result).toBeUndefined() }) }) describe('and has more than one addon', () => { describe('and have different createdAt', () => { let addonToday: Addon let addonYesterday: Addon beforeAll(async () => { addonToday = await factory.build('addon:active', { createdAt: new Date() }) addonYesterday = await factory.build('addon:active', { createdAt: date.past(1) }) addons = [addonToday, addonYesterday] }) it('returns the newest creation date', async () => { const result = await subscriptionService.getActiveAddon(person, canBeInCancellation) expect(result.createdAt).toEqual(addonToday.createdAt) }) }) describe.each` firstAddonStatus | secondAddonStatus | returns ${'active'} | ${'active'} | ${'active'} ${'active'} | ${'trial'} | ${'active'} ${'active'} | ${'in_cancellation'} | ${'active'} ${'active'} | ${'cancelled'} | ${'active'} ${'trial'} | ${'active'} | ${'active'} ${'trial'} | ${'trial'} | ${'trial'} ${'trial'} | ${'in_cancellation'} | ${'trial'} ${'trial'} | ${'cancelled'} | ${'trial'} ${'in_cancellation'} | ${'active'} | ${'active'} ${'in_cancellation'} | ${'trial'} | ${'trial'} ${'in_cancellation'} | ${'in_cancellation'} | ${'in_cancellation'} ${'in_cancellation'} | ${'cancelled'} | ${'in_cancellation'} ${'cancelled'} | ${'active'} | ${'active'} ${'cancelled'} | ${'trial'} | ${'trial'} ${'cancelled'} | ${'in_cancellation'} | ${'in_cancellation'} ${'cancelled'} | ${'cancelled'} | ${undefined} `( 'and first addon is "$firstAddonStatus" and second addon is "$secondAddonStatus"', ({ firstAddonStatus, secondAddonStatus, returns }) => { beforeAll(async () => { const createdAt = new Date() addons = [ await factory.build(`addon:${firstAddonStatus}`, { createdAt }), await factory.build(`addon:${secondAddonStatus}`, { createdAt }), ] }) it(`returns an addon with "${returns}" status`, async () => { const result = await subscriptionService.getActiveAddon(person, canBeInCancellation) expect(returns === undefined ? result : result.status).toEqual(returns) }) }, ) }) }) }) }) describe('#checkAddon', () => { let patient: Patient let plan: Plan let addon: Addon let person: Person let getPersonSky: jest.SpyInstance let getAllowAddonsSky: jest.SpyInstance beforeAll(async () => { person = await factory.build('smartfit:person') patient = await factory.build('patient') plan = await factory.build('plan:smartfit') }) beforeEach(async () => { getPersonSky = jest.spyOn(subscriptionService, 'getPerson').mockResolvedValue(person) getAllowAddonsSky = jest.spyOn(subscriptionService, 'getAllowAddons').mockResolvedValue([addon]) smartfitApi.addAddon.mockResolvedValue(null) }) afterAll(() => { jest.restoreAllMocks() }) describe('when user has an active addon', () => { beforeAll(async () => { addon = await factory.build('addon:active') }) describe('and addon isn`t in cancellation and isn`t the same', () => { it('throws an error', async () => { await expect(subscriptionService.checkAddon(patient, plan)).rejects.toThrowError(SubscribeError) }) it('not adds the addon', async () => { try { await subscriptionService.checkAddon(patient, plan) } catch (error) { expect(smartfitApi.addAddon).not.toBeCalled() } }) }) describe('and addon is in cancellation and isn`t the same', () => { beforeAll(() => { addon.expiresAt = new Date() }) it('adds the addon', async () => { await subscriptionService.checkAddon(patient, plan) expect(smartfitApi.addAddon).toBeCalled() }) }) }) describe('when user has no active addon', () => { beforeAll(async () => { addon = await factory.build('addon:cancelled') }) it('it`s try get person', async () => { await subscriptionService.checkAddon(patient, plan) expect(getPersonSky).toBeCalled() }) it('it`s try get addons', async () => { await subscriptionService.checkAddon(patient, plan) expect(getAllowAddonsSky).toBeCalled() }) it('adds the addon by smartfit API', async () => { await subscriptionService.checkAddon(patient, plan) expect(smartfitApi.addAddon).toBeCalled() }) }) }) describe('#addSubscription', () => { let patient: Patient let plan: Plan let paymentProvider: { provider: PaymentProviders } let subscriptionId: string let options: Partial beforeAll(async () => { patient = await factory.build('patient') plan = await factory.build('plan') paymentProvider = { provider: plan.payment_providers[0].provider } subscriptionId = datatype.uuid() MockDate.set(new Date()) }) beforeEach(async () => { subscriptionRepository.create.mockResolvedValue(subscriptionId) patientRepository.updateSubscription.mockResolvedValue(null) }) afterAll(() => { jest.restoreAllMocks() MockDate.reset() }) describe('when plan has teleconsultation credits', () => { beforeAll(async () => { const planFeature = await factory.build('plan_feature', { feature: 'teleconsultation' }) plan = await factory.build('plan', { features: [planFeature], }) }) it('creates a new subscription', async () => { await subscriptionService.addSubscription({ patient, plan, paymentProvider, data: { origin: sample(['n2b', 'smartfit']) }, }) expect(subscriptionRepository.create).toBeCalledWith( expect.objectContaining({ createdAt: expect.anything(), patientId: patient.id, planId: plan.id, teleconsultationRenewsAt: addMonths(new Date(), plan.getFeature('teleconsultation').intervalInMonths), ...(!isEmpty(plan.trial_value) ? { trialExpiresAt: plan.trialExpiresAtFromNow } : {}), }), ) }) it('adds teleconsultation credits', async () => { await subscriptionService.addSubscription({ patient, plan, paymentProvider, data: { origin: sample(['n2b', 'smartfit']) }, }) expect(teleconsultationsCreditService.createTransaction).toHaveBeenCalledWith({ patientId: patient.id, value: plan.getFeature('teleconsultation').quantity, reason: 'plan_subscription', author: patient, objectModel: expect.objectContaining({ id: subscriptionId }), }) }) }) describe('when plan hasn`t teleconsultation credits', () => { beforeAll(async () => { plan = await factory.build('plan', { teleconsultations_value: undefined, }) }) it('creates a new subscription', async () => { await subscriptionService.addSubscription({ patient, plan, paymentProvider, data: { origin: sample(['n2b', 'smartfit']) }, }) expect(subscriptionRepository.create).toBeCalledWith( expect.objectContaining({ createdAt: expect.anything(), patientId: patient.id, planId: plan.id, ...(!isEmpty(plan.trial_value) ? { trialExpiresAt: plan.trialExpiresAtFromNow } : {}), }), ) }) }) it("updates the patient's current subscription", async () => { await subscriptionService.addSubscription({ patient, plan, paymentProvider, data: { origin: sample(['n2b', 'smartfit']) }, }) expect(patientRepository.updateSubscription).toBeCalledWith(patient, subscriptionId) }) it('returns the new subscription', async () => { const periodUnit = plan.trial_interval === Intervals.YEARLY ? 'years' : plan.trial_interval === Intervals.MONTHLY ? 'months' : 'days' expect( await subscriptionService.addSubscription({ patient, plan, paymentProvider, data: { origin: sample(['n2b', 'smartfit']) }, }), ).toEqual( expect.objectContaining({ createdAt: expect.anything(), patientId: patient.id, planId: plan.id, id: subscriptionId, ...(plan.trial_value ? { trialExpiresAt: add(new Date(), { [periodUnit]: plan.trial_value }) } : {}), }), ) }) describe('when options is gived', () => { let now: Date beforeAll(() => { now = new Date() options = { trialExpiresAt: now } }) it('saves the information passed by options', async () => { await subscriptionService.addSubscription({ patient, plan, paymentProvider, options, data: { origin: sample(['n2b', 'smartfit']) }, }) expect(subscriptionRepository.create).toBeCalledWith( expect.objectContaining({ trialExpiresAt: now, }), ) }) }) it('stores the discount coupon if it was used', async () => { const discountCoupon = await factory.build('discount_coupon') await subscriptionService.addSubscription( { patient, plan, paymentProvider, data: { origin: sample(['n2b', 'smartfit']) } }, discountCoupon, ) expect(subscriptionRepository.create).toBeCalledWith( expect.objectContaining({ discountCouponId: discountCoupon.id, }), ) }) }) describe('#addInstallment', () => { let subscription: Subscription let installments: Installment[] beforeAll(async () => { subscription = await factory.build('subscription') installments = [ { id: 1, installment_number: 1, due_date: new Date(), amount: 15 } as Installment, { id: 2, installment_number: 2, due_date: new Date(), amount: 16 } as Installment, ] }) it('creates installments', async () => { await subscriptionService.addInstallments(installments, subscription) expect(subscriptionInstallmentRepository.create).toBeCalledWith( expect.arrayContaining([ expect.objectContaining({ subscription_id: subscription.id, amount: installments[0].amount, due_date: installments[0].due_date, installment: installments[0].installment_number, }), expect.objectContaining({ subscription_id: subscription.id, amount: installments[1].amount, due_date: installments[1].due_date, installment: installments[1].installment_number, }), ]), ) }) }) describe('#getAllowAddons', () => { let addons: Array = [] let person: Person let plans: Array = [] beforeAll(async () => { person = await factory.build('smartfit:person') }) beforeEach(() => { smartfitApi.findAddons.mockResolvedValue(addons) planRepository.findEnabled.mockResolvedValue(plans) }) describe('when non exists plans', () => { beforeAll(() => { plans = [] }) it('returns an empty array', async () => { const result = await subscriptionService.getAllowAddons(person) expect(result.length).toBeLessThanOrEqual(0) }) }) describe('when exists plans', () => { beforeAll(async () => { plans = [await factory.build('plan:smartfit')] }) describe('when person hasn`t addons', () => { beforeAll(() => { addons = [] }) it('returns empty array', async () => { const result = await subscriptionService.getAllowAddons(person) expect(result.length).toBeLessThanOrEqual(0) }) }) describe('when person has addons', () => { beforeAll(async () => { addons = [await factory.build('addon'), await factory.build('addon')] }) describe('when has not a plan with same addon', () => { it('returns empty array', async () => { const result = await subscriptionService.getAllowAddons(person) expect(result.length).toBeLessThanOrEqual(0) }) }) describe('when has plan with same addon', () => { beforeAll(() => { addons[0].addonId = plans[0].getPaymentProvider(PaymentProviders.SMARTFIT).smartfit_id }) it("returns addons' array", async () => { const result = await subscriptionService.getAllowAddons(person) expect(result).toEqual(expect.arrayContaining([addons[0]])) }) }) }) }) }) describe('#getPerson', () => { let person: Person const login = datatype.uuid() beforeEach(() => { smartfitApi.getPersonByLogin.mockResolvedValue(person) }) describe('when person non exists', () => { beforeAll(async () => { person = null }) it('throws an error', async () => { await expect(subscriptionService.getPerson(login)).rejects.toThrowError(Error) }) }) describe('when person exists', () => { describe('when person can purchase addon', () => { beforeAll(async () => { person = await factory.build('smartfit:person', { canPurchaseAddon: true, }) }) it('returns person', async () => { expect(await subscriptionService.getPerson(login)).toEqual(person) }) }) }) }) describe('#cancelSubscription', () => { let patient: Patient let addon: Addon let cancellationReason: CancellationReason = { id: CancellationsReasons.CONCILIATION } let installments: SubscriptionInstallment[] const cancellationBy: CancellationUser = { id: datatype.uuid() } const amount = datatype.number({ min: 1 }) beforeAll(() => { installments = [] }) beforeEach(async () => { patient = await factory.build('patient:subscriber') subscriptionInstallmentRepository.findBySubscription.mockResolvedValue(installments) }) describe('when subscription is already cancelled', () => { beforeEach(async () => { patient.subscription.cancellationAt = date.past() patient.subscription.cancellationRequestAt = date.past() }) it('throws an error', async () => { await expect( subscriptionService.cancelSubscription({ patient, cancellationReason, cancellationBy }), ).rejects.toThrowError(UnprocessableEntityError) }) }) describe('when addon has an expiration date', () => { beforeAll(async () => { addon = await factory.build('addon:in_cancellation') }) it('cancels the subscription with expiresAt', async () => { await subscriptionService.cancelSubscription({ patient, cancellationReason, cancellationBy, expiresAt: addon.expiresAt, }) expect(subscriptionRepository.cancel).toBeCalledWith( expect.objectContaining({ expiresAt: addon.expiresAt, }), cancellationReason, cancellationBy.id, ) }) }) describe('when cancellations reason is upgrade', () => { beforeAll(() => { cancellationReason = { id: CancellationsReasons.PLAN_REPLACEMENT } }) it('cancels the subscription with cancellationAt', async () => { await subscriptionService.cancelSubscription({ patient, cancellationReason, cancellationBy, expiresAt: addon.expiresAt, }) expect(subscriptionRepository.cancel).toBeCalledWith( expect.objectContaining({ cancellationAt: expect.any(Date), }), cancellationReason, cancellationBy.id, ) }) }) describe('when subscription is not N2B', () => { beforeEach(async () => { patient.subscription.paymentProvider = sample( Object.values(PaymentProviders).filter(provider => provider !== PaymentProviders.N2B), ) }) it('does not check subscription installments', async () => { await expect(subscriptionInstallmentRepository.findBySubscription).not.toBeCalled() }) }) describe('when subscription is N2B', () => { beforeEach(() => { patient.subscription.paymentProvider = PaymentProviders.N2B }) describe('when subscription has no installments', () => { beforeAll(async () => { installments = [] }) it('calls askRefund service', async () => { await subscriptionService.cancelSubscription({ patient, cancellationReason, cancellationBy, amount }) expect(n2bPaymentsService.askRefund).toBeCalled() }) }) describe('when subscription has installments', () => { beforeAll(async () => { installments = await factory.buildMany('subscription_installment', datatype.number({ min: 1, max: 10 })) }) it('calls askCancelSubscription service', async () => { await subscriptionService.cancelSubscription({ patient, cancellationReason, cancellationBy, amount }) expect(n2bPaymentsService.askCancelSubscription).toBeCalled() }) }) }) describe('when patient never was subscriber', () => { beforeEach(async () => { patient = await factory.build('patient') }) it('throws an error', async () => { await expect( subscriptionService.cancelSubscription({ patient, cancellationReason, cancellationBy }), ).rejects.toThrowError(UnprocessableEntityError) }) }) }) describe('#getToken', () => { const token: string = datatype.uuid() let subscription: Subscription beforeAll(async () => { subscription = await factory.build('subscription', { token }) }) describe('when subscription gives already contains token', () => { it('does not call repository', async () => { await subscriptionService.getToken(subscription) expect(subscriptionRepository.getToken).not.toBeCalled() }) it('returns subscription token', async () => { await expect(subscriptionService.getToken(subscription)).resolves.toEqual(token) }) }) describe('when subscription gives does not contains token', () => { beforeAll(() => { subscription.token = undefined }) beforeEach(() => { subscriptionRepository.getToken.mockResolvedValue(token) }) it('calls repository', async () => { await subscriptionService.getToken(subscription) expect(subscriptionRepository.getToken).toBeCalled() }) it('returns subscription token', async () => { await expect(subscriptionService.getToken(subscription)).resolves.toEqual(token) }) }) }) describe('#buildN2BPaymentsParams', () => { let patient: Patient let plan: Plan let data: SubscriptionData let card: CardParams beforeAll(async () => { patient = await factory.build('patient') plan = await factory.build('plan:n2b', { interval: sample(['monthly', 'quarterly', 'semesterly', 'yearly']) }) data = { origin: sample(['smartfit', 'n2b']), sales_channel: 'communication_guideline', smartfit_unit_id: datatype.number(), } card = { token: datatype.uuid(), kind: sample(['credit', 'debit']), } MockDate.set(new Date()) }) afterAll(() => { MockDate.reset() }) it('buils params schema', async () => { await expect(subscriptionService['buildN2BPaymentsParams']({ patient, plan, data, card })).resolves.toEqual({ amount: plan.getPaymentProvider(PaymentProviders.N2B).amount, externalReferenceId: plan.id, period: IntervalsHelper.IntervalsToN2BBillingPeriods(plan.interval), issuedAt: new Date(), customer: { name: patient.name, email: patient.email, document: patient.document_number, phone: patient.cell_phone, country_code: CountryHelper.extractOnlyCountryCode(CountryHelper.parseCode(patient.country_code)), currency: patient.country_code === 'pt_BR' ? Currency.BRL : Currency.MXN, external_id: patient.id, }, card: { ...card, installments: 1, }, installmentsQuantity: IntervalsHelper.IntervalsToCardInstallments(plan.interval), data, }) }) describe.each` tokenKind | success ${'credit'} | ${true} ${'debit'} | ${true} ${'any'} | ${false} `('when token kind is "$tokenKind"', ({ tokenKind, success }) => { beforeAll(() => { card.kind = tokenKind }) if (success) it('generates params', async () => { await expect(subscriptionService['buildN2BPaymentsParams']({ patient, plan, data, card })).resolves.toEqual( expect.objectContaining({ card: expect.objectContaining(card), }), ) }) else it('throws an error', async () => { await expect( subscriptionService['buildN2BPaymentsParams']({ patient, plan, data, card }), ).rejects.toThrowError(TokenKindDisallowedError) }) }) describe.each` intervals | period | installments ${Intervals.MONTHLY} | ${BillingPeriod.MONTH} | ${1} ${Intervals.QUARTERLY} | ${BillingPeriod.QUARTER} | ${3} ${Intervals.SEMESTERLY} | ${BillingPeriod.SEMESTER} | ${6} ${Intervals.YEARLY} | ${BillingPeriod.YEAR} | ${12} `('when plan interval is "$intervals"', ({ intervals, period, installments }) => { beforeAll(() => { plan.interval = intervals card.kind = 'credit' }) it(`returns with ${period} period`, async () => { await expect(subscriptionService['buildN2BPaymentsParams']({ patient, plan, data, card })).resolves.toEqual( expect.objectContaining({ period }), ) }) it(`returns with ${installments} installments`, async () => { await expect(subscriptionService['buildN2BPaymentsParams']({ patient, plan, data, card })).resolves.toEqual( expect.objectContaining({ card: expect.objectContaining({ installments: 1 }), installmentsQuantity: installments, }), ) }) }) describe('when token is not given', () => { const smartToken = datatype.uuid() const smartKind = sample(['credit', 'debit']) const smartPersonToken = datatype.uuid() beforeAll(() => { card.token = null }) beforeEach(() => { smartfitApi.getByLogin.mockResolvedValue({ token: smartPersonToken }) }) beforeEach(() => { smartfitApi.getWalletToken.mockResolvedValue({ token: smartToken, kind: smartKind }) }) it('fetchs token from smartfit api', async () => { await subscriptionService['buildN2BPaymentsParams']({ patient, plan, data, card }) expect(smartfitApi.getWalletToken).toBeCalled() }) it('builds params with fetched token', async () => { await subscriptionService['buildN2BPaymentsParams']({ patient, plan, data, card }) await expect(subscriptionService['buildN2BPaymentsParams']({ patient, plan, data, card })).resolves.toEqual( expect.objectContaining({ card: expect.objectContaining({ token: smartToken, kind: smartKind, }), }), ) }) }) }) describe('#processN2bPaymentsSubscriptionNow', () => { const paymentProvider = { provider: PaymentProviders.N2B } const smartToken = datatype.uuid() const smartPersonToken = datatype.uuid() let patient: Patient let person: Person let plan: Plan let addSubscriptionSpy: jest.SpyInstance beforeAll(async () => { patient = await factory.build('patient') plan = await factory.build('plan:n2b') person = await factory.build('smartfit:person') addSubscriptionSpy = jest.spyOn(subscriptionService, 'addSubscription') }) beforeEach(() => { smartfitApi.getByLogin.mockResolvedValue({ token: smartPersonToken }) smartfitApi.getWalletToken.mockResolvedValue({ token: smartToken, kind: 'credit' }) }) afterAll(() => { addSubscriptionSpy.mockRestore() }) describe('when a discount coupon is used', () => { let discountCoupon: DiscountCoupon describe('and it does not exists', () => { beforeEach(() => { discountCouponRepository.getByCode.mockResolvedValueOnce(null) }) it('throws an not found error', async () => { await expect( subscriptionService['processN2bPaymentsSubscriptionNow']({ patient, plan, paymentProvider, options: {}, data: { origin: sample(['n2b', 'smartfit']) }, discountCouponCode: 'CINQUENTAO', }), ).rejects.toThrowError(DiscountCouponNotFoundError) }) }) describe('and it is invalid', () => { beforeEach(async () => { discountCoupon = await factory.build('discount_coupon:expired') discountCouponRepository.getByCode.mockResolvedValueOnce(discountCoupon) }) it('throws an invalid error', async () => { await expect( subscriptionService['processN2bPaymentsSubscriptionNow']({ patient, plan, paymentProvider, options: {}, data: { origin: sample(['n2b', 'smartfit']) }, discountCouponCode: discountCoupon.code, }), ).rejects.toThrowError(DiscountCouponInvalidError) }) }) }) describe('when payment is successed', () => { const subscription = { id: datatype.number({ min: 1 }), expires_at: new Date(), invoice: { installments: [ { installment_number: 1, amount: datatype.number(1000), paid_at: new Date(), payments: [ { id: datatype.number({ min: 1 }), amount: datatype.number(1000), paid_at: new Date(), transaction: { card: { last4: '0000' } }, }, ], }, ], }, } let discountCoupon: DiscountCoupon const subject = async () => { await subscriptionService['processN2bPaymentsSubscriptionNow']({ patient, plan, paymentProvider, options: {}, data: { origin: sample(['n2b', 'smartfit']) }, discountCouponCode: discountCoupon?.code, }) } beforeEach(() => { n2bPaymentsService.generateSubscriptionAndChargePayment.mockResolvedValue({ success: true, subscription, }) smartfitApi.getPersonByLogin.mockResolvedValue(person) }) it('creates a subscription', async () => { await subject() expect(addSubscriptionSpy).toBeCalledWith( expect.objectContaining({ options: expect.objectContaining({ expiresAt: subscription.expires_at, originalTransaction: subscription.id.toString(), }), }), undefined, ) }) it('create installments', async () => { await subject() expect(subscriptionInstallmentRepository.create).toBeCalledWith( expect.arrayContaining([ expect.objectContaining({ amount: subscription.invoice.installments[0].amount, installment: subscription.invoice.installments[0].installment_number, paid_at: subscription.invoice.installments[0].paid_at, }), ]), ) }) it('logs transaction', async () => { await subject() expect(subscriptionTransactionRepository.create).toBeCalledWith( expect.objectContaining({ status: 'success', payment_provider: PaymentProviders.N2B, card_type: 'credit', }), ) }) describe('and valid discount coupon was used', () => { let generateSubscriptionAndChargePaymentSpy: jest.SpyInstance beforeEach(async () => { generateSubscriptionAndChargePaymentSpy = jest.spyOn( n2bPaymentsService, 'generateSubscriptionAndChargePayment', ) discountCoupon = await factory.build('discount_coupon', { planDiscountCoupons: [{ plan_id: plan.id }], }) discountCouponRepository.getByCode.mockResolvedValueOnce(discountCoupon) }) afterEach(() => { generateSubscriptionAndChargePaymentSpy.mockRestore() }) it('applies the discount coupon', async () => { const amount = discountCoupon.apply(plan.getPaymentProvider(PaymentProviders.N2B).amount) await subject() expect(generateSubscriptionAndChargePaymentSpy).toBeCalledWith( expect.objectContaining({ amount, }), ) }) it('forwards the discount coupon', async () => { await subject() expect(addSubscriptionSpy).toBeCalledWith(expect.anything(), discountCoupon) }) }) }) describe('when payment is failed', () => { beforeEach(() => { n2bPaymentsService.generateSubscriptionAndChargePayment.mockResolvedValueOnce({ success: false, subscription: { invoice: { installments: [ { installment_number: 1, payments: [ { id: datatype.number({ min: 1 }), amount: 0, transaction: { error: { code: '01', message: 'Error msg' } }, }, ], }, ], }, }, }) }) it('does not create a subscription', async () => { try { await subscriptionService['processN2bPaymentsSubscriptionNow']({ patient, plan, paymentProvider, options: {}, data: { origin: sample(['n2b', 'smartfit']) }, }) } catch (_error) {} expect(addSubscriptionSpy).not.toBeCalled() }) it('does not create installments', async () => { try { await subscriptionService['processN2bPaymentsSubscriptionNow']({ patient, plan, paymentProvider, options: {}, data: { origin: sample(['n2b', 'smartfit']) }, }) } catch (_error) {} expect(subscriptionInstallmentRepository.create).not.toBeCalled() }) it('logs transaction', async () => { try { await subscriptionService['processN2bPaymentsSubscriptionNow']({ patient, plan, paymentProvider, options: {}, data: { origin: sample(['n2b', 'smartfit']) }, }) } catch (_error) {} expect(subscriptionTransactionRepository.create).toBeCalledWith( expect.objectContaining({ status: 'failed', payment_provider: PaymentProviders.N2B, card_type: 'credit', error_code: '01', error_message: 'Error msg', }), ) }) }) }) describe('#addSubscriptionWithN2BPayments', () => { const data: N2BPaymentProviderData = { card: { token: datatype.uuid(), kind: sample(['credit', 'debit']), }, discount: 0, unitId: datatype.number(), source: 'smartfit', } const paymentProvider = { provider: PaymentProviders.N2B, data } let patient: Patient let plan: Plan let processN2bPaymentsSubscriptionNowSpy: jest.SpyInstance let origin: OriginRequest beforeAll(async () => { patient = await factory.build('patient') plan = await factory.build('plan:n2b') processN2bPaymentsSubscriptionNowSpy = jest.spyOn( subscriptionService, 'processN2bPaymentsSubscriptionNow', ) }) beforeEach(() => { processN2bPaymentsSubscriptionNowSpy.mockResolvedValue({}) }) describe('when request is from smartfit', () => { beforeAll(() => { origin = 'smartfit' }) it('processes the payment with no waiting it', async () => { await subscriptionService['addSubscriptionWithN2BPayments']({ patient, plan, paymentProvider, options: {}, data: { origin }, }) expect(n2bPaymentsService.generateSubscription).toBeCalled() }) }) describe('when request is from n2b', () => { beforeAll(() => { origin = 'n2b' }) it('processes the payment with no waiting it', async () => { await subscriptionService['addSubscriptionWithN2BPayments']({ patient, plan, paymentProvider, options: {}, data: { origin }, }) expect(processN2bPaymentsSubscriptionNowSpy).toBeCalled() }) }) }) describe('#subscribeN2BPaymentWithFallbackToSmarfit', () => { const data: N2BPaymentProviderData = { card: { token: datatype.uuid(), kind: sample(['credit', 'debit']), }, discount: 0, unitId: datatype.number(), source: 'smartfit', } const paymentProvider = { provider: PaymentProviders.N2B, data } let patient: Patient let plan: Plan let addSubscriptionWithN2BPaymentsSpy: jest.SpyInstance let addSubscriptionWithAddonSpy: jest.SpyInstance let origin: OriginRequest beforeAll(async () => { patient = await factory.build('patient') plan = await factory.build('plan:n2b') addSubscriptionWithN2BPaymentsSpy = jest.spyOn(subscriptionService, 'addSubscriptionWithN2BPayments') addSubscriptionWithAddonSpy = jest.spyOn(subscriptionService, 'addSubscriptionWithAddon') }) afterEach(() => { addSubscriptionWithN2BPaymentsSpy.mockReset() addSubscriptionWithAddonSpy.mockReset() }) describe('when request is from smartfit', () => { beforeAll(() => { origin = 'smartfit' }) it('processes the payment with N2B', async () => { await subscriptionService['subscribeN2BPaymentWithFallbackToSmarfit']({ patient, plan, paymentProvider, options: {}, data: { origin }, }) expect(addSubscriptionWithN2BPaymentsSpy).toBeCalled() }) describe('when n2b throws an error', () => { beforeEach(() => { addSubscriptionWithN2BPaymentsSpy.mockRejectedValueOnce(new Error('Error msg')) }) it('throws an error', async () => { await expect( subscriptionService['subscribeN2BPaymentWithFallbackToSmarfit']({ patient, plan, paymentProvider, options: {}, data: { origin }, }), ).rejects.toThrowError() }) }) }) describe('when request is from n2b', () => { beforeAll(() => { origin = 'n2b' }) it('processes the payment with N2B', async () => { await subscriptionService['subscribeN2BPaymentWithFallbackToSmarfit']({ patient, plan, paymentProvider, options: {}, data: { origin }, }) expect(addSubscriptionWithN2BPaymentsSpy).toBeCalled() }) describe('when n2b throws an error', () => { beforeEach(() => { addSubscriptionWithN2BPaymentsSpy.mockRejectedValueOnce(new Error('Error msg')) }) it('tries processes the payment with smartfit', async () => { await subscriptionService['subscribeN2BPaymentWithFallbackToSmarfit']({ patient, plan, paymentProvider, options: {}, data: { origin }, }) expect(addSubscriptionWithAddonSpy).toBeCalledWith({ patient, plan, paymentProvider: { ...paymentProvider, provider: PaymentProviders.SMARTFIT }, data: { origin }, options: {}, }) }) }) }) }) })