import { ConflictException, Injectable } from '@nestjs/common' import { isNil } from 'lodash' import { BusinessExceptionError, NonSubscriberError, NotFoundError, PlanNotFoundError, SubscribeError, UnprocessableEntityError, UserNotFoundError, } from '../../../errors' import { CancellationReason, CancellationsReasons, CardType, Patient, PaymentProviders, Plan, Subscription as SubscriptionModel, } from '../../../models' import { PatientRepository, PlanRepository, SubscriptionInstallmentRepository } from '../../../repositories' import { CancellationUser, CardParams, N2BPaymentsService, SmartfitApi, SubscriptionData, SubscriptionService, TokenType, } from '../../../services' import { CountryCode } from '../../../utils' import { AppleCheckSubscription, CheckSubscription, GoogleCheckSubscription, SmartfitCheckSubscription } from './check' @Injectable() export class Subscription { constructor( private readonly patientRepository: PatientRepository, private readonly subscriptionService: SubscriptionService, private readonly planRepository: PlanRepository, private readonly smartfitApi: SmartfitApi, private readonly smartfitCheckSubscription: SmartfitCheckSubscription, private readonly appleCheckSubscription: AppleCheckSubscription, private readonly googleCheckSubscription: GoogleCheckSubscription, private readonly subscriptionInstallmentRepository: SubscriptionInstallmentRepository, private readonly n2bPaymentsService: N2BPaymentsService, ) {} async subscribe({ patientId, planId, token, paymentProvider = PaymentProviders.SMARTFIT, card, originalTransaction, discountCouponCode, data, }: { patientId: string planId: string paymentProvider?: PaymentProviders token?: string card?: Omit originalTransaction?: string discountCouponCode?: string data: SubscriptionData }): Promise { const patient = await this.getPatient(patientId) const plan = await this.getPlan(planId, patient.country_code as CountryCode, paymentProvider) const paymentProviderData = { provider: paymentProvider, data: { ...(card ? { card: { ...card, tokenType: TokenType.PAYMENT_METHOD_TOKEN } } : {}) }, } return this.subscriptionService.subscribe({ patient, plan, paymentProvider: paymentProviderData, options: { token, originalTransaction }, discountCouponCode, data: { ...data, origin: 'n2b' }, }) } async cancel( patientId: string, cancellationReason: CancellationReason, cancellationBy: CancellationUser, cancellationRequestedByPatientAt: Date, amountToRefund?: number, ignoreTrial = false, ): Promise { const patient = await this.getPatient(patientId) const subscription = patient.subscription if (patient.collaborator) throw new UnprocessableEntityError('Usuário é um colaborador') if (!ignoreTrial && (!patient.subscriber || subscription.status === 'in_cancellation')) throw new NonSubscriberError(`id: ${patient.id}`) if (subscription.paymentProvider === PaymentProviders.SMARTFIT) await this.cancelSubscriptionWithAddon( patient, cancellationReason, cancellationBy, cancellationRequestedByPatientAt, ) if (subscription.paymentProvider === PaymentProviders.N2B) await this.cancelSubscriptionAndRefund( patient, cancellationReason, cancellationBy, cancellationRequestedByPatientAt, amountToRefund, ignoreTrial, ) return subscription.id } async check(patientId: string): Promise { const patient = await this.getPatient(patientId) if (patient.collaborator) return patient.collaborator const { paymentProvider } = patient.subscription || {} try { let isSubscriber = await this.checkSubscriptionByProvider(patient, paymentProvider) if ( !isSubscriber && [PaymentProviders.GOOGLE, PaymentProviders.APPLE, PaymentProviders.N2B].includes(paymentProvider) ) isSubscriber = await this.checkSubscriptionByProvider(patient, PaymentProviders.SMARTFIT) return isSubscriber } catch (error) { console.error(error) return false } } async change( patientId: string, planId: string, token?: string, originalTransaction?: string, ): Promise { const patient = await this.getPatient(patientId) if (patient.collaborator) throw new UnprocessableEntityError('Usuário é um colaborador') if (!patient.subscriber) throw new NonSubscriberError(`id: ${patient.id}`) if (patient.subscription.planId === planId) throw new SubscribeError(`Usuário já possui o plano informado.`) await this.cancelSubscription(patient, { id: CancellationsReasons.PLAN_REPLACEMENT }, { id: patient.id }) const plan = await this.getPlan(planId, patient.country_code as CountryCode, patient.subscription.paymentProvider) return patient.subscription.paymentProvider === PaymentProviders.SMARTFIT ? this.subscriptionService.addSubscriptionWithAddon({ patient, plan, paymentProvider: { provider: patient.subscription.paymentProvider }, data: {}, }) : this.subscriptionService.addSubscription({ patient, plan, paymentProvider: { provider: patient.subscription.paymentProvider }, options: { token, originalTransaction }, data: {}, }) } async regularizeSubscription(patientId: string, token: string, kind: CardType): Promise { const patient = await this.patientRepository.get(patientId) if (!patient) throw new NotFoundError('Patient not found') if (!patient.subscription || patient.subscription.isCancelled) throw new BusinessExceptionError(`Patient hasn't an active subscription`) const overdueInstallment = await this.subscriptionInstallmentRepository.findOverdueBySubscription( patient.subscription.id, ) if (!overdueInstallment) throw new ConflictException(`Patient hasn't overdue installments`) const invoice = await this.n2bPaymentsService.chargeInstallment( patient.subscription.originalTransaction, overdueInstallment.installment, token && kind ? { token, kind, tokenType: TokenType.PAYMENT_METHOD_TOKEN } : null, ) await this.subscriptionService.addTransaction({ amount: overdueInstallment?.amount, due_at: overdueInstallment?.due_date, paid_at: invoice?.paid_at, patient, subscription: patient.subscription, payment_id: invoice.id, card_type: kind || invoice.transaction?.card?.kind, installment_number: overdueInstallment.installment, card_number: invoice.transaction?.card?.last4, installments: 1, payment_provider: PaymentProviders.N2B, ...(invoice.amount > 0 ? {} : { error: invoice.transaction.error }), }) if (invoice.amount <= 0) throw new BusinessExceptionError('Payment not successful') await this.subscriptionService.updateSuspendedDate(patient.subscription, null) await this.subscriptionInstallmentRepository.save({ id: overdueInstallment.id, paid_at: invoice.paid_at }, [ { field: 'paid_at', type: 'timestamptz!' }, ]) } private async cancelSubscription( patient: Patient, cancellationReason: CancellationReason, cancellationBy: CancellationUser, ): Promise { if (patient.subscription.paymentProvider === PaymentProviders.SMARTFIT) return this.cancelSubscriptionWithAddon(patient, cancellationReason, cancellationBy) await this.subscriptionService.cancelSubscription({ patient, cancellationReason, cancellationBy }) } private async cancelSubscriptionWithAddon( patient: Patient, cancellationReason: CancellationReason, cancellationBy: CancellationUser, cancellationRequestedByPatientAt?: Date, ): Promise { const person = await this.subscriptionService.getPerson(patient.login) let activeAddon = await this.subscriptionService.getActiveAddon(person, false) if (!isNil(activeAddon)) await this.smartfitApi.removeAddon({ token: person.authToken }, activeAddon.addonCode) activeAddon = (await this.subscriptionService.getAllowAddons(person)).find( addon => addon.addonCode === activeAddon?.addonCode, ) await this.subscriptionService.cancelSubscription({ patient, cancellationReason, cancellationBy, cancellationRequestedByPatientAt, expiresAt: activeAddon?.expiresAt, }) } private async cancelSubscriptionAndRefund( patient: Patient, cancellationReason: CancellationReason, cancellationBy: CancellationUser, cancellationRequestedByPatientAt: Date, amount?: number, ignoreTrial = false, ): Promise { const subscription = patient.subscription if (this.checkIfAskRefund(amount, subscription.isTrial, ignoreTrial)) amount = await this.subscriptionService.getSubscriptionAmountToRefund(subscription) await this.subscriptionService.cancelSubscription({ patient, cancellationReason, cancellationBy, cancellationRequestedByPatientAt, amount, }) } private async checkSubscriptionByProvider(patient: Patient, paymentProvider: PaymentProviders): Promise { let checkSubscription: CheckSubscription = this.smartfitCheckSubscription if (paymentProvider === PaymentProviders.APPLE) checkSubscription = this.appleCheckSubscription if (paymentProvider === PaymentProviders.GOOGLE) checkSubscription = this.googleCheckSubscription if (paymentProvider === PaymentProviders.N2B) return patient.subscriber return checkSubscription.check(patient) } private async getPatient(id: string): Promise { const patient = await this.patientRepository.get(id) if (!patient) throw new UserNotFoundError(`id: ${id}`) return patient } private async getPlan(id: string, countryCode: CountryCode, provider: PaymentProviders): Promise { const plan = await this.planRepository.getWithFeatures(id, countryCode, provider) if (!plan) throw new PlanNotFoundError(`id: ${id}`) return plan } private checkIfAskRefund = (amountToRefund: number, isTrial: boolean, ignoreTrial: boolean): boolean => { return (amountToRefund > 0 || isTrial || ignoreTrial) && (isNil(amountToRefund) || amountToRefund <= 0) } }