From f0e230fec4222ba30223a9bc27f8a2607a7337b4 Mon Sep 17 00:00:00 2001 From: aldoEMatamala Date: Tue, 9 Dec 2025 10:07:25 -0300 Subject: [PATCH] feat(REC-174): Generar rutas para prescripciones de insumos --- initialize.ts | 2 + modules/insumos/insumos-schema.ts | 4 +- modules/insumos/insumos.routes.ts | 2 +- modules/recetas/recetasInsumos/index.ts | 2 + .../recetasInsumos/receta-insumo.events.ts | 54 ++++ .../recetasInsumos/receta-insumo.routes.ts | 43 +++ .../recetasInsumos/receta-insumo.schema.ts | 157 +++++++++++ .../recetasInsumos/recetaInsumosController.ts | 254 ++++++++++++++++++ 8 files changed, 515 insertions(+), 3 deletions(-) create mode 100644 modules/recetas/recetasInsumos/index.ts create mode 100644 modules/recetas/recetasInsumos/receta-insumo.events.ts create mode 100644 modules/recetas/recetasInsumos/receta-insumo.routes.ts create mode 100644 modules/recetas/recetasInsumos/receta-insumo.schema.ts create mode 100644 modules/recetas/recetasInsumos/recetaInsumosController.ts diff --git a/initialize.ts b/initialize.ts index 2e2a5bd3c..fa7e32318 100644 --- a/initialize.ts +++ b/initialize.ts @@ -122,6 +122,8 @@ export function initAPI(app: Express) { app.use('/api/modules', require('./modules/constantes').ConstantesRouter); app.use('/api/modules', require('./modules/recetas').RecetasRouter); app.use('/api/modules', require('./modules/insumos').InsumosRouter); + app.use('/api/modules', require('./modules/recetas/recetasInsumos').RecetaInsumoRouter); + if (configPrivate.hosts.BI_QUERY) { app.use( diff --git a/modules/insumos/insumos-schema.ts b/modules/insumos/insumos-schema.ts index 8b8dc9f36..18141f5b4 100644 --- a/modules/insumos/insumos-schema.ts +++ b/modules/insumos/insumos-schema.ts @@ -3,7 +3,7 @@ import { AuditPlugin } from '@andes/mongoose-plugin-audit'; export const insumoSchema = new mongoose.Schema({ - insumo: String, + nombre: String, tipo: { type: String, enum: ['dispositivo', 'nutricion', 'magistral'] @@ -14,4 +14,4 @@ export const insumoSchema = new mongoose.Schema({ insumoSchema.plugin(AuditPlugin); -export const Insumo = mongoose.model('insumo', insumoSchema, 'insumo'); +export const Insumo = mongoose.model('insumos', insumoSchema, 'insumos'); diff --git a/modules/insumos/insumos.routes.ts b/modules/insumos/insumos.routes.ts index 2a440c97f..d0036e80a 100644 --- a/modules/insumos/insumos.routes.ts +++ b/modules/insumos/insumos.routes.ts @@ -8,7 +8,7 @@ class InsumosResource extends ResourceBase { resourceName = 'insumos'; middlewares = [Auth.authenticate()]; searchFileds = { - insumo: MongoQuery.partialString, + nombre: MongoQuery.partialString, tipo: MongoQuery.equalMatch, requiereEspecificacion: MongoQuery.equalMatch, }; diff --git a/modules/recetas/recetasInsumos/index.ts b/modules/recetas/recetasInsumos/index.ts new file mode 100644 index 000000000..6adda677d --- /dev/null +++ b/modules/recetas/recetasInsumos/index.ts @@ -0,0 +1,2 @@ +import './receta-insumo.events'; +export { RecetaInsumoRouter } from './receta-insumo.routes'; diff --git a/modules/recetas/recetasInsumos/receta-insumo.events.ts b/modules/recetas/recetasInsumos/receta-insumo.events.ts new file mode 100644 index 000000000..9ed7dfda4 --- /dev/null +++ b/modules/recetas/recetasInsumos/receta-insumo.events.ts @@ -0,0 +1,54 @@ +import { EventCore } from '@andes/event-bus'; +import { crearRecetaInsumo } from '../../recetas/recetasInsumos/recetaInsumosController'; +import { getProfesionActualizada } from '../../recetas/recetasController'; +import * as moment from 'moment'; +import { RecetaInsumo } from './receta-insumo.schema'; +import { createLog, informarLog, updateLog, jobsLog } from './../recetaLogs'; + +EventCore.on('prestacion:recetaInsumo:create', async ({ prestacion, registro }) => { + const idRegistro = registro._id; + const profPrestacion = prestacion.solicitud.profesional; + const { profesionGrado, matriculaGrado, especialidades } = await getProfesionActualizada(profPrestacion.id); + const profesional = { + id: profPrestacion.id, + nombre: profPrestacion.nombre, + apellido: profPrestacion.apellido, + documento: profPrestacion.documento, + profesion: profesionGrado, + especialidad: especialidades, + matricula: matriculaGrado + }; + + const organizacion = { + id: prestacion.ejecucion.organizacion.id, + nombre: prestacion.ejecucion.organizacion.nombre + }; + + const dataReceta = { + idPrestacion: prestacion.id, + idRegistro, + fechaRegistro: prestacion.ejecucion.fecha || moment().toDate(), + fechaPrestacion: prestacion.ejecucion.fecha, + paciente: prestacion.paciente, + profesional, + organizacion, + insumo: null, + diagnostico: null, + }; + try { + for (const insumo of registro.valor.insumos) { + const receta: any = await RecetaInsumo.findOne({ + 'insumo.nombre': insumo.generico.nombre, + idRegistro + }); + if (!receta) { + dataReceta.insumo = insumo; + await crearRecetaInsumo(dataReceta, prestacion.createdBy); // falta return + } + + } + } catch (err) { + createLog.error('create', { dataReceta, prestacion, profesional }, err, { prestacion, registro }); + return err; + } +}); diff --git a/modules/recetas/recetasInsumos/receta-insumo.routes.ts b/modules/recetas/recetasInsumos/receta-insumo.routes.ts new file mode 100644 index 000000000..97282621d --- /dev/null +++ b/modules/recetas/recetasInsumos/receta-insumo.routes.ts @@ -0,0 +1,43 @@ +import { MongoQuery, ResourceBase } from '@andes/core'; +import { Auth } from '../../../auth/auth.class'; +import { RecetaInsumo } from './receta-insumo.schema'; +import { asyncHandler, Request, Response } from '@andes/api-tool'; +import { create } from './recetaInsumosController'; + +class RecetaInsumoResource extends ResourceBase { + Model = RecetaInsumo; + resourceName = 'recetaInsumo'; + routesEnable = ['get, post']; + middlewares = [Auth.authenticate()]; + searchFileds = { + paciente: { + field: 'paciente.id', + fn: MongoQuery.equalMatch + }, + documento: { + field: 'paciente.documento', + fn: MongoQuery.equalMatch + } + }; +} + +export const post = async (req, res) => { + const resp = await create(req); + const status = resp?.status || resp?.errors || 200; + res.status(status).json(resp); +}; +export const RecetaInsumoCtr = new RecetaInsumoResource({}); +export const RecetaInsumoRouter = RecetaInsumoCtr.makeRoutes(); + +const authorizeByToken = async (req: Request, res: Response, next) => + Auth.authorizeByToken(req, res, next, [ + 'huds:visualizacionHuds', + 'huds:visualizacionParcialHuds:laboratorio', + 'huds:visualizacionParcialHuds:vacuna', + 'huds:visualizacionParcialHuds:receta', + 'huds:visualizacionParcialHuds:*', + 'recetas:read' + ]); + +RecetaInsumoRouter.use(Auth.authenticate()); +RecetaInsumoRouter.post('/recetasInsumos', authorizeByToken, asyncHandler(post)); diff --git a/modules/recetas/recetasInsumos/receta-insumo.schema.ts b/modules/recetas/recetasInsumos/receta-insumo.schema.ts new file mode 100644 index 000000000..81585fda8 --- /dev/null +++ b/modules/recetas/recetasInsumos/receta-insumo.schema.ts @@ -0,0 +1,157 @@ +import { AuditPlugin } from '@andes/mongoose-plugin-audit'; +import * as mongoose from 'mongoose'; +import { ProfesionalSubSchema } from '../../../core/tm/schemas/profesional'; +import { PacienteSubSchema } from '../../../core-v2/mpi/paciente/paciente.schema'; +const insumoSubSchema = new mongoose.Schema({ + insumo: String, + tipo: { + type: String, + enum: ['dispositivo', 'nutricion', 'magistral'] + }, + requiereEspecificacion: Boolean, + cantidad: Number, + especificacion: String +}, { _id: false }); + +const cancelarSchema = new mongoose.Schema({ + idDispensaApp: { + type: String, + required: false + }, + motivo: { + type: String, + required: false + }, + organizacion: { + id: String, + nombre: String + } +}); +const sistemaSchema = { + type: String, + enum: ['sifaho', 'recetar'] +}; +const estadoDispensaSchema = new mongoose.Schema({ + tipo: { + type: String, + enum: ['sin-dispensa', 'dispensada', 'dispensa-parcial'], + required: true, + default: 'sin-dispensa' + }, + idDispensaApp: { + type: String, + required: false + }, + fecha: Date, + sistema: sistemaSchema, + cancelada: { + type: cancelarSchema, + required: false + } +}); +const estadosSchema = new mongoose.Schema({ + tipo: { + type: String, + enum: ['pendiente', 'vigente', 'finalizada', 'vencida', 'suspendida', 'rechazada'], + required: true, + default: 'vigente' + }, + motivo: { + type: String, + required: false + }, + observacion: { + type: String, + required: false + }, + profesional: { + type: ProfesionalSubSchema, + required: false + }, + organizacionExterna: { + id: { + type: String, + required: false + }, + nombre: { + type: String, + required: false + } + } +}); + + +export const recetaInsumoSchema = new mongoose.Schema({ + organizacion: { + id: mongoose.SchemaTypes.ObjectId, + nombre: String + }, + profesional: { + id: mongoose.SchemaTypes.ObjectId, + nombre: String, + apellido: String, + documento: String, + profesion: String, + matricula: Number, + especialidad: String, + }, + fechaRegistro: Date, + fechaPrestacion: Date, + idPrestacion: String, + idRegistro: String, + diagnostico: mongoose.SchemaTypes.Mixed, + insumo: { type: insumoSubSchema, required: true }, + dispensa: [ + { + idDispensaApp: String, + fecha: Date, + insumos: [{ + cantidad: Number, + descripcion: String, + insumo: mongoose.SchemaTypes.Mixed, + cantidadEnvases: Number, + observacion: { + type: String, + required: false + } + }], + organizacion: { + id: String, + nombre: String + }, + } + ], + estados: [estadosSchema], + estadoActual: estadosSchema, + estadosDispensa: [estadoDispensaSchema], + estadoDispensaActual: estadoDispensaSchema, + paciente: PacienteSubSchema, + renovacion: String, + appNotificada: [{ app: sistemaSchema, fecha: Date }], + origenExterno: { + id: String, // id receta creada por sistema que no es Andes + app: sistemaSchema, + fecha: Date + } +}); + +recetaInsumoSchema.pre('save', function (next) { + const recetaInsumo: any = this; + + if (recetaInsumo.estados && recetaInsumo.estados.length > 0) { + recetaInsumo.estadoActual = recetaInsumo.estados[recetaInsumo.estados.length - 1]; + } + if (recetaInsumo.estadosDispensa && recetaInsumo.estadosDispensa.length > 0) { + recetaInsumo.estadoDispensaActual = recetaInsumo.estadosDispensa[recetaInsumo.estadosDispensa.length - 1]; + } + + next(); +}); + +recetaInsumoSchema.plugin(AuditPlugin); + +recetaInsumoSchema.index({ + idPrestacion: 1, +}); + +export const RecetaInsumo = mongoose.model('recetasInsumo', recetaInsumoSchema, 'recetasInsumo'); diff --git a/modules/recetas/recetasInsumos/recetaInsumosController.ts b/modules/recetas/recetasInsumos/recetaInsumosController.ts new file mode 100644 index 000000000..c38273fb2 --- /dev/null +++ b/modules/recetas/recetasInsumos/recetaInsumosController.ts @@ -0,0 +1,254 @@ +import * as moment from 'moment'; +import { Types } from 'mongoose'; +import { Auth } from '../../../auth/auth.class'; +import { RecetaInsumo } from './receta-insumo.schema'; +import { createLog, informarLog, updateLog } from '../recetaLogs'; +import { ParamsIncorrect, RecetaNotFound } from '../recetas.error'; +import { Paciente } from '../../../core-v2/mpi/paciente/paciente.schema'; +import { Profesional } from '../../../core/tm/schemas/profesional'; +import { getProfesionActualizada } from '../recetasController'; + +export async function buscarRecetasInsumos(req) { + const options: any = {}; + const params = req.params.id ? req.params : req.query; + const pacienteId = params.pacienteId || null; + const documento = params.documento || null; + const sexo = params.sexo || null; + try { + if ((!pacienteId && (!documento || !sexo)) || (pacienteId && !Types.ObjectId.isValid(pacienteId))) { + throw new ParamsIncorrect(); + } + const paramMap = { + id: '_id', + pacienteId: 'paciente.id', + documento: 'paciente.documento', + sexo: 'paciente.sexo', + estado: 'estadoActual.tipo' + }; + Object.keys(paramMap).forEach(key => { + if (params[key]) { + options[paramMap[key]] = key === 'id' ? Types.ObjectId(params[key]) : params[key]; + } + }); + + if (params.estadoDispensa) { + const estadoDispensaArray = params.estadoDispensa.split(','); + options['estadoDispensaActual.tipo'] = { $in: estadoDispensaArray }; + } else { + options['estadoDispensaActual.tipo'] = 'sin-dispensa'; + } + + if (params.fechaInicio || params.fechaFin) { + const fechaInicio = params.fechaInicio ? moment(params.fechaInicio).startOf('day').toDate() : moment().subtract(1, 'years').startOf('day').toDate(); + const fechaFin = params.fechaFin ? moment(params.fechaFin).endOf('day').toDate() : moment().endOf('day').toDate(); + options['fechaRegistro'] = { $gte: fechaInicio, $lte: fechaFin }; + } + if (Object.keys(options).length === 0) { + throw new ParamsIncorrect(); + } + const recetasInsumos: any = await RecetaInsumo.find(options); + return recetasInsumos; + } catch (err) { + await informarLog.error('buscarRecetasInsumos', { params, options }, err, req); + return err; + } +} + +export async function create(req) { + const pacienteRecetar = req.body.paciente; + const profRecetar = req.body.profesional; + const dataRecetaInsumo = { + insumo: req.body.insumo, + idPrestacion: req.body.idPrestacion, + idRegistro: req.body.idRegistro || req.body.idPrestacion, + fechaRegistro: null, + fechaPrestacion: null, + paciente: null, + profesional: null, + organizacion: req.body.organizacion, + diagnostico: null, + origenExterno: null + }; + try { + dataRecetaInsumo.fechaRegistro = dataRecetaInsumo.fechaRegistro ? moment(dataRecetaInsumo.fechaRegistro).toDate() : moment().toDate(); + dataRecetaInsumo.fechaPrestacion = dataRecetaInsumo.fechaPrestacion ? dataRecetaInsumo.fechaPrestacion : dataRecetaInsumo.fechaRegistro; + const insumoIncompleto = !req.body.insumo || (!req.body.insumo.concepto?.conceptId && !req.body.insumo.generico?.id); + dataRecetaInsumo.origenExterno = { + id: req.body.origenExterno?.id || '', + app: req.user.app?.nombre.toLowerCase() || '', + fecha: req.body.origenExterno?.fecha ? new Date(req.body.origenExterno.fecha) : dataRecetaInsumo.fechaRegistro, + }; + if (insumoIncompleto) { + throw new ParamsIncorrect('Faltan datos del insumo'); + } else { + const query: any = { + idRegistro: dataRecetaInsumo.idRegistro + }; + if (dataRecetaInsumo.insumo.generico) { + query['insumo.insumo'] = dataRecetaInsumo.insumo.generico.insumo; + } else { + query['insumo.concepto.conceptId'] = dataRecetaInsumo.insumo.concepto.conceptId; + } + const recetaInsumo = await RecetaInsumo.findOne(query); + if (recetaInsumo) { + throw new ParamsIncorrect('Receta de insumo ya registrada'); + } + } + if (!req.body.idPrestacion || !dataRecetaInsumo.organizacion) { + throw new ParamsIncorrect('Faltan datos de la receta de insumo'); + } + if (!pacienteRecetar || !pacienteRecetar.id) { + throw new ParamsIncorrect('Faltan datos del paciente'); + } else { + const pacienteAndes: any = await Paciente.findById(pacienteRecetar.id); + if (!pacienteAndes) { + throw new ParamsIncorrect('Paciente no encontrado'); + } else { + pacienteAndes.obraSocial = (!pacienteRecetar.obraSocial) ? null : + { + origen: pacienteRecetar.obraSocial.otraOS ? 'RECETAR' : 'PUCO', + nombre: pacienteRecetar.obraSocial.nombre, + financiador: pacienteRecetar.obraSocial.nombre, + codigoPuco: pacienteRecetar.obraSocial.codigoPuco || null, + numeroAfiliado: pacienteRecetar.obraSocial.numeroAfiliado || null + }; + } + dataRecetaInsumo.paciente = pacienteAndes; + } + if (!profRecetar || !profRecetar.id) { + throw new ParamsIncorrect('Faltan datos del profesional'); + } else { + const profAndes = await Profesional.findById(profRecetar.id); + if (!profAndes) { + throw new ParamsIncorrect('Profesional no encontrado'); + } + const { profesionGrado, matriculaGrado, especialidades } = await getProfesionActualizada(profRecetar.id); + dataRecetaInsumo.profesional = { + _id: profAndes._id, + id: profAndes._id, + nombre: profAndes.nombre, + apellido: profAndes.apellido, + documento: profAndes.documento, + profesion: profesionGrado, + especialidad: especialidades, + matricula: matriculaGrado + }; + } + return await crearRecetaInsumo(dataRecetaInsumo, req); + } catch (err) { + createLog.error('create', { dataRecetaInsumo, pacienteRecetar, profRecetar }, err, req); + return err; + } +} + +export async function crearRecetaInsumo(dataRecetaInsumo, req) { + try { + const recetaInsumo: any = new RecetaInsumo(); + recetaInsumo.idPrestacion = dataRecetaInsumo.idPrestacion; + recetaInsumo.idRegistro = dataRecetaInsumo.idRegistro; + const diag = dataRecetaInsumo.insumo?.diagnostico; + recetaInsumo.diagnostico = (typeof diag === 'string') ? { descripcion: diag } : diag; + if (dataRecetaInsumo.insumo.generico) { + recetaInsumo.insumo = { + ...dataRecetaInsumo.insumo.generico, + cantidad: dataRecetaInsumo.insumo.cantidad, + especificacion: dataRecetaInsumo.insumo.especificacion + }; + } else { + recetaInsumo.insumo = dataRecetaInsumo.insumo; + } + recetaInsumo.estados = [{ tipo: 'vigente' }]; + recetaInsumo.estadosDispensa = [{ tipo: 'sin-dispensa', fecha: moment().toDate() }]; + recetaInsumo.paciente = dataRecetaInsumo.paciente; + recetaInsumo.paciente.obraSocial = dataRecetaInsumo.paciente.obraSocial; + recetaInsumo.paciente.id = dataRecetaInsumo.paciente.id || dataRecetaInsumo.paciente._id; + recetaInsumo.profesional = dataRecetaInsumo.profesional; + recetaInsumo.profesional._id = dataRecetaInsumo.profesional.id || dataRecetaInsumo.profesional._id; + recetaInsumo.organizacion = dataRecetaInsumo.organizacion; + recetaInsumo.fechaRegistro = moment(dataRecetaInsumo.fechaRegistro).toDate(); + recetaInsumo.fechaPrestacion = moment(dataRecetaInsumo.fechaPrestacion).toDate(); + if (dataRecetaInsumo.origenExterno) { + recetaInsumo.origenExterno = dataRecetaInsumo.origenExterno; + } + if (req.user) { + Auth.audit(recetaInsumo, req as any); + } else { + recetaInsumo.audit(req); + } + await recetaInsumo.save(); + return recetaInsumo; + } catch (err) { + createLog.error('crearRecetaInsumo', { dataRecetaInsumo }, err, req); + return err; + } +} + +export async function buscarRecetasInsumosPorProfesional(req) { + try { + const profesionalId = req.params.id; + const { estadoReceta, desde, hasta, origenExternoApp, excluirEstado } = req.query; + if (!profesionalId || !Types.ObjectId.isValid(profesionalId)) { + throw new ParamsIncorrect(); + } + const filter: any = { + 'profesional.id': Types.ObjectId(profesionalId) + }; + if (estadoReceta) { + filter['estadoActual.tipo'] = estadoReceta; + } + if (desde || hasta) { + filter['fechaRegistro'] = {}; + if (desde) { + filter['fechaRegistro'].$gte = moment(desde).startOf('day').toDate(); + } + if (hasta) { + filter['fechaRegistro'].$lte = moment(hasta).endOf('day').toDate(); + } + } + if (origenExternoApp) { + filter['origenExterno.app'] = origenExternoApp; + } + if (excluirEstado) { + filter['estadoActual.tipo'] = { $ne: excluirEstado }; + } + const recetasInsumos = await RecetaInsumo.find(filter); + return recetasInsumos; + } catch (err) { + await informarLog.error('buscarRecetasInsumosPorProfesional', { params: req.params, query: req.query }, err); + return err; + } +} + +export async function suspender(recetaInsumoId, req) { + const motivo = req.body.motivo; + const observacion = req.body.observacion; + const profesional = req.body.profesional; + try { + const recetaInsumo: any = await RecetaInsumo.findById(recetaInsumoId); + if (!recetaInsumo) { + throw new RecetaNotFound(); + } + const recetasASuspender = await RecetaInsumo.find({ + 'insumo.concepto.conceptId': recetaInsumo.insumo.concepto.conceptId, + idRegistro: recetaInsumo.idRegistro + }); + const promises = recetasASuspender.map(async (receta: any) => { + if ((receta.estadoActual.tipo === 'vigente') || (receta.estadoDispensaActual.tipo !== 'dispensa-parcial' && receta.estadoActual.tipo === 'pendiente')) { + receta.estados.push({ + tipo: 'suspendida', + motivo, + observacion, + profesional, + fecha: new Date() + }); + Auth.audit(receta, req); + await receta.save(); + } + }); + await Promise.all(promises); + return { success: true }; + } catch (error) { + await updateLog.error('suspender', { motivo, observacion, profesional, recetaInsumoId }, error); + return error; + } +}