import { Injectable } from '@nestjs/common' import { isNil } from 'lodash' import { BusinessExceptionError, NotFoundError } from '../../../errors' import { Appointment, AppointmentCancellationReasons, AppointmentState, FinancialProvidedServicesIncome, FinancialUserConsolidation, IncomeStatus, IncomeType, } from '../../../models' import { UserRepository, FinancialProvidedServicesIncomeRepository, FinancialUserConsolidationRepository, TeleconsultationsCreditRepository, AnamnesisRepository, FoodPlanRepository, AppointmentRepository, PatientRepository, } from '../../../repositories' import { TeleconsultationsCreditService } from '../../../services/credits' import { chunkedEach } from '../../../utils/async' const REASON_BY_INCOME_STATUS: { [K in IncomeStatus]: string } = { cancelled: 'The income was cancelled', accomplished: 'The income was accomplished', } @Injectable() export class Financial { constructor( private readonly userRepository: UserRepository, private readonly incomeRepository: FinancialProvidedServicesIncomeRepository, private readonly consolidationRepository: FinancialUserConsolidationRepository, private readonly teleconsultationsCreditRepository: TeleconsultationsCreditRepository, private readonly teleconsultationsCreditService: TeleconsultationsCreditService, private readonly anamnesisRepository: AnamnesisRepository, private readonly foodPlanRepository: FoodPlanRepository, private readonly appointmentRepository: AppointmentRepository, private readonly patientRepository: PatientRepository, ) {} async changeIncomeStatus(incomeId: string, status: IncomeStatus): Promise { const income = await this.incomeRepository.get(incomeId) if (!income) throw new NotFoundError('Financial provided services income not found') if (income.status === status) throw new BusinessExceptionError('Financial provided services income already has this status') income.status = status income.amount = await this.getIncomeAmount(status, income.user_id, income.type) await this.incomeRepository.save(income, [ { field: 'status', type: 'String' }, { field: 'amount', type: 'numeric' }, ]) await this.updateAppointmentState(income.appointment_id, income.status) await this.discountOrRefundTeleconsultationCredit(income.appointment_id, income.status) } async consolidateMonth(userId: string, referenceMonth: number): Promise { const incomes = await this.incomeRepository.getAllWithReviews(userId, referenceMonth.toString()) const total = incomes.reduce((previous, income) => { income.financial_services_reviews.forEach(review => { if (review.status === 'pending') new BusinessExceptionError('Financial provided services income has pending reviews') }) return income.status === 'accomplished' ? (previous += income.amount) : previous }, 0) const id = await this.consolidationRepository.create( FinancialUserConsolidation.toInstance({ user_id: userId, reference_month: referenceMonth, total_incomes: total, status: 'pending', }), ) if (!id) throw new Error('Consolidation not created correctly') await chunkedEach(incomes, 10, async income => { income.consolidation_id = id await this.incomeRepository.save(income, [{ field: 'consolidation_id', type: 'uuid' }]) }) } async createIncomeByAppointment(appointment: Appointment): Promise { let status: IncomeStatus if ( appointment.state === 'cancelled' && appointment.cancellation_reason !== AppointmentCancellationReasons.PATIENT_RESCHEDULED ) status = 'cancelled' if (appointment.state === 'food_plan_pending' || appointment.state === 'finished') status = 'accomplished' if (!status) return const income = await this.incomeRepository.getByAppointmentIdAndStatus(appointment.id, status) const anamneseAndFoodPlan = await this.checkIfHasAnamneseAndFoodPlan(appointment) if (income) { if (appointment.state === 'finished') await this.incomeRepository.save({ id: income.id, ...anamneseAndFoodPlan }, [ { field: 'anamnese', type: 'Boolean' }, { field: 'plan_food', type: 'Boolean' }, ]) return } const credit = await this.teleconsultationsCreditRepository.getReferredByAppointmentId(appointment.id) const type: IncomeType = credit?.reason === 'single_buy' ? 'single' : 'plan' const amount = await this.getIncomeAmount(status, appointment.user_id, type) await this.incomeRepository.create( FinancialProvidedServicesIncome.toInstance({ type, user_id: appointment.user_id, appointment_id: appointment.id, status, amount, ...anamneseAndFoodPlan, }), ) } private async checkIfHasAnamneseAndFoodPlan( appointment: Appointment, ): Promise<{ anamnese: boolean; plan_food: boolean }> { const [anamnese, foodPlan] = await Promise.all([ this.anamnesisRepository.exists(appointment.created_at, appointment.user_id, appointment.patient_id), this.foodPlanRepository.getByAppointmentId(appointment.id), ]) return { anamnese, plan_food: !!foodPlan } } private async discountOrRefundTeleconsultationCredit(appointmentId: string, status: IncomeStatus): Promise { const appointment = await this.appointmentRepository.get(appointmentId) const patient = await this.patientRepository.get(appointment.patient_id) if (patient.internal_collaborator) return const credit = await this.teleconsultationsCreditRepository.getByAppointmentId(appointmentId) if (status === 'cancelled') { // TODO check allowDecreaseCredit if (!credit) return if (await this.teleconsultationsCreditRepository.getRefundByAppointmentId(appointmentId)) return } if (status === 'accomplished' && credit) return await this.teleconsultationsCreditService.undoCredit(credit, REASON_BY_INCOME_STATUS[status]) } private async getIncomeAmount(status: IncomeStatus, userId: string, type: IncomeType): Promise { if (status === 'cancelled') return 0 if (status !== 'accomplished') throw new Error('Invalid income status') const userFinancialData = await this.userRepository.getFinancialProperties(userId) if (!userFinancialData) throw new NotFoundError('User not found') if (isNil(userFinancialData[`remuneration_teleconsultation_${type}`])) throw new BusinessExceptionError(`User hasn't valid plan remuneration amount`) return userFinancialData[`remuneration_teleconsultation_${type}`] } private async updateAppointmentState(appointmentId: string, status: IncomeStatus): Promise { let state: AppointmentState = status === 'cancelled' ? status : null if (!state) { const foodPlan = await this.foodPlanRepository.getByAppointmentId(appointmentId) state = foodPlan ? (foodPlan.published_by_id ? 'finished' : 'food_plan_in_construction') : 'food_plan_pending' } await this.appointmentRepository.save({ id: appointmentId, state }, ['state']) } }