import { Injectable } from '@nestjs/common' import { compare } from 'compare-versions' import { isNil } from 'lodash' import { RequiredArgumentError, UserNotFoundError } from '../../../errors' import { PaymentMethods, Person, SmartfitApi, SmartfitPlanHelper } from '../../../services/smartfit' import { DiscountCouponRepository, PatientRepository, PlanFeatureRepository, PlanRepository, } from '../../../repositories' import { Patient, PaymentProviders, Plan as PlanModel, PlanFeature } from '../../../models' import { CountryCode } from '../../../utils' import { IntervalsHelper } from '../../../services/subscription/helpers' import { OS } from '../../../params/enums' import { PlanResult } from './types' import { planToPlanResult } from './converters' const ALLOW_SMARTFIT_PAYMENT_METHODS = [PaymentMethods.credit, PaymentMethods.debitCard] @Injectable() export class Plan { constructor( private readonly patientRepository: PatientRepository, private readonly planRepository: PlanRepository, private readonly smartfitApi: SmartfitApi, private readonly planFeatureRepository: PlanFeatureRepository, private readonly discountCouponRepository: DiscountCouponRepository, ) {} private get isN2BEnabled(): boolean { return globalThis.secret.n2b_payments?.enable || false } async fetchPlans(patientId: string, appVersion: string, os: OS, discountCouponCode?: string): Promise { const { patient, person } = await this.getPatientAndPerson(patientId) const { plans, paymentProvider } = await this.getPaymentProviderAndPlans({ patient, person, appVersion, os, discountCouponCode, }) const available = await Promise.all( plans.map(plan => this.mapToPlanResult({ plan, person, countryCode: patient.country_code as CountryCode, paymentProvider, }), ), ) return paymentProvider === PaymentProviders.N2B ? await this.applyDiscountCoupon(discountCouponCode, available, person) : available } async getAvailableFeatures(countryCode: CountryCode): Promise>> { const planFeatures = await this.planFeatureRepository.findByCountryCode(countryCode) return planFeatures.map(planFeature => ({ feature: planFeature.feature, description: planFeature.description, })) } private async getPaymentProviderAndPlans({ patient, person, appVersion, os, discountCouponCode, }: { patient: Patient person: Person appVersion: string os: OS discountCouponCode: string }): Promise<{ paymentProvider: PaymentProviders; plans: Array }> { const plans = await this.getPlansBy(patient, appVersion) const paymentProvider = this.getPaymentProviderBy({ person, plans, os, countryCode: patient.country_code, discountCouponCode, }) return { plans: plans.filter(plan => plan.getPaymentProvider(paymentProvider, patient.country_code)), paymentProvider, } } private getPaymentProviderBy({ person, plans, countryCode, os, discountCouponCode, }: { person: Person plans: Array countryCode: CountryCode os: OS discountCouponCode: string }): PaymentProviders { if (!isNil(person) && person.canPurchaseAddon && ALLOW_SMARTFIT_PAYMENT_METHODS.includes(person.paymentMethod)) { if ( this.isN2BEnabled && plans.find(plan => plan.getPaymentProvider(PaymentProviders.N2B, countryCode)) && (person.paymentMethod === PaymentMethods.credit || !isNil(discountCouponCode)) ) return PaymentProviders.N2B if (plans.find(plan => plan.getPaymentProvider(PaymentProviders.SMARTFIT, countryCode))) return PaymentProviders.SMARTFIT } if (!os) throw new RequiredArgumentError('OS') return os === OS.ANDROID ? PaymentProviders.GOOGLE : PaymentProviders.APPLE } private async getPlansBy(patient: Patient, appVersion: string): Promise> { const plans = await this.getPlansByPatient(patient) const filteredPlans = plans?.filter(plan => { return !plan.version_gte || compare(appVersion, plan.version_gte, '>=') }) || [] return filteredPlans } private async getPatientAndPerson(id: string): Promise<{ patient: Patient; person: Person }> { const patient = await this.patientRepository.get(id) if (!patient) throw new UserNotFoundError(`id: ${id}`) let person: Person try { person = await this.smartfitApi.getPersonByLogin(patient.login) } catch (error) { console.error(error) } return { patient, person } } private getPlansByPatient(patient: Patient): Promise> { if (patient.subscriber && patient.subscription.planId) return this.planRepository.findUpgradeablePlansByPlanId(patient.subscription.planId, { language: patient.country_code, }) return this.planRepository.findEnabledAndVisible({ language: patient.country_code }) } private async mapToPlanResult({ plan, paymentProvider, countryCode, person, }: { plan: PlanModel paymentProvider: PaymentProviders countryCode: CountryCode person: Person }): Promise { if (paymentProvider === PaymentProviders.SMARTFIT) plan = SmartfitPlanHelper.makePlanWithSmartfitPaymentProvider(person, plan) if (paymentProvider === PaymentProviders.N2B) plan.getPaymentProvider(paymentProvider, countryCode).installments = IntervalsHelper.IntervalsToCardInstallments( plan.interval, ) const features = await this.planFeatureRepository.findByPlanId(plan.id, paymentProvider, countryCode) return planToPlanResult(plan, paymentProvider, features) } private async applyDiscountCoupon(discountCouponCode: string, available: PlanResult[], person: Person) { if (isNil(discountCouponCode)) return available const discountCoupon = await this.discountCouponRepository.getByCode(discountCouponCode) if (isNil(discountCoupon) || discountCoupon.isInactive) return available return (available = available .filter(planResult => discountCoupon.allows(planResult.id)) .map(planResult => { planResult.from_amount = planResult.amount planResult.amount = discountCoupon.apply(planResult.amount) planResult.new_card_required = person.paymentMethod !== PaymentMethods.credit return planResult })) } }