/* eslint-disable @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/ban-types */ import { Injectable } from '@nestjs/common' import { chunk, isEmpty, isNil } from 'lodash' import { StorageService } from '../../../services/storage/storage.service' import { BioimpedanceCreditService } from '../../../services/credits/bioimpedance_credit.service' import { Anthropometry, Bioimpedance, BioimpedanceImportation, Patient } from '../../../models' import { AnthropometryRepository, BioimpedanceImportationRepository, PatientRepository, SmartfitUnitRepository, } from '../../../repositories' import { csvToJSON } from '../utils' import { FileNotFoundError, UnprocessableEntityError } from '../../../errors' import { ImportationErrors, ImportationResult } from '../types' import { CountryHelper } from '../../../utils/countries' import { BioimpedanceNotificationService, BioimpedanceService } from '../../../services' import { VALID_HEADERS_TO_BIOIMPEDANCE } from '../../../utils/bioimpedance' export const FROM_PATH_PREFIX = 'bioimpedances/pending/' export const TO_PATH_PREFIX = 'bioimpedances/processed/' export const FILE_NOT_FOUND = 'BIO_FILE_NOT_FOUND' export const FILE_EMPTY = 'BIO_EMPTY_FILE' export const FILE_WITH_INVALID_HEADER = 'BIO_INVALID_HEADER' export const BIO_ALREADY_IMPORTED_ERROR = 'BIO_ALREADY_IMPORTED' export const BIO_MISSING_REQUIRED_FIELDS_ERROR = 'BIO_MISSING_REQUIRED_FIELDS' export const BIO_UNKNOWN_ERROR = 'BIO_UNKNOWN_ERROR' @Injectable() export class Importation { constructor( private readonly storageService: StorageService, private readonly creditService: BioimpedanceCreditService, private readonly bioimpedanceImportationRepository: BioimpedanceImportationRepository, private readonly patientRepository: PatientRepository, private readonly anthropometryRepository: AnthropometryRepository, private readonly smartfitUnitRepository: SmartfitUnitRepository, private readonly bioimpedanceNotificationService: BioimpedanceNotificationService, private readonly bioimpedanceService: BioimpedanceService, ) {} async importBioimpedances({ userId, filePath, users, smartfitUnitId, }: { userId: string filePath: string users: Array smartfitUnitId: string }): Promise { const { file, fileName } = await this.getFile(filePath) const rows = await csvToJSON({ file }) const usefulLines = rows.filter(row => !isNil(row) && !isEmpty(row)) this.validateFile(usefulLines) const importationResult = await this.process({ rows: usefulLines, filePath: fileName, userId, users, smartfitUnitId, }) await Promise.all([ this.storageService.moveFile(`${FROM_PATH_PREFIX}${fileName}`, `${TO_PATH_PREFIX}${fileName}`), this.bioimpedanceNotificationService.sendNotificatonForBioImports( importationResult.rowsInserted, importationResult.countryCode, ), ]) return { rowsWithError: importationResult.rowsWithError } } async createAnthropometryIfNotExists(bioimpedanceImportation: BioimpedanceImportation): Promise { if (bioimpedanceImportation.anthropometry_id) return const smartfitUnit = await this.smartfitUnitRepository.get(+bioimpedanceImportation.smartfit_unit_id) const countryCode = CountryHelper.getLanguageCountryCodeByCountry(smartfitUnit.country_code) const login = bioimpedanceImportation[Patient.loginField(countryCode)] const patient = await this.patientRepository.findByLogin(login, countryCode) if (!patient) return if (patient.smartfit_unit_acronym !== smartfitUnit.acronym) await this.patientRepository.updateSmartfitUnitAcronym(patient.id, smartfitUnit.acronym) await this.effectABioimpedance(patient, bioimpedanceImportation) await this.bioimpedanceService.createPatientAnthropometriesCache(patient.id, 'bioimpedances') } async createBioimpedanceIfPending(patientData: Patient): Promise { const patient = await this.getPatient(patientData) const bioimpedanceImportations = await this.bioimpedanceImportationRepository.findPending(patient.login) const chunked = chunk(bioimpedanceImportations, 10) for (const chunkedBioimpedanceImportations of chunked) { await Promise.all( chunkedBioimpedanceImportations.map( async (bioimpedanceImportation): Promise => { await this.effectABioimpedance(patient, bioimpedanceImportation) }, ), ) } await this.bioimpedanceService.createPatientAnthropometriesCache(patient.id, 'bioimpedances') } private async effectABioimpedance(patient: Patient, bioimpedanceImportation: BioimpedanceImportation): Promise { const anthropometry = await this.createAnthropometry(patient, bioimpedanceImportation) await this.debitFromBioimpedanceCreditBalance(patient, anthropometry.bioimpedance) } private async createAnthropometry( patient: Patient, bioimpedanceImportation: BioimpedanceImportation, ): Promise { const anthropometry = bioimpedanceImportation.toAnthropometry(patient) const anthropometryResult = await this.anthropometryRepository.create(anthropometry) anthropometry.bioimpedance_id = anthropometry.bioimpedance.id = anthropometryResult.bioimpedance_id bioimpedanceImportation.anthropometry_id = anthropometryResult.id await this.bioimpedanceImportationRepository.update(bioimpedanceImportation) return anthropometry } private async process({ rows, filePath, userId, users, smartfitUnitId, }: { rows: any[] filePath: string userId: string users: Array smartfitUnitId: string }): Promise { const rowsWithError = {} const rowsInserted: any[] = [] const smartfitUnit = await this.smartfitUnitRepository.get(+smartfitUnitId) const countryCode = CountryHelper.getLanguageCountryCodeByCountry(smartfitUnit.country_code) const loginProperty = Patient.loginField(countryCode) const parsedRows = rows.map(row => BioimpedanceImportation.toParsedValue(row, filePath, userId, users, smartfitUnit.id), ) const chunked = chunk(parsedRows, 5) for (const bioRows of chunked) { await Promise.all( bioRows.map(async bioimpedance => { try { rowsInserted.push(await this.bioimpedanceImportationRepository.create(bioimpedance)) } catch (error) { rowsWithError[bioimpedance[loginProperty]] = this.getErrorCode(error[0]?.message) } }), ) } return { rowsWithError, rowsInserted, countryCode, } } private async getFile(filePath: string): Promise<{ fileName: string; file: Buffer }> { const fileName = filePath.split(FROM_PATH_PREFIX)[1] let file: Buffer try { file = await this.storageService.getFile(`${FROM_PATH_PREFIX}${fileName}`) } catch (_err) { throw new FileNotFoundError(FILE_NOT_FOUND) } if (file.length === 0) throw new UnprocessableEntityError(FILE_EMPTY) return { fileName, file } } private async debitFromBioimpedanceCreditBalance(patient: Patient, bioimpedance: Bioimpedance): Promise { if (patient.subscriber) return const creditToUse = await this.creditService.getNextValidCreditToUse(patient.id) await this.creditService.createTransaction({ patientId: patient.id, reason: 'bioimpedance_use', value: -1, objectModel: bioimpedance, author: patient, creditId: creditToUse?.id || null, }) } private async getPatient(patient: Partial): Promise { const patientAttrsNames = Object.keys(patient) if (patientAttrsNames.length === 1 && patientAttrsNames[0] === 'id') return this.patientRepository.get(patient.id) return patient as Patient } private validateFile(rows: any[]): void { if (isEmpty(rows)) throw new UnprocessableEntityError(FILE_EMPTY) this.validateHeader(rows[0]) } private getErrorCode(message: string): string { return message?.includes('Uniqueness violation') ? BIO_ALREADY_IMPORTED_ERROR : message?.includes('Not-NULL violation') ? BIO_MISSING_REQUIRED_FIELDS_ERROR : BIO_UNKNOWN_ERROR } private validateHeader(row: any): void { const unknownProperties = Object.keys(VALID_HEADERS_TO_BIOIMPEDANCE).filter( header => !Object.prototype.hasOwnProperty.call(row, header), ) if (!isEmpty(unknownProperties)) throw new UnprocessableEntityError(FILE_WITH_INVALID_HEADER) } }