import { ConflictException, Injectable } from '@nestjs/common' import { isEmpty, isString } from 'lodash' import { addHours, differenceInDays, format } from 'date-fns' import { MailService, ContractService, ContractData, SMSService, MessagingService, PushNotificationTemplate, } from '../../../services' import { CancellationsReasons, Patient, PaymentProviders, Plan, Subscription, SubscriptionTransaction, } from '../../../models' import { PatientRepository, PlanRepository, SubscriptionInstallmentRepository, SubscriptionTransactionRepository, } from '../../../repositories' import { NotFoundError } from '../../../errors' import { maskCPF, LanguageCountryCode, getOffsetBy, CountryCode } from '../../../utils' import { TranslateNotificationsHelper } from '../i18n' const DATE_FORMAT = 'dd/MM/yyyy' export type SubscriptionNotificationDynamicData = { first_name: string card_function: string last_numbers: string error_message: string plan: { name: string type: { [key in 'bio' | 'teleconsultation' | 'complete']?: boolean } } } @Injectable() export class Subscriptions { constructor( private readonly patientRepository: PatientRepository, private readonly mailService: MailService, private readonly contractService: ContractService, private readonly planRepository: PlanRepository, private readonly subscriptionTransactionRepository: SubscriptionTransactionRepository, private readonly subscriptionInstallmentRepository: SubscriptionInstallmentRepository, private readonly messagingService: MessagingService, private readonly smsService: SMSService, private readonly translator: TranslateNotificationsHelper, ) {} async notifyCancelled(subscription: Subscription): Promise { if (subscription.cancellationReasonId === CancellationsReasons.PLAN_REPLACEMENT) return const patient = await this.patientRepository.get(subscription.patientId) if (patient.country_code !== 'pt_BR') return const templateId = this.translator.get(patient.country_code, 'email_templates.subscription_cancel') await this.mailService.send({ to: patient.email, templateId, dynamicTemplateData: { nome: patient.name }, }) } async notifyNew(subscription: Subscription): Promise { const { patient, ...entities } = await this.getRelatedEntities(subscription) const data = await this.makeContractData({ ...entities, patient, subscription }) await this.contractService.generateSubscriptionContract( data, patient.id, subscription.id, patient.country_code as LanguageCountryCode, ) } async notifySuccessOverduePayment(transaction: SubscriptionTransaction): Promise { const transactionFaileds = await this.subscriptionTransactionRepository.countFailedBySubscriptionIdAndInstallment( transaction.subscription_id, transaction.installment_number, ) if (transactionFaileds <= 0) return const overdueInstallment = await this.subscriptionInstallmentRepository.findBySubscriptionAndNumber( transaction.subscription_id, transaction.installment_number, ) if (!overdueInstallment) throw new NotFoundError('Subscription installment not found') if (overdueInstallment.paid_at.getTime() < overdueInstallment.due_date.getTime()) return const { dynamidTemplateData, patient } = await this.getTransactionRelatedData(transaction) await this.notify({ email: 'billing_success', dynamidTemplateData, patient, }) } async notifyFailedOverduePayment(transaction: SubscriptionTransaction): Promise { const installment = await this.subscriptionInstallmentRepository.findBySubscriptionAndNumber( transaction.subscription_id, transaction.installment_number, ) if (!installment) throw new NotFoundError('Subscription installment not found') if (installment.due_date.getTime() > new Date().getTime()) throw new ConflictException(`Installment isn't overdue`) const { dynamidTemplateData, patient, plan } = await this.getTransactionRelatedData(transaction) const days = differenceInDays(new Date(), installment.due_date) if (days === 0) return this.notify({ template: 'billing_failure_d1', email: true, sms: true, push: true, dynamidTemplateData, patient, }) if (days === 4) return this.notify({ push: `billing_failure_${plan.kind}_d5`, email: 'billing_failure_d5', dynamidTemplateData, patient, }) if (days === 9) return this.notify({ push: `billing_failure_${plan.kind}_d10`, email: 'billing_failure_d10', dynamidTemplateData, patient, }) if (days === 14) return this.notify({ push: `billing_failure_${plan.kind}_d15`, dynamidTemplateData, patient, }) if (days === 19) return this.notify({ sms: `billing_failure_${plan.kind}_d20`, email: 'billing_failure_d20', dynamidTemplateData, patient, }) } private async getTransactionRelatedData( transaction: SubscriptionTransaction, ): Promise<{ patient: Patient; plan: Plan; dynamidTemplateData: SubscriptionNotificationDynamicData }> { const patient = await this.getPatient(transaction.patient_id) const plan = await this.getPlan( patient.subscription.planId, patient.country_code, patient.subscription.paymentProvider, ) const dynamidTemplateData = this.getDynamicTemplateData(patient, transaction, plan) return { patient, plan, dynamidTemplateData } } private getDynamicTemplateData( patient: Patient, transaction: SubscriptionTransaction, plan: Plan, ): SubscriptionNotificationDynamicData { return { first_name: patient.firstName, card_function: this.translator.get(patient.country_code, `card_kind.${transaction.card_type}`), last_numbers: transaction.card_number, error_message: transaction.error_message, plan: { name: plan.name, type: { [plan.kind]: true, }, }, } } private async notify({ template, dynamidTemplateData, patient, push, sms, email, }: { dynamidTemplateData: SubscriptionNotificationDynamicData patient: Patient template?: string push?: boolean | string sms?: boolean | string email?: boolean | string }): Promise { if (!sms && !push && !email) return const promises = [] if (email) { const emailTemplate = `email_templates.${isString(email) ? email : template}` as any const templateId = this.translator.get(patient.country_code, emailTemplate, { params: dynamidTemplateData, }) promises.push( this.mailService.send({ to: patient.email, templateId: templateId, dynamicTemplateData: dynamidTemplateData, }), ) } if (push) { const pushTemplate = this.translator.get( patient.country_code, `pushes.${isString(push) ? push : template}` as any, { params: dynamidTemplateData }, ) promises.push(this.messagingService.sendPushNotificationByTemplate(pushTemplate, patient.device_id)) } if (sms) { const smsTemplate = `sms.${isString(sms) ? sms : template}` as any const message = this.translator.get(patient.country_code, smsTemplate, { params: dynamidTemplateData }) promises.push(this.smsService.send(patient.cell_phone, message)) } await Promise.all(promises) } private async getRelatedEntities( subscription: Subscription, ): Promise<{ patient: Patient; plan: Plan; transaction: SubscriptionTransaction; installments: number }> { const patient = await this.getPatient(subscription.patientId) const plan = await this.getPlan( subscription.planId, patient.country_code as CountryCode, subscription.paymentProvider, ) const transaction = await this.subscriptionTransactionRepository.findLastBySubscription(subscription.id) if (!transaction) throw new NotFoundError('Subscription transaction not found') const installments = await this.subscriptionInstallmentRepository.getTotalBySubscription(subscription.id) if (!installments) throw new NotFoundError('Subscription installments not found') return { patient, plan, transaction, installments } } private async getPatient(id: string): Promise { const patient = await this.patientRepository.get(id) if (!patient) throw new NotFoundError('Patient not found') return patient } private async getPlan(id: string, countryCode: CountryCode, paymentProvider: PaymentProviders): Promise { const plan = await this.planRepository.getWithFeatures(id, countryCode, paymentProvider) if (!plan) throw new NotFoundError('Plan not found') return plan } private async makeContractData({ patient, plan, transaction, subscription, installments, }: { patient: Patient plan: Plan transaction: SubscriptionTransaction subscription: Subscription installments: number }): Promise { const price = transaction.installment_price.toLocaleString('pt-br', { style: 'currency', currency: 'BRL' }) const wallet = { masked_card_number: `xxxx xxxx xxxx ${!isEmpty(transaction.card_number) ? transaction.card_number : 'xxxx'}`, responsible_name: patient.name, responsible_cpf: maskCPF(patient.document_number), } const subscriptionData = { id: subscription.id, plan_name: plan.name, date: format( addHours(subscription.createdAt, getOffsetBy(patient.country_code as CountryCode, subscription.createdAt)), DATE_FORMAT, ), price, installments, teleconsultations_value: plan.getFeature('teleconsultation')?.quantity || 0, origin_humanize: transaction.data?.origin?.toUpperCase(), } return { wallet, subscription: subscriptionData, person: { name: patient.name, birthday: patient.birth_date ? format(patient.birth_date, DATE_FORMAT) : '', document_cpf: maskCPF(patient.document_number), email: patient.email, }, } } }