Skip to content

Commit be77098

Browse files
committed
feat(REC-249): Creacion de recetas Andes: Generar colección Auxiliar
1 parent bea014e commit be77098

5 files changed

Lines changed: 568 additions & 6 deletions

File tree

jobs/jobRecetaControl.ts

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
import * as moment from 'moment';
2+
import { Prestacion } from '../modules/rup/schemas/prestacion';
3+
import { Profesional } from '../core/tm/schemas/profesional';
4+
import { Receta } from '../modules/recetas/receta-schema';
5+
import { RecetaInsumo } from '../modules/recetas/recetasInsumos/receta-insumo.schema';
6+
import { RecetaControl } from '../modules/recetas/receta-control-schema';
7+
import { crearReceta, getProfesionActualizada } from '../modules/recetas/recetasController';
8+
import { crearRecetaInsumo } from '../modules/recetas/recetasInsumos/recetaInsumosController';
9+
import { generarCUIL } from '../core-v2/mpi/validacion/validacion.controller';
10+
import { jobsLog as logger } from '../modules/recetas/recetaLogs';
11+
import { userScheduler } from '../config.private';
12+
13+
/**
14+
* Job: generarRecetasFaltantes
15+
*
16+
* Recorre la colección auxiliar `recetaPendiente` buscando registros
17+
* `realizada: false` dentro de un rango de fechas/horas, reconstruye los
18+
* datos necesarios desde `Prestacion` y crea la receta faltante.
19+
*
20+
* Soporta tres tipos de prescripción:
21+
* - 'medicamento' y 'magistral' -> comparten flujo/colección (Receta / crearReceta),
22+
* se diferencian internamente por `medicamento.tipoReceta`.
23+
* - 'insumo' -> flujo/colección separados (RecetaInsumo / crearRecetaInsumo).
24+
*
25+
* Parametrización (recibida por args, ver bin/jobsDaemon.js):
26+
* --desde YYYY-MM-DD[THH:mm] (opcional)
27+
* --hasta YYYY-MM-DD[THH:mm] (opcional, default: ahora)
28+
* --horas N (opcional, alternativa a --desde: últimas N horas)
29+
*
30+
* Uso típico (jobsDaemon):
31+
* node bin/jobsDaemon.js generarRecetasFaltantes --horas 6
32+
* node bin/jobsDaemon.js generarRecetasFaltantes --desde 2024-05-01 --hasta 2024-05-02
33+
*/
34+
35+
interface ParamsJob {
36+
desde?: string;
37+
hasta?: string;
38+
horas?: number;
39+
}
40+
41+
function resolverRangoFechas(params: ParamsJob) {
42+
const hasta = params.hasta ? moment(params.hasta) : moment();
43+
44+
let desde;
45+
if (params.desde) {
46+
desde = moment(params.desde);
47+
} else {
48+
const horas = params.horas && Number(params.horas) > 0 ? Number(params.horas) : 24;
49+
desde = moment(hasta).subtract(horas, 'hours');
50+
}
51+
52+
return { desde: desde.toDate(), hasta: hasta.toDate() };
53+
}
54+
55+
/**
56+
* Datos comunes a los tres tipos de prescripción: prestación, registro RUP
57+
* y organización. Reconstruye lo mismo que arman los listeners
58+
* `prestacion:receta:create` y `prestacion:recetaInsumo:create` antes de
59+
* llamar a crearReceta/crearRecetaInsumo.
60+
*/
61+
async function reconstruirContexto(pendiente: any) {
62+
const prestacion: any = await Prestacion.findById(pendiente.idPrestacion);
63+
if (!prestacion) {
64+
throw new Error(`No se encontró la prestación ${pendiente.idPrestacion}`);
65+
}
66+
67+
// Supuesto a confirmar: tanto medicamentos como magistrales viven en el mismo
68+
// array `registro.valor.medicamentos` (se diferencian por `tipoReceta`), e insumos
69+
// en `registro.valor.insumos`. Si el modelo real difiere, ajustar este lookup.
70+
const registro = (prestacion.ejecucion?.registros || []).find((r: any) => String(r._id) === String(pendiente.idRegistro))
71+
|| (prestacion.evolucion || []).find((r: any) => String(r._id) === String(pendiente.idRegistro))
72+
|| (prestacion.solicitud?.registros || []).find((r: any) => String(r._id) === String(pendiente.idRegistro));
73+
74+
if (!registro) {
75+
throw new Error(`No se encontró el registro ${pendiente.idRegistro} en la prestación ${pendiente.idPrestacion}`);
76+
}
77+
78+
const organizacion = {
79+
id: prestacion.ejecucion.organizacion.id,
80+
nombre: prestacion.ejecucion.organizacion.nombre
81+
};
82+
83+
return { prestacion, registro, organizacion };
84+
}
85+
86+
/** Resuelve profesional para medicamento/magistral: por documento del createdBy o de la solicitud. */
87+
async function resolverProfesionalReceta(prestacion: any) {
88+
const documentoProfesional = prestacion.estadoActual.createdBy?.documento
89+
? prestacion.estadoActual.createdBy?.documento
90+
: prestacion.solicitud.profesional.documento;
91+
92+
const profPrestacion = await Profesional.findOne({ documento: documentoProfesional });
93+
if (!profPrestacion) {
94+
throw new Error(`No se encontró el profesional con documento ${documentoProfesional}`);
95+
}
96+
97+
const { profesionGrado, matriculaGrado, especialidades } = await getProfesionActualizada(profPrestacion);
98+
99+
return {
100+
id: profPrestacion.id,
101+
nombre: profPrestacion.nombre,
102+
apellido: profPrestacion.apellido,
103+
documento: profPrestacion.documento,
104+
profesion: profesionGrado,
105+
especialidad: especialidades,
106+
matricula: matriculaGrado
107+
};
108+
}
109+
110+
/** Resuelve profesional para insumo: directamente desde prestacion.solicitud.profesional (igual que el listener). */
111+
async function resolverProfesionalInsumo(prestacion: any) {
112+
const profPrestacion = prestacion.solicitud.profesional;
113+
const { profesionGrado, matriculaGrado, especialidades } = await getProfesionActualizada(profPrestacion.id);
114+
115+
return {
116+
id: profPrestacion.id,
117+
nombre: profPrestacion.nombre,
118+
apellido: profPrestacion.apellido,
119+
documento: profPrestacion.documento,
120+
profesion: profesionGrado,
121+
especialidad: especialidades,
122+
matricula: matriculaGrado
123+
};
124+
}
125+
126+
/** Rama medicamento/magistral: comparten Receta + crearReceta. */
127+
async function crearFaltantesMedicamento(pendiente: any) {
128+
const { prestacion, registro, organizacion } = await reconstruirContexto(pendiente);
129+
const profesional = await resolverProfesionalReceta(prestacion);
130+
131+
const pacienteCUIL = prestacion.paciente.cuil || generarCUIL(prestacion.paciente.documento, prestacion.paciente.sexo);
132+
const pacienteObj = (typeof prestacion.paciente.toObject === 'function') ? prestacion.paciente.toObject() : prestacion.paciente;
133+
const paciente = { ...pacienteObj, id: pacienteObj.id || pacienteObj._id || prestacion.paciente.id || prestacion.paciente._id, cuil: pacienteCUIL };
134+
135+
const dataRecetaBase = {
136+
idPrestacion: prestacion.id,
137+
idRegistro: pendiente.idRegistro,
138+
fechaRegistro: prestacion.ejecucion.fecha || moment().toDate(),
139+
fechaPrestacion: prestacion.ejecucion.fecha,
140+
paciente,
141+
profesional,
142+
organizacion,
143+
medicamento: null,
144+
diagnostico: null,
145+
};
146+
147+
const medicamentos = registro.valor?.medicamentos || [];
148+
const recetasCreadas = [];
149+
for (const medicamento of medicamentos) {
150+
const conceptId = medicamento?.concepto?.conceptId || medicamento?.generico?.conceptId;
151+
if (!conceptId) {
152+
continue;
153+
}
154+
155+
const yaExiste = await Receta.findOne({
156+
'medicamento.concepto.conceptId': conceptId,
157+
idRegistro: pendiente.idRegistro
158+
});
159+
160+
if (yaExiste) {
161+
recetasCreadas.push(yaExiste);
162+
continue;
163+
}
164+
165+
const dataReceta = {
166+
...dataRecetaBase,
167+
medicamento,
168+
diagnostico: medicamento?.diagnostico || null,
169+
};
170+
171+
const creadas = await crearReceta(dataReceta, userScheduler);
172+
if (Array.isArray(creadas)) {
173+
recetasCreadas.push(...creadas);
174+
}
175+
}
176+
177+
if (!recetasCreadas.length) {
178+
throw new Error('No se pudo crear ninguna receta de medicamento/magistral (sin items válidos en el registro)');
179+
}
180+
181+
return recetasCreadas;
182+
}
183+
184+
/** Rama insumo: RecetaInsumo + crearRecetaInsumo. */
185+
async function crearFaltantesInsumo(pendiente: any) {
186+
const { prestacion, registro, organizacion } = await reconstruirContexto(pendiente);
187+
const profesional = await resolverProfesionalInsumo(prestacion);
188+
189+
const pacienteObj = (typeof prestacion.paciente.toObject === 'function') ? prestacion.paciente.toObject() : prestacion.paciente;
190+
const paciente = { ...pacienteObj, id: pacienteObj.id || pacienteObj._id || prestacion.paciente.id || prestacion.paciente._id };
191+
192+
const dataRecetaBase = {
193+
idPrestacion: prestacion.id,
194+
idRegistro: pendiente.idRegistro,
195+
fechaRegistro: prestacion.ejecucion.fecha || moment().toDate(),
196+
fechaPrestacion: prestacion.ejecucion.fecha,
197+
paciente,
198+
profesional,
199+
organizacion,
200+
insumo: null,
201+
diagnostico: null,
202+
};
203+
204+
const insumos = registro.valor?.insumos || [];
205+
const recetasCreadas = [];
206+
207+
for (const insumo of insumos) {
208+
const idGenerico = insumo?.generico?.id;
209+
const nombreGenerico = insumo?.generico?.nombre;
210+
if (!idGenerico || !nombreGenerico) {
211+
continue;
212+
}
213+
214+
const yaExiste = await RecetaInsumo.findOne({
215+
'insumo.id': idGenerico,
216+
'insumo.nombre': nombreGenerico,
217+
idRegistro: pendiente.idRegistro
218+
});
219+
220+
if (yaExiste) {
221+
recetasCreadas.push(yaExiste);
222+
continue;
223+
}
224+
225+
const dataRecetaInsumo = {
226+
...dataRecetaBase,
227+
insumo
228+
};
229+
230+
const creadas = await crearRecetaInsumo(dataRecetaInsumo, { user: userScheduler });
231+
if (Array.isArray(creadas)) {
232+
recetasCreadas.push(...creadas);
233+
}
234+
}
235+
236+
if (!recetasCreadas.length) {
237+
throw new Error('No se pudo crear ninguna receta de insumo (sin items válidos en el registro)');
238+
}
239+
240+
return recetasCreadas;
241+
}
242+
243+
async function crearRecetaFaltante(pendiente: any) {
244+
switch (pendiente.tipoPrescripcion) {
245+
case 'medicamento':
246+
case 'magistrales':
247+
return crearFaltantesMedicamento(pendiente);
248+
case 'insumos':
249+
return crearFaltantesInsumo(pendiente);
250+
default:
251+
throw new Error(`tipoPrescripcion '${pendiente.tipoPrescripcion}' no soportado por el job`);
252+
}
253+
}
254+
255+
async function generarRecetasFaltantes(params: ParamsJob | any, done?: any) {
256+
let callback = done;
257+
let actualParams = params;
258+
259+
if (typeof params === 'function') {
260+
callback = params;
261+
actualParams = {};
262+
}
263+
264+
if (!actualParams || Object.keys(actualParams).length === 0) {
265+
actualParams = {};
266+
process.argv.forEach(val => {
267+
if (val.startsWith('--desde=')) {
268+
actualParams.desde = val.split('=')[1];
269+
} else if (val === '--desde') {
270+
const idx = process.argv.indexOf('--desde');
271+
if (idx !== -1 && process.argv[idx + 1]) {
272+
actualParams.desde = process.argv[idx + 1];
273+
}
274+
}
275+
if (val.startsWith('--hasta=')) {
276+
actualParams.hasta = val.split('=')[1];
277+
} else if (val === '--hasta') {
278+
const idx = process.argv.indexOf('--hasta');
279+
if (idx !== -1 && process.argv[idx + 1]) {
280+
actualParams.hasta = process.argv[idx + 1];
281+
}
282+
}
283+
if (val.startsWith('--horas=')) {
284+
actualParams.horas = parseInt(val.split('=')[1], 10);
285+
} else if (val === '--horas') {
286+
const idx = process.argv.indexOf('--horas');
287+
if (idx !== -1 && process.argv[idx + 1]) {
288+
actualParams.horas = parseInt(process.argv[idx + 1], 10);
289+
}
290+
}
291+
});
292+
}
293+
294+
const { desde, hasta } = resolverRangoFechas(actualParams);
295+
296+
const resultados = {
297+
total: 0,
298+
creadas: 0,
299+
errores: 0,
300+
porTipo: { medicamento: 0, magistrales: 0, insumos: 0 }
301+
};
302+
303+
try {
304+
const query = {
305+
creada: false,
306+
createdAt: { $gte: desde, $lte: hasta }
307+
};
308+
309+
const cursor = RecetaControl.find(query).cursor({ batchSize: 50 });
310+
311+
await cursor.eachAsync(async (pendiente: any) => {
312+
resultados.total++;
313+
try {
314+
const recetas = await crearRecetaFaltante(pendiente);
315+
316+
pendiente.creada = true;
317+
pendiente.idReceta = recetas[0]?.idReceta || recetas[0]?._id?.toString() || null;
318+
if (typeof (pendiente as any).audit === 'function') {
319+
(pendiente as any).audit(userScheduler);
320+
}
321+
await pendiente.save();
322+
323+
resultados.creadas++;
324+
resultados.porTipo[pendiente.tipoPrescripcion] = (resultados.porTipo[pendiente.tipoPrescripcion] || 0) + 1;
325+
} catch (error) {
326+
resultados.errores++;
327+
await logger.error('generarRecetasFaltantes', { idPendiente: pendiente.id, tipoPrescripcion: pendiente.tipoPrescripcion }, error);
328+
}
329+
}, { parallel: 5 });
330+
331+
await logger.info('generarRecetasFaltantes', { desde, hasta, resultados });
332+
} catch (err) {
333+
await logger.error('generarRecetasFaltantes', { desde, hasta, resultados }, err);
334+
if (callback) {
335+
return callback(err);
336+
}
337+
return;
338+
}
339+
if (callback) {
340+
callback();
341+
}
342+
}
343+
344+
export = generarRecetasFaltantes;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { AuditPlugin } from '@andes/mongoose-plugin-audit';
2+
import * as mongoose from 'mongoose';
3+
4+
export const recetaControlSchema = new mongoose.Schema({
5+
idPrestacion: {
6+
type: String,
7+
required: true
8+
},
9+
idRegistro: {
10+
type: String,
11+
required: true
12+
},
13+
idPaciente: {
14+
type: String,
15+
required: true
16+
},
17+
tipoPrescripcion: {
18+
type: String,
19+
enum: ['medicamento', 'insumos', 'magistrales'],
20+
required: true
21+
},
22+
creada: {
23+
type: Boolean,
24+
default: false,
25+
required: true
26+
},
27+
idReceta: {
28+
type: String,
29+
required: false
30+
},
31+
conceptId: {
32+
type: String,
33+
required: false
34+
},
35+
insumoId: {
36+
type: String,
37+
required: false
38+
},
39+
ordenTratamiento: {
40+
type: Number,
41+
required: false
42+
}
43+
});
44+
45+
recetaControlSchema.plugin(AuditPlugin);
46+
47+
recetaControlSchema.index({ idPrestacion: 1 });
48+
recetaControlSchema.index({ idRegistro: 1 });
49+
recetaControlSchema.index({ idPaciente: 1 });
50+
51+
export const RecetaControl = mongoose.model('recetaControl', recetaControlSchema, 'recetaControl');

0 commit comments

Comments
 (0)