import { datatype } from 'faker' import { cloneDeep, sample } from 'lodash' import { TestingModule, Test } from '@nestjs/testing' import { factory } from '../../../../test/factories' import { PaymentMethods, Person, SmartfitApi } from '../../../services/smartfit' import { RequiredArgumentError, UserNotFoundError } from '../../../errors' import { OS } from '../../../params/enums/os.enum' import { CountryCode, countryCodes } from '../../../utils' import { MockedModule } from '../../../../test/types' import { PatientRepository, PlanRepository, PlanFeatureRepository, DiscountCouponRepository, } from '../../../repositories' import { DiscountTypes } from '../../../models/enums/discount_type.enum' import { DEFAULT_INSTALLMENTS_AMOUNT, DiscountCoupon, Intervals, Patient, PaymentProviders, Plan as PlanModel, PlanFeature, } from './../../../models' import { Plan } from './plan' globalThis.secret = { n2b_payments: { enable: false, }, } as any describe('Plan', () => { let subject: Plan let patientRepository: MockedModule let planRepository: MockedModule let smartfitApi: MockedModule let planFeatureRepository: MockedModule let discountCouponRepository: MockedModule beforeAll(async () => { patientRepository = { get: jest.fn(), } planRepository = { findUpgradeablePlansByPlanId: jest.fn(), findEnabledAndVisible: jest.fn(), } smartfitApi = { getPersonByLogin: jest.fn(), } planFeatureRepository = { findByCountryCode: jest.fn(), findByPlanId: jest.fn(), } discountCouponRepository = { getByCode: jest.fn(), } const app: TestingModule = await Test.createTestingModule({ controllers: [], providers: [ { provide: PlanRepository, useValue: planRepository, }, { provide: SmartfitApi, useValue: smartfitApi, }, { provide: PlanFeatureRepository, useValue: planFeatureRepository, }, { provide: PatientRepository, useValue: patientRepository, }, { provide: DiscountCouponRepository, useValue: discountCouponRepository, }, Plan, ], }).compile() subject = app.get(Plan) }) afterEach(() => { jest.clearAllMocks() }) afterAll(() => { jest.restoreAllMocks() }) describe('#getAvailableFeatures', () => { let countryCode: CountryCode let planFeatures: Array beforeAll(() => { countryCode = sample(countryCodes) }) beforeEach(() => { planFeatureRepository.findByCountryCode.mockResolvedValue(planFeatures) }) describe('when has features for a provided country', () => { beforeAll(async () => { const number = datatype.number({ min: 1, max: 10 }) const features = await factory.buildMany('feature', number) planFeatures = (await factory.buildMany('plan_feature', number)).map( (planFeature: PlanFeature, index: number) => { planFeature.country_code = countryCode planFeature.feature = features[index].value planFeature.plan_feature = features[index] return planFeature }, ) }) it('return a list of features', async () => { const features = await subject.getAvailableFeatures(countryCode) expect(features).toEqual( planFeatures.map(planFeature => ({ feature: planFeature.feature, description: planFeature.description, })), ) }) }) }) describe('#fetchPlans()', () => { let patient: Patient let person: Person let plans: Array let features: Array let version: string let os: OS = OS.ANDROID beforeEach(async () => { patientRepository.get.mockResolvedValue(patient) smartfitApi.getPersonByLogin.mockResolvedValue(person) planRepository.findUpgradeablePlansByPlanId.mockResolvedValue(cloneDeep(plans)) planRepository.findEnabledAndVisible.mockResolvedValue(cloneDeep(plans)) planFeatureRepository.findByPlanId.mockResolvedValue(features) }) describe('when patient non exists', () => { beforeAll(() => { patient = null }) it('thows error', async () => { await expect(subject.fetchPlans(datatype.uuid(), version, os)).rejects.toBeInstanceOf(UserNotFoundError) }) }) describe('when patient exists', () => { beforeAll(async () => { patient = await factory.build('patient') plans = [] features = [] }) it('try to get patient', async () => { beforeAll(async () => { patient = await factory.build('patient') }) await subject.fetchPlans(datatype.uuid(), version, os) expect(patientRepository.get).toBeCalled() }) it('try to get smartfit person', async () => { await subject.fetchPlans(datatype.uuid(), version, os) expect(smartfitApi.getPersonByLogin).toBeCalled() }) describe('when smartfit API throws exception', () => { beforeAll(async () => { plans = [await factory.build('plan')] plans[0].payment_providers[0].provider = PaymentProviders.GOOGLE plans[0].payment_providers[0].country_code = patient.country_code plans[0].payment_providers.push( await factory.build('plan_payment_provider:smartfit', { country_code: patient.country_code }), ) os = OS.ANDROID }) beforeEach(() => { smartfitApi.getPersonByLogin.mockRejectedValue(new Error()) }) it('returns plans with InApp paymentProvider', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.GOOGLE).code, payment_provider: PaymentProviders.GOOGLE, }), ]), ) }) }) describe('when is already a subscriber', () => { beforeAll(async () => { patient = await factory.build('patient:subscriber') }) it('try to get upgradeable plans from your current plan', async () => { await subject.fetchPlans(datatype.uuid(), version, os) expect(planRepository.findUpgradeablePlansByPlanId).toBeCalled() }) }) describe('when is not already a subscriber', () => { beforeAll(async () => { patient.subscription = undefined }) it('try to get plans', async () => { await subject.fetchPlans(datatype.uuid(), version, os) expect(planRepository.findEnabledAndVisible).toBeCalled() }) }) describe('when does not have plans', () => { beforeAll(async () => { plans = [] }) it('returns empty array', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result.length).toBeLessThanOrEqual(0) }) }) describe('when has plans', () => { describe('when plans has version gte', () => { let planInvalid: PlanModel let planValid: PlanModel let planNeutral: PlanModel beforeAll(async () => { os = OS.ANDROID version = '1.0.0' planInvalid = await factory.build('plan', { version_gte: '2.0.0' }) planNeutral = await factory.build('plan', { version_gte: undefined }) planValid = await factory.build('plan', { version_gte: '1.0.0' }) plans = [planInvalid, planNeutral, planValid].map(plan => { plan.payment_providers[0].provider = PaymentProviders.GOOGLE plan.payment_providers[0].country_code = patient.country_code return plan }) }) it('returns only valid and neutral plans by given version', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ id: planValid.id, }), expect.objectContaining({ id: planNeutral.id, }), ]), ) }) it(`doesn't return invalid plans by given version`, async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).not.toEqual( expect.arrayContaining([ expect.objectContaining({ id: planInvalid.id, }), ]), ) }) }) describe('when is not a smartfit user', () => { beforeAll(async () => { plans = [await factory.build('plan')] }) describe('when os is not given', () => { beforeAll(() => { os = undefined plans[0].payment_providers[0].provider = PaymentProviders.GOOGLE }) it('throws error', async () => { await expect(subject.fetchPlans(datatype.uuid(), version, os)).rejects.toBeInstanceOf(RequiredArgumentError) }) }) describe('when os is Android', () => { beforeAll(() => { os = OS.ANDROID }) describe('when there is not android payment provider', () => { beforeAll(async () => { plans[0].payment_providers = [ await factory.build('plan_payment_provider:smartfit', { country_code: patient.country_code }), ] }) it('returns empty', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual([]) }) describe('when there is android payment provider', () => { beforeAll(async () => { plans[0].payment_providers[0].provider = PaymentProviders.GOOGLE plans[0].payment_providers.push( await factory.build('plan_payment_provider:smartfit', { country_code: patient.country_code }), ) }) it('returns plans with paymentProvider Google', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.GOOGLE).code, payment_provider: PaymentProviders.GOOGLE, }), ]), ) }) }) describe('when n2b payments is enabled', () => { beforeAll(async () => { globalThis.secret.n2b_payments.enable = true plans[0].payment_providers[0].provider = PaymentProviders.GOOGLE plans[0].payment_providers.push( await factory.build('plan_payment_provider:smartfit', { country_code: patient.country_code }), ) }) afterAll(() => { globalThis.secret.n2b_payments.enable = false }) it('returns plans with paymentProvider Google', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.GOOGLE).code, payment_provider: PaymentProviders.GOOGLE, }), ]), ) }) }) }) describe('when os is iOS', () => { beforeAll(() => { os = OS.IOS }) describe('when there is not apple payment provider', () => { beforeAll(async () => { plans[0].payment_providers = [ await factory.build('plan_payment_provider:smartfit', { country_code: patient.country_code }), ] }) it('returns empty', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual([]) }) }) describe('when there is apple payment provider', () => { beforeAll(async () => { plans[0].payment_providers[0].provider = PaymentProviders.APPLE plans[0].payment_providers.push( await factory.build('plan_payment_provider:smartfit', { country_code: patient.country_code }), ) }) it('returns plans with paymentProvider Apple', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.APPLE).code, payment_provider: PaymentProviders.APPLE, }), ]), ) }) }) describe('when n2b payments is enabled', () => { beforeAll(async () => { globalThis.secret.n2b_payments.enable = true plans[0].payment_providers[0].provider = PaymentProviders.APPLE plans[0].payment_providers.push( await factory.build('plan_payment_provider:smartfit', { country_code: patient.country_code }), ) }) afterAll(() => { globalThis.secret.n2b_payments.enable = false }) it('returns plans with paymentProvider Apple', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.APPLE).code, payment_provider: PaymentProviders.APPLE, }), ]), ) }) }) }) }) }) describe('when is a smartfit user', () => { beforeAll(async () => { version = null patient.country_code = 'pt_BR' plans = [ await factory.build('plan:smartfit', { interval: Intervals.YEARLY, }), ] person = await factory.build('smartfit:person') }) describe('when n2b payments is not enabled', () => { beforeAll(() => { globalThis.secret.n2b_payments.enable = false }) describe('when he can purchase addon', () => { let installments: number beforeAll(() => { installments = datatype.number({ min: 1, max: 12 }) person.canPurchaseAddon = true person.paymentMethod = PaymentMethods.credit person.addonsInstallments = [ { addonCode: plans[0].getPaymentProvider(PaymentProviders.SMARTFIT).code, installments }, ] plans[0].payment_providers[0].country_code = patient.country_code }) it('returns plans with paymentProvider Smartfit', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.SMARTFIT).code, payment_provider: PaymentProviders.SMARTFIT, }), ]), ) }) it('returns plans with paymentProvider installments', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.SMARTFIT).code, installments, }), ]), ) }) describe('when the payment method is credit', () => { describe('when addon installment is null from Smartfit', () => { beforeAll(() => { person.addonsInstallments = [ { addonCode: plans[0].getPaymentProvider(PaymentProviders.SMARTFIT).code, installments: null }, ] }) it('returns plans with paymentProvider installments with 12', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.SMARTFIT).code, installments: 12, }), ]), ) }) }) describe('when addon installment is empty from Smartfit', () => { beforeAll(() => { person.addonsInstallments = [] }) it('returns plans with paymentProvider installments with 12', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.SMARTFIT).code, installments: 12, }), ]), ) }) }) }) describe('when the payment method is not credit', () => { beforeAll(() => { person.paymentMethod = PaymentMethods.debitCard }) describe('when addon installment is null from Smartfit', () => { beforeAll(() => { person.addonsInstallments = [ { addonCode: plans[0].getPaymentProvider(PaymentProviders.SMARTFIT).code, installments: null }, ] }) it('returns plans with paymentProvider installments with 12', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.SMARTFIT).code, installments: 12, }), ]), ) }) }) describe('when addon installment is empty from Smartfit', () => { beforeAll(() => { person.addonsInstallments = [] }) it('returns plans with paymentProvider installments with 12', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.SMARTFIT).code, installments: 12, }), ]), ) }) }) }) }) describe('when he cannot purchase addon', () => { beforeAll(() => { os = sample(Object.values(OS)) plans[0].payment_providers[0].provider = os === 'android' ? PaymentProviders.GOOGLE : PaymentProviders.APPLE person.canPurchaseAddon = false person.addonsInstallments = [] }) it('returns plans other than paymentProvider Smartfit', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(plans[0].payment_providers[0].provider).code, payment_provider: plans[0].payment_providers[0].provider, }), ]), ) }) it('returns plans with default paymentProvider installments', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(plans[0].payment_providers[0].provider).code, installments: DEFAULT_INSTALLMENTS_AMOUNT, }), ]), ) }) it('does not return plans with the last four digits of card number', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ last_digits_card_number: null, }), ]), ) }) }) describe('when he can purchase addon but payment method is not allowed', () => { beforeAll(() => { os = sample(Object.values(OS)) plans[0].payment_providers[0].provider = os === 'android' ? PaymentProviders.GOOGLE : PaymentProviders.APPLE person.canPurchaseAddon = true person.paymentMethod = sample([PaymentMethods.debit, PaymentMethods.money]) person.addonsInstallments = [] }) it('returns plans other than paymentProvider Smartfit', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(plans[0].payment_providers[0].provider).code, payment_provider: plans[0].payment_providers[0].provider, }), ]), ) }) it('returns plans with default paymentProvider installments', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(plans[0].payment_providers[0].provider).code, installments: DEFAULT_INSTALLMENTS_AMOUNT, }), ]), ) }) it('does not return plans with the last four digits of card number', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ last_digits_card_number: null, }), ]), ) }) }) }) describe('when n2b payments is enabled', () => { beforeAll(async () => { globalThis.secret.n2b_payments.enable = true plans[0].payment_providers.push( await factory.build('plan_payment_provider:smartfit', { country_code: patient.country_code }), await factory.build('plan_payment_provider:n2b', { country_code: patient.country_code }), ) }) afterAll(() => { globalThis.secret.n2b_payments.enable = false }) describe('and he can purchase addon', () => { beforeAll(() => { person.canPurchaseAddon = true }) describe('and the payment method is credit card', () => { beforeAll(() => { person.paymentMethod = PaymentMethods.credit }) it('returns plans with paymentProvider N2B', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.N2B).code, payment_provider: PaymentProviders.N2B, installments: 12, }), ]), ) }) describe('and a discount coupon was informed', () => { let discountCoupon: DiscountCoupon describe('but does not exists', () => { beforeEach(() => { discountCouponRepository.getByCode.mockResolvedValueOnce(null) }) it('ignores the discount coupon', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os, 'CINQUENTAO') expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.N2B).code, payment_provider: PaymentProviders.N2B, installments: 12, }), ]), ) }) }) describe('is inactive', () => { beforeEach(async () => { discountCoupon = await factory.build('discount_coupon:expired') discountCouponRepository.getByCode.mockResolvedValueOnce(discountCoupon) }) it('ignores the discount coupon', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os, 'CINQUENTAO') expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.N2B).code, payment_provider: PaymentProviders.N2B, installments: 12, }), ]), ) }) }) describe('and is active', () => { beforeEach(async () => { discountCoupon = await factory.build('discount_coupon', { discountAmount: 10, discountType: DiscountTypes.TOTAL, planDiscountCoupons: [{ plan_id: plans[0].id }], }) discountCouponRepository.getByCode.mockResolvedValueOnce(discountCoupon) }) it('applies the discount coupon', async () => { const fromAmount = plans[0].getPaymentProvider(PaymentProviders.N2B).amount const amount = fromAmount - discountCoupon.discountAmount const result = await subject.fetchPlans(datatype.uuid(), version, os, discountCoupon.code) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.N2B).code, payment_provider: PaymentProviders.N2B, installments: 12, from_amount: fromAmount, amount: amount, }), ]), ) }) }) }) }) describe('and the payment method is not credit card', () => { beforeEach(() => { person.paymentMethod = PaymentMethods.debitCard }) it('returns plans with paymentProvider Smartfit', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.SMARTFIT).code, payment_provider: PaymentProviders.SMARTFIT, installments: 12, }), ]), ) }) describe('and a discount coupon is used', () => { let discountCoupon: DiscountCoupon beforeEach(async () => { discountCoupon = await factory.build('discount_coupon', { discountAmount: 10, discountType: DiscountTypes.TOTAL, planDiscountCoupons: [{ plan_id: plans[0].id }], }) discountCouponRepository.getByCode.mockResolvedValueOnce(discountCoupon) }) it('requires a new card', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os, discountCoupon.code) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.N2B).code, payment_provider: PaymentProviders.N2B, installments: 12, new_card_required: true, }), ]), ) }) }) }) }) describe('when he cannot purchase addon', () => { beforeAll(() => { os = OS.ANDROID plans[0].payment_providers[0].provider = PaymentProviders.GOOGLE person.canPurchaseAddon = false person.addonsInstallments = [] }) it('returns plans with paymentProvider Google', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(PaymentProviders.GOOGLE).code, payment_provider: PaymentProviders.GOOGLE, }), ]), ) }) }) describe('when payment method is not allowed', () => { beforeAll(() => { os = sample(Object.values(OS)) plans[0].payment_providers[0].provider = os === 'android' ? PaymentProviders.GOOGLE : PaymentProviders.APPLE person.canPurchaseAddon = true person.paymentMethod = sample([PaymentMethods.debit, PaymentMethods.money]) person.addonsInstallments = [] }) it('returns plans other than paymentProvider N2B', async () => { const result = await subject.fetchPlans(datatype.uuid(), version, os) expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ code: plans[0].getPaymentProvider(plans[0].payment_providers[0].provider).code, payment_provider: plans[0].payment_providers[0].provider, amount: plans[0].payment_providers[0].amount, from_amount: plans[0].payment_providers[0].from_amount, }), ]), ) }) }) }) }) }) }) }) })