From f7b5179cf84fad4e4fa0cf92e9030b53e1e273ce Mon Sep 17 00:00:00 2001 From: aldoEMatamala <123381977+aldoEMatamala@users.noreply.github.com> Date: Tue, 12 May 2026 09:23:22 -0300 Subject: [PATCH 1/8] =?UTF-8?q?feat(REC-181):=20Esquema=20paciente:=20limp?= =?UTF-8?q?iar=20datos=20que=20no=20se=20est=C3=A1n=20usando=20(#84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interfaces/patient.interface.ts | 14 +++++++++++ src/models/certificate.model.ts | 8 +++---- src/models/patient.model.ts | 37 +++++++++++++++++++++++++++++ src/models/practice.model.ts | 4 ++-- src/models/prescription.model.ts | 4 ++-- 5 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/interfaces/patient.interface.ts b/src/interfaces/patient.interface.ts index 607840c..a5caf98 100644 --- a/src/interfaces/patient.interface.ts +++ b/src/interfaces/patient.interface.ts @@ -1,6 +1,20 @@ import { Document } from 'mongoose'; import IObraSocial from './obraSocial.interface'; +export interface IPatientSub { + dni?: string; + lastName: string; + firstName: string; + fechaNac?: Date | null; + sex: string; + nombreAutopercibido?: string; + idMPI?: string; + obraSocial?: { + nombre?: string; + numeroAfiliado?: string; + } | IObraSocial; +} + export default interface IPatient extends Document { dni?: string; lastName: string; diff --git a/src/models/certificate.model.ts b/src/models/certificate.model.ts index 71c6386..24d72f4 100644 --- a/src/models/certificate.model.ts +++ b/src/models/certificate.model.ts @@ -1,10 +1,10 @@ -import { Schema, Model, model } from 'mongoose'; -import { patientSchema } from './patient.model'; -import ICertificate from '../interfaces/certificate.interface'; +import { Schema, Model, model } from "mongoose"; +import { patientSubSchema } from "./patient.model"; +import ICertificate from "../interfaces/certificate.interface"; const certificateSchema = new Schema({ - patient: patientSchema, + patient: patientSubSchema, professional: { userId: Schema.Types.ObjectId, businessName: { type: String, required: true }, diff --git a/src/models/patient.model.ts b/src/models/patient.model.ts index f32156b..42aef16 100644 --- a/src/models/patient.model.ts +++ b/src/models/patient.model.ts @@ -4,6 +4,43 @@ import needle from 'needle'; import { obraSocialSchema } from './obraSocial.model'; import axios from 'axios'; + +export const patientSubSchema = new Schema({ + firstName: { + type: String, + required: '{PATH} is required' + }, + lastName: { + type: String, + required: '{PATH} is required' + }, + nombreAutopercibido: { + type: String, + default: '' + }, + dni: { + type: String, + default: '' + }, + fechaNac: { + type: Date, + default: null + }, + sex: { + type: String, + enum: ['Femenino', 'Masculino', 'Otro'], + required: '{PATH} is required' + }, + obraSocial: { + nombre: { type: String, default: '' }, + numeroAfiliado: { type: String, default: '' } + }, + idMPI: { + type: String, + default: '' + } +}, { _id: false }); + // Schema export const patientSchema = new Schema({ dni: { diff --git a/src/models/practice.model.ts b/src/models/practice.model.ts index b843ad0..ce95d5e 100644 --- a/src/models/practice.model.ts +++ b/src/models/practice.model.ts @@ -1,13 +1,13 @@ import { Schema, Model, model } from 'mongoose'; import IPractice from '../interfaces/practice.interface'; -import { patientSchema } from './patient.model'; +import { patientSubSchema } from './patient.model'; const practiceSchema: Schema = new Schema({ date: { type: Date, required: true }, - patient: patientSchema, + patient: patientSubSchema, professional: { userId: { type: String, diff --git a/src/models/prescription.model.ts b/src/models/prescription.model.ts index b70056e..c76de4c 100644 --- a/src/models/prescription.model.ts +++ b/src/models/prescription.model.ts @@ -1,7 +1,7 @@ import { Schema, Model, model } from 'mongoose'; import IPrescription from '../interfaces/prescription.interface'; import { supplySchema } from '../models/supply.model'; -import { patientSchema } from '../models/patient.model'; +import { patientSubSchema } from '../models/patient.model'; // Schema const prescriptionSchema = new Schema({ @@ -10,7 +10,7 @@ const prescriptionSchema = new Schema({ unique: true, sparse: true }, - patient: patientSchema, + patient: patientSubSchema, professional: { userId: Schema.Types.ObjectId, businessName: { type: String, required: true }, From fccd64ac9ae79be8384544e2a8209b1792f0de83 Mon Sep 17 00:00:00 2001 From: ma7payne Date: Tue, 12 May 2026 09:40:02 -0300 Subject: [PATCH 2/8] =?UTF-8?q?feat(REC):=20cambio=20de=20email=20con=20co?= =?UTF-8?q?nfirmaci=C3=B3n=20(#82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/users.controller.ts | 127 ++++++++++++++++++++++++- src/interfaces/user.interface.ts | 3 + src/models/user.model.ts | 9 ++ src/routes/private.ts | 1 + src/routes/public.ts | 4 +- src/templates/emails/update-email.html | 13 +++ 6 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 src/templates/emails/update-email.html diff --git a/src/controllers/users.controller.ts b/src/controllers/users.controller.ts index 644d795..2abe4ad 100644 --- a/src/controllers/users.controller.ts +++ b/src/controllers/users.controller.ts @@ -5,7 +5,7 @@ import { renderHTML, MailOptions, sendMail } from '../utils/roboSender/sendEmail import Role from '../models/role.model'; import IRole from '../interfaces/role.interface'; import axios from 'axios'; - +import crypto from 'crypto'; class UsersController { public index = async (req: Request, res: Response): Promise => { try { @@ -46,6 +46,102 @@ class UsersController { } }; + public requestEmailUpdate = async (req: Request, res: Response): Promise => { + try { + const { email, userId } = req.body; + + if (!email) { + return res.status(400).json({ mensaje: 'Email requerido' }); + } + + // Verificar si el email ya existe en otro usuario + const emailExists = await this.validateEmailUniqueness(email, userId); + if (emailExists) { + return res.status(400).json({ mensaje: 'El email ya está registrado por otro usuario' }); + } + + // Generar token y expiración + const token = crypto.randomBytes(20).toString('hex'); + const expires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 horas + + const user = await User.findByIdAndUpdate(userId, { + pendingEmail: email, + emailConfirmationToken: token, + emailConfirmationExpires: expires + }, { new: true }); + + if (!user) { + return res.status(404).json({ mensaje: 'Usuario no encontrado' }); + } + + // Enviar email de confirmación + await this.sendEmailUpdateConfirmation(user, email, token); + + return res.status(200).json({ mensaje: 'Se ha enviado un correo de confirmación a la nueva dirección.' }); + + } catch (e) { + return res.status(500).json({ mensaje: `${e}` }); + } + }; + + public confirmEmailUpdate = async (req: Request, res: Response): Promise => { + try { + const { token } = req.body; + + if (!token) { + return res.status(400).json({ mensaje: 'Token requerido' }); + } + + // Buscar usuario con el token y verificar expiración + const user = await User.findOne({ + emailConfirmationToken: token, + emailConfirmationExpires: { $gt: new Date() } + }).populate('roles'); + + if (!user) { + return res.status(400).json({ mensaje: 'Token inválido o expirado' }); + } + + // Aplicar el cambio + user.email = user.pendingEmail!; + + // Si es farmacia, actualizamos también el username + const isPharmacy = user.roles.some((role: any) => role.role === 'pharmacist'); + if (isPharmacy) { + user.username = user.pendingEmail!; + } + + user.pendingEmail = undefined; + user.emailConfirmationToken = undefined; + user.emailConfirmationExpires = undefined; + + await user.save(); + + return res.status(200).json({ mensaje: 'Email actualizado correctamente' }); + + } catch (e) { + return res.status(500).json({ mensaje: `${e}` }); + } + }; + + /** +* Valida que el email no esté siendo usado por otro usuario +* @param email - Email a validar +* @param userId - ID del usuario actual (para excluirlo de la búsqueda) +* @returns true si el email ya existe, false si está disponible +*/ + private validateEmailUniqueness = async (email: string, userId: string): Promise => { + try { + const existingUser = await User.findOne({ + email, + _id: { $ne: userId } + }); + return !!existingUser; + } catch (error) { + throw new Error(`Error validating email uniqueness: ${error}`); + } + }; + public getUserInfo = async (req: Request, res: Response): Promise => { // obtenemos la información personal del usuario por su ID const { id } = req.params; @@ -473,6 +569,35 @@ class UsersController { return res.status(500).json('Server Error'); } }; + + /** + * Envía un email con el token para confirmar el cambio de email + * @param user - Usuario + * @param newEmail - Nuevo email + * @param token - Token de confirmación + */ + private sendEmailUpdateConfirmation = async (user: IUser, newEmail: string, token: string): Promise => { + try { + const extras: any = { + titulo: 'Confirmar cambio de email', + usuario: user, + url: `${process.env.APP_DOMAIN || 'https://recetar.andes.gob.ar'}/auth/confirm-update/${token}` + }; + + const htmlToSend = await renderHTML('emails/update-email.html', extras); + const options: MailOptions = { + from: `${process.env.EMAIL_USERNAME}`, + to: newEmail, + subject: 'Confirmar cambio de email - RecetAR', + text: '', + html: htmlToSend, + attachments: null + }; + await sendMail(options); + } catch (error) { + console.error('Error enviando confirmación de cambio de email:', error); + } + }; }; export default new UsersController; diff --git a/src/interfaces/user.interface.ts b/src/interfaces/user.interface.ts index 747e96d..c95cc9f 100644 --- a/src/interfaces/user.interface.ts +++ b/src/interfaces/user.interface.ts @@ -4,6 +4,9 @@ import IProfesionAutorizada from './profesionAutorizada.interface'; export default interface IUser extends Document { username: string; email: string; + pendingEmail?: string; + emailConfirmationToken?: string; + emailConfirmationExpires?: Date; businessName: string; enrollment?: string; cuil?: string; diff --git a/src/models/user.model.ts b/src/models/user.model.ts index 4378391..aaa78f6 100644 --- a/src/models/user.model.ts +++ b/src/models/user.model.ts @@ -40,6 +40,15 @@ export const userSchema = new Schema({ email: { type: String }, + pendingEmail: { + type: String + }, + emailConfirmationToken: { + type: String + }, + emailConfirmationExpires: { + type: Date + }, enrollment: { type: String }, diff --git a/src/routes/private.ts b/src/routes/private.ts index a7a719b..190536d 100644 --- a/src/routes/private.ts +++ b/src/routes/private.ts @@ -107,6 +107,7 @@ class PrivateRoutes { this.router.post('/users/create', hasPermissionIn('createAny', 'user'), usersController.create); this.router.post('/users/update', hasPermissionIn('updateAny', 'user'), usersController.update); this.router.post('/users/update-own', hasPermissionIn('updateOwn', 'user'), usersController.updateOwn); + this.router.post('/users/request-update', hasPermissionIn('updateOwn', 'user'), usersController.requestEmailUpdate); this.router.get('/organizaciones-andes', hasPermissionIn('readOwn', 'user'), usersController.organizacionesAndes); // pharmacy diff --git a/src/routes/public.ts b/src/routes/public.ts index ea1e7da..cb50296 100644 --- a/src/routes/public.ts +++ b/src/routes/public.ts @@ -1,13 +1,15 @@ import { Router } from 'express'; import certificateController from '../controllers/certificate.controller'; import practiceController from '../controllers/practice.controller'; +import usersController from '../controllers/users.controller'; class PublicRoutes{ constructor(private router: Router = Router()){} public routes(): Router{ + this.router.post('/users/confirm-update', usersController.confirmEmailUpdate); this.router.get('/certificates/:id', certificateController.getById); - this.router.get('/practices/:id', practiceController.getById); + this.router.get('/practices/:id', practiceController.getById); return this.router; } diff --git a/src/templates/emails/update-email.html b/src/templates/emails/update-email.html new file mode 100644 index 0000000..d871745 --- /dev/null +++ b/src/templates/emails/update-email.html @@ -0,0 +1,13 @@ +{{#> layout }} +
+ Hola {{ nombre }}!
+
+

+ Recibiste este mensaje porque solicitaste cambiar tu dirección de email. +

+
+ +Para confirmar este cambio, haz click en el siguiente enlace: Confirmar cambio de email +
+Este enlace expirará en 24 horas. +{{/layout}} From 89cf62f3f99555182fee7938494f8d0f573ee679 Mon Sep 17 00:00:00 2001 From: ma7payne Date: Tue, 12 May 2026 13:00:59 -0300 Subject: [PATCH 3/8] feat(REC): mejoras en auditoria de usuarios (#89) --- .../andesPrescription.controller.ts | 96 +------------- src/controllers/users.controller.ts | 11 +- src/interfaces/user.interface.ts | 3 + src/models/user.model.ts | 11 +- src/services/andesService.ts | 117 ++++++++++++++++++ 5 files changed, 142 insertions(+), 96 deletions(-) diff --git a/src/controllers/andesPrescription.controller.ts b/src/controllers/andesPrescription.controller.ts index a068e6a..7c667ba 100644 --- a/src/controllers/andesPrescription.controller.ts +++ b/src/controllers/andesPrescription.controller.ts @@ -83,105 +83,15 @@ class AndesPrescriptionController implements BaseController { }; public getFromAndes = async (req: Request, res: Response): Promise => { - try { - if (!req.query.dni) { return res.status(400).json({ mensaje: 'Missing required params!' }); } - const dni = req.query.dni as string; - const sexo = req.query.sexo ? (req.query.sexo as any) : undefined; - let prescriptions: IPrescriptionAndes[] | null = []; - let andesPrescriptions: IPrescriptionAndes[] | null = null; - - andesPrescriptions = await AndesService.getPrescriptionsByPatient({ documento: dni, estado: 'vigente', sexo }); - - if (andesPrescriptions) { - andesPrescriptions = andesPrescriptions.map(aPrescription => { - aPrescription.idAndes = aPrescription._id; - return aPrescription; - }); - prescriptions = [...prescriptions, ...andesPrescriptions]; - } - - const savedPrescriptions: IPrescriptionAndes[] | null = await PrescriptionAndes.find({ 'paciente.documento': dni }); - if (savedPrescriptions) { - prescriptions = [...prescriptions, ...savedPrescriptions]; - } - return res.status(200).json(prescriptions); - } catch (e) { - return res.status(500).json({ error: e }); - } + return AndesService.getFromAndes(req, res); }; public searchProfessionals = async (req: Request, res: Response): Promise => { - const { documento } = req.query; - - if (!documento) { - return res.status(400).json({ - ok: false, - message: 'El parámetro "documento" es requerido' - }); - } - - try { - const professionals = await AndesService.searchProfessionalsGuide(documento as string); - - if (professionals && professionals.length > 0) { - return res.status(200).json({ - ok: true, - message: 'Profesionales encontrados', - data: professionals, - total: professionals.length - }); - } else { - return res.status(200).json({ - ok: false, - message: 'No se encontraron profesionales con el documento proporcionado', - data: [], - total: 0 - }); - } - } catch (error) { - return res.status(500).json({ - ok: false, - message: 'Error al buscar profesionales en Andes', - error - }); - } + return AndesService.searchProfessionals(req, res); }; public searchPharmacies = async (req: Request, res: Response): Promise => { - const { cuit } = req.query; - - if (!cuit) { - return res.status(400).json({ - ok: false, - message: 'El parámetro "cuit" es requerido' - }); - } - - try { - const pharmacies = await AndesService.searchPharmaciesCore(cuit as string); - - if (pharmacies && pharmacies.length > 0) { - return res.status(200).json({ - ok: true, - message: 'Farmacias encontradas', - data: pharmacies, - total: pharmacies.length - }); - } else { - return res.status(200).json({ - ok: false, - message: 'No se encontraron farmacias con el CUIT proporcionado', - data: [], - total: 0 - }); - } - } catch (error) { - return res.status(500).json({ - ok: false, - message: 'Error al buscar farmacias en Andes', - error - }); - } + return AndesService.searchPharmacies(req, res); }; public dispense = async (req: Request, res: Response): Promise => { diff --git a/src/controllers/users.controller.ts b/src/controllers/users.controller.ts index 2abe4ad..fb60714 100644 --- a/src/controllers/users.controller.ts +++ b/src/controllers/users.controller.ts @@ -469,7 +469,11 @@ class UsersController { public create = async (req: Request, res: Response): Promise => { try { - const { email, username, businessName, enrollment, cuil, roles, password, idAndes, profesionGrado } = req.body; + const { + email, username, businessName, enrollment, cuil, + roles, password, idAndes, profesionGrado, + authorizationExpiration, authorizationDisposition, responsibleDTEnrollment + } = req.body; let finalUsername = username; @@ -515,7 +519,10 @@ class UsersController { ...(password && { password }), ...(roles && Array.isArray(roles) && { roles }), ...(idAndes !== undefined && { idAndes }), - ...(profesionGrado && Array.isArray(profesionGrado) && profesionGrado.length > 0 && { profesionGrado }) + ...(profesionGrado && Array.isArray(profesionGrado) && profesionGrado.length > 0 && { profesionGrado }), + ...(authorizationExpiration && { authorizationExpiration }), + ...(authorizationDisposition && { authorizationDisposition }), + ...(responsibleDTEnrollment && { responsibleDTEnrollment }) }); // Campos por defecto diff --git a/src/interfaces/user.interface.ts b/src/interfaces/user.interface.ts index c95cc9f..d29bc33 100644 --- a/src/interfaces/user.interface.ts +++ b/src/interfaces/user.interface.ts @@ -34,5 +34,8 @@ export default interface IUser extends Document { }]; isValidPassword(thisUser: IUser, password: string): Promise; idAndes?: string; + authorizationExpiration?: Date; + authorizationDisposition?: string; + responsibleDTEnrollment?: string; // eslint-disable-next-line semi } diff --git a/src/models/user.model.ts b/src/models/user.model.ts index aaa78f6..4c3a898 100644 --- a/src/models/user.model.ts +++ b/src/models/user.model.ts @@ -138,7 +138,16 @@ export const userSchema = new Schema({ nombre: String, direccion: String, } - ] + ], + authorizationExpiration: { + type: Date + }, + authorizationDisposition: { + type: String + }, + responsibleDTEnrollment: { + type: String + } }); // Model diff --git a/src/services/andesService.ts b/src/services/andesService.ts index a0608a0..7b91a7e 100644 --- a/src/services/andesService.ts +++ b/src/services/andesService.ts @@ -1,5 +1,7 @@ import axios, { AxiosResponse } from 'axios'; +import { Request, Response } from 'express'; import IPrescriptionAndes from '../interfaces/prescriptionAndes.interface'; +import PrescriptionAndes from '../models/prescriptionAndes.model'; interface GetPrescriptionsParams { professionalId: string; @@ -26,6 +28,121 @@ class AndesService { } } + public async getFromAndes(req: Request, res: Response): Promise { + try { + if (!req.query.dni) { return res.status(400).json({ mensaje: 'Missing required params!' }); } + const dni = req.query.dni as string; + const sexo = req.query.sexo ? (req.query.sexo as any) : undefined; + let prescriptions: IPrescriptionAndes[] | null = []; + let andesPrescriptions: IPrescriptionAndes[] | null = null; + + andesPrescriptions = await this.getPrescriptionsByPatient({ documento: dni, estado: 'vigente', sexo }); + + if (andesPrescriptions) { + andesPrescriptions = andesPrescriptions.map(aPrescription => { + aPrescription.idAndes = aPrescription._id; + return aPrescription; + }); + prescriptions = [...prescriptions, ...andesPrescriptions]; + } + + const savedPrescriptions: IPrescriptionAndes[] | null = await PrescriptionAndes.find({ 'paciente.documento': dni }); + if (savedPrescriptions) { + prescriptions = [...prescriptions, ...savedPrescriptions]; + } + return res.status(200).json(prescriptions); + } catch (e) { + return res.status(500).json({ error: e }); + } + } + + public async searchProfessionals(req: Request, res: Response): Promise { + const { documento } = req.query; + + if (!documento) { + return res.status(400).json({ + ok: false, + message: 'El parámetro "documento" es requerido' + }); + } + + try { + const professionals = await this.searchProfessionalsGuide(documento as string); + + if (professionals && professionals.length > 0) { + return res.status(200).json({ + ok: true, + message: 'Profesionales encontrados', + data: professionals, + total: professionals.length + }); + } else { + return res.status(200).json({ + ok: false, + message: 'No se encontraron profesionales con el documento proporcionado', + data: [], + total: 0 + }); + } + } catch (error) { + return res.status(500).json({ + ok: false, + message: 'Error al buscar profesionales en Andes', + error + }); + } + } + + public async searchPharmacies(req: Request, res: Response): Promise { + const { cuit } = req.query; + + if (!cuit) { + return res.status(400).json({ + ok: false, + message: 'El parámetro "cuit" es requerido' + }); + } + + try { + let cuitStr = cuit as string; + let altCuit = ''; + + if (/^\d{11}$/.test(cuitStr)) { + altCuit = `${cuitStr.slice(0, 2)}-${cuitStr.slice(2, 10)}-${cuitStr.slice(10)}`; + } else if (/^\d{2}-\d{8}-\d{1}$/.test(cuitStr)) { + altCuit = cuitStr.replace(/-/g, ''); + } + + let pharmacies = await this.searchPharmaciesCore(cuitStr); + + if ((!pharmacies || pharmacies.length === 0) && altCuit) { + pharmacies = await this.searchPharmaciesCore(altCuit); + } + + if (pharmacies && pharmacies.length > 0) { + return res.status(200).json({ + ok: true, + message: 'Farmacias encontradas', + data: pharmacies, + total: pharmacies.length + }); + } else { + return res.status(200).json({ + ok: false, + message: 'No se encontraron farmacias con el CUIT proporcionado', + data: [], + total: 0 + }); + } + } catch (error) { + return res.status(500).json({ + ok: false, + message: 'Error al buscar farmacias en Andes', + error + }); + } + } + public async getPrescriptionsByPatient(params: GetPrescriptionsByPatientParams): Promise { try { const url = `${this.baseURL}/modules/recetas`; From 0fcca4d0066f0e5dace89b756a3493c2e108e530 Mon Sep 17 00:00:00 2001 From: ma7payne Date: Tue, 12 May 2026 15:12:09 -0300 Subject: [PATCH 4/8] feat(REC): implementa verificacion de recets en ANDES (#93) --- .../andesPrescription.controller.ts | 34 +++++++++++++++++++ src/routes/private.ts | 1 + src/services/andesService.ts | 28 +++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/src/controllers/andesPrescription.controller.ts b/src/controllers/andesPrescription.controller.ts index 7c667ba..f119e36 100644 --- a/src/controllers/andesPrescription.controller.ts +++ b/src/controllers/andesPrescription.controller.ts @@ -224,6 +224,40 @@ class AndesPrescriptionController implements BaseController { } }; + public verificarReceta = async (req: Request, res: Response): Promise => { + const { dni, conceptId } = req.query; + + if (!dni || !conceptId) { + return res.status(400).json({ mensaje: 'Se requieren los parámetros dni y conceptId' }); + } + + try { + // Consulta la DB local por DNI del paciente (identificador común entre sistemas) + const estadosValidos = ['vigente', 'pendiente']; + const estadosDispensaExcluidos = ['dispensada']; + + const recetasLocales = await PrescriptionAndes.find({ + 'paciente.documento': dni, + 'medicamento.concepto.conceptId': conceptId, + 'estadoActual.tipo': { $in: estadosValidos }, + 'estadoDispensaActual.tipo': { $nin: estadosDispensaExcluidos } + }).lean(); + + // Consulta a Andes por documento y conceptId utilizando el endpoint especifico + const verificacionAndes = await AndesService.verificarRecetaExistente(dni as string, conceptId as string).catch(() => null); + const recetasAndes = verificacionAndes && verificacionAndes.existe && verificacionAndes.receta + ? [verificacionAndes.receta] + : []; + + // Combinar resultados + const allRecetas = [...recetasLocales, ...recetasAndes]; + + return res.status(200).json(allRecetas); + } catch (e) { + return res.status(500).json({ mensaje: 'Error al verificar receta', error: e }); + } + }; + } export default new AndesPrescriptionController(); diff --git a/src/routes/private.ts b/src/routes/private.ts index 190536d..cc00a81 100644 --- a/src/routes/private.ts +++ b/src/routes/private.ts @@ -57,6 +57,7 @@ class PrivateRoutes { // Andes prescriptions this.router.get('/andes-prescriptions/from-andes/', hasPermissionIn('readAny', 'prescription'), andesPrescriptionController.getFromAndes); + this.router.get('/andes-prescriptions/verificar', hasPermissionIn('readAny', 'prescription'), andesPrescriptionController.verificarReceta); this.router.get('/andes-prescriptions/:id', hasPermissionIn('readAny', 'prescription'), andesPrescriptionController.show); this.router.patch('/andes-prescriptions/dispense', hasPermissionIn('updateAny', 'prescription'), andesPrescriptionController.dispense); this.router.patch('/andes-prescriptions/cancel-dispense', hasPermissionIn('updateAny', 'prescription'), andesPrescriptionController.cancelDispense); diff --git a/src/services/andesService.ts b/src/services/andesService.ts index 7b91a7e..46de69f 100644 --- a/src/services/andesService.ts +++ b/src/services/andesService.ts @@ -158,6 +158,8 @@ class AndesService { const fullUrl = queryParams.toString() ? `${url}?${queryParams.toString()}` : url; + console.log('fullUrl', fullUrl); + const response: AxiosResponse = await axios.get(fullUrl, { headers: { Authorization: this.token, @@ -264,6 +266,32 @@ class AndesService { } } + /** + * Verifica si existe una receta vigente para un paciente y medicamento (por conceptId SNOMED) en ANDES + */ + public async verificarRecetaExistente(dni: string, conceptId: string): Promise { + try { + const url = `${this.baseURL}/modules/recetas/verificar`; + console.log('fullUrl', url); + + const response: AxiosResponse = await axios.get(url, { + params: { documento: dni, conceptId }, + headers: { + Authorization: this.token, + 'Content-Type': 'application/json' + } + }); + + console.log('response', response.data); + + return response.data; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error al verificar receta existente en ANDES:', error); + throw error; + } + } + public async suspendPrescription(recetaId: string, motivo: string, observacion?: string, profesional?: any): Promise { try { const url = `${this.baseURL}/modules/recetas`; From b77137c991fc3485c53aecb4bdca551defc573fa Mon Sep 17 00:00:00 2001 From: ma7payne Date: Tue, 12 May 2026 15:25:04 -0300 Subject: [PATCH 5/8] feat(REC): implementa rutas para control de insumos (#72) --- src/controllers/andesStock.controller.ts | 33 ++++++++++ src/controllers/prescription.controller.ts | 58 +++++++++++++---- src/controllers/stock.controller.ts | 17 +++++ src/dtos/andes-insumo.dto.ts | 74 ++++++++++++++++++++++ src/interfaces/supply.interface.ts | 3 + src/models/prescription.model.ts | 6 ++ src/models/supply.model.ts | 13 ++-- src/routes/private.ts | 6 ++ src/services/andesService.ts | 51 +++++++++++++++ 9 files changed, 245 insertions(+), 16 deletions(-) create mode 100644 src/controllers/andesStock.controller.ts create mode 100644 src/controllers/stock.controller.ts create mode 100644 src/dtos/andes-insumo.dto.ts diff --git a/src/controllers/andesStock.controller.ts b/src/controllers/andesStock.controller.ts new file mode 100644 index 0000000..a634c70 --- /dev/null +++ b/src/controllers/andesStock.controller.ts @@ -0,0 +1,33 @@ +import { Request, Response } from 'express'; +import AndesService from '../services/andesService'; + +class AndesStockController { + + public search = async (req: Request, res: Response): Promise => { + try { + const insumo = req.query.insumo as string; + const tipos = req.query.tipos as string; + + if (!insumo) { + return res.status(400).json({ mensaje: 'Missing required param: insumo' }); + } + + const result = await AndesService.searchStock({ insumo, tipos }); + return res.status(200).json(result); + } catch (e) { + return res.status(500).json({ mensaje: 'Error', error: e }); + } + }; + + public getStock = async (req: Request, res: Response): Promise => { + try { + const result = await AndesService.getAllStock(); + return res.status(200).json(result); + } catch (e) { + return res.status(500).json({ mensaje: 'Error', error: e }); + } + }; + +} + +export default new AndesStockController(); diff --git a/src/controllers/prescription.controller.ts b/src/controllers/prescription.controller.ts index d03a724..fe3055f 100644 --- a/src/controllers/prescription.controller.ts +++ b/src/controllers/prescription.controller.ts @@ -16,6 +16,7 @@ const csv = require('fast-csv'); import needle from 'needle'; import axios from 'axios'; import AndesService from '../services/andesService'; +import { AndesInsumoDTO } from '../dtos/andes-insumo.dto'; class PrescriptionController implements BaseController { @@ -148,9 +149,12 @@ class PrescriptionController implements BaseController { public create = async (req: Request, res: Response): Promise => { const { professional, patient, date, supplies, trimestral, ambito, organizacion } = req.body; - const myProfessional: IUser | null = await User.findOne({ _id: professional }); + const professionalId = professional.userId ? professional.userId : professional; + const myProfessional: IUser | null = await User.findOne({ _id: professionalId }); let myPatient: IPatient | null; - if (ambito === 'publico') { + const hasInsumo = supplies.some((sup: any) => sup.supply.type); + + if (ambito === 'publico' || hasInsumo) { const resp = await needle('get', `${process.env.ANDES_ENDPOINT}/core/tm/profesionales/guia?documento=${myProfessional?.username}`); if ((resp.body && resp.body.length > 0 && resp.body[0].profesiones && resp.body[0].profesiones.length > 0)) { // Actualización del profesional @@ -159,7 +163,7 @@ class PrescriptionController implements BaseController { myProfessional.businessName = `${resp.body[0]?.apellido}, ${resp.body[0]?.nombre}`; await myProfessional.save(); } - myPatient = await Patient.schema.methods.findOrCreate(patient, ambito); + myPatient = await Patient.schema.methods.findOrCreate(patient, 'publico'); } else { // eslint-disable-next-line no-console console.log('No se encuentra el profesional.'); @@ -197,11 +201,16 @@ class PrescriptionController implements BaseController { organizacion: organizacion || undefined }); let createAndes = false; - if (ambito === 'publico') { + if (ambito === 'publico' || sup.supply.type) { // Solo crear en andes si el profesional tiene idAndes if (myProfessional?.idAndes) { - createAndes = await this.createPrescriptionAndes(newPrescription, myProfessional, myPatient); + if (sup.supply.type) { + createAndes = await this.createInsumoPrescriptionAndes(newPrescription, myProfessional, myPatient, sup); + } else { + createAndes = await this.createPrescriptionAndes(newPrescription, myProfessional, myPatient); + } } + // En caso de que no se haya podido crear en andes, se guarda localmente if (!createAndes) { console.log('No se pudo crear la prescripción en ANDES, se guarda localmente.'); @@ -264,6 +273,8 @@ class PrescriptionController implements BaseController { return res.status(200).json(allPrescription); } catch (err) { + // eslint-disable-next-line no-console + console.log(err); return res.status(500).json('Error al cargar la prescripción'); } } else { @@ -305,8 +316,8 @@ class PrescriptionController implements BaseController { direccion: newPrescription.organizacion?.direccion || null, }, medicamento: { - diagnostico: newPrescription.supplies[0].diagnostic, - concepto: newPrescription.supplies[0].supply.snomedConcept, + diagnostico: newPrescription.supplies[0].diagnostic || 'Sin diagnóstico', + concepto: newPrescription.supplies[0].supply.snomedConcept || (newPrescription.supplies[0].supply as any).concepto, presentacion: '', unidades: '', cantidad: newPrescription.supplies[0].quantityPresentation ? newPrescription.supplies[0].quantityPresentation : 1, @@ -314,7 +325,7 @@ class PrescriptionController implements BaseController { dosisDiaria: { dosis: null, dias: null, - notaMedica: newPrescription.supplies[0].indication ? newPrescription.supplies[0].indication : '' + notaMedica: (newPrescription.supplies[0].indication || '') + (newPrescription.supplies[0].supply.specification ? ` - Especificación: ${newPrescription.supplies[0].supply.specification}` : '') }, tratamientoProlongado: newPrescription.trimestral ? true : false, tiempoTratamiento: !newPrescription.trimestral ? null : { id: '3', nombre: '3 meses' }, @@ -347,6 +358,26 @@ class PrescriptionController implements BaseController { return sendToAndes; }; + private createInsumoPrescriptionAndes = async (newPrescription: IPrescription, profesional: IUser, patient: IPatient, originalSupply: any) => { + let sendToAndes = false; + + try { + const payload = AndesInsumoDTO.transform(newPrescription, profesional, patient, originalSupply); + const Authorization = process.env.JWT_MPI_TOKEN || ''; + const respAndes = await axios.post(`${process.env.ANDES_ENDPOINT}/modules/recetasInsumos`, + payload, + { headers: { Authorization } }); + if (respAndes.status === 200 || respAndes.status === 201) { + sendToAndes = true; + } + + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error al enviar insumo a ANDES:', e); + } + return sendToAndes; + }; + public getPrescriptionsByDateOrPatientId = async (req: Request, res: Response): Promise => { try { const filterPatient = req.params.patient_id; @@ -385,19 +416,24 @@ class PrescriptionController implements BaseController { public getByUserId = async (req: Request, res: Response): Promise => { try { const { id } = req.params; - const { offset = 0, limit = 10, ambito = 'privado' } = req.query; + const { offset = 0, limit = 10, ambito = 'privado', insumos = 'false' } = req.query; await this.updateStatuses(id, ''); + const query: any = { 'professional.userId': id }; + if (insumos === 'false' && ambito === 'privado') { + query['supplies.supply.type'] = { $exists: false }; + } + // Obtener prescripciones locales - const localPrescriptions: IPrescription[] | null = await Prescription.find({ 'professional.userId': id }) + const localPrescriptions: IPrescription[] | null = await Prescription.find(query) .sort({ date: -1 }); if (localPrescriptions) { await this.ensurePrescriptionIds(localPrescriptions); } - const localTotal = await Prescription.countDocuments({ 'professional.userId': id }); + const localTotal = await Prescription.countDocuments(query); // Combinar con prescripciones de ANDES si es necesario const { combinedPrescriptions, totalPrescriptions } = await this.combineLocalAndAndesPrescriptions( diff --git a/src/controllers/stock.controller.ts b/src/controllers/stock.controller.ts new file mode 100644 index 0000000..de37822 --- /dev/null +++ b/src/controllers/stock.controller.ts @@ -0,0 +1,17 @@ +import { Request, Response } from 'express'; +import Prescription from '../models/prescription.model'; + +class StockController { + + public getStock = async (req: Request, res: Response): Promise => { + try { + const prescriptions = await Prescription.find({ 'supplies.supply.type': { $exists: true } }).limit(100).sort({ date: -1 }); + + return res.status(200).json(prescriptions); + } catch (e) { + return res.status(500).json({ mensaje: 'Error', error: e }); + } + }; +} + +export default new StockController(); diff --git a/src/dtos/andes-insumo.dto.ts b/src/dtos/andes-insumo.dto.ts new file mode 100644 index 0000000..64af816 --- /dev/null +++ b/src/dtos/andes-insumo.dto.ts @@ -0,0 +1,74 @@ +import IPrescription from '../interfaces/prescription.interface'; +import IUser from '../interfaces/user.interface'; +import IPatient from '../interfaces/patient.interface'; + +export class AndesInsumoDTO { + public static transform(prescription: IPrescription, professional: IUser, patient: IPatient, originalSupply?: any) { + const supplyInfo = prescription.supplies[0]; + const supply = supplyInfo.supply; + const originalSupplyId = originalSupply?.supply?._id || supply._id || supply.id || ''; + + // Separar nombre y apellido del profesional + const parts = professional.businessName ? professional.businessName.split(',') : []; + const apellido = parts[0] ? parts[0].trim() : ''; + const nombre = parts[1] ? parts[1].trim() : ''; + + const payload: any = { + organizacion: { + id: prescription.organizacion?._id || null, + nombre: prescription.organizacion?.nombre || 'Recetar' + }, + profesional: { + id: professional.idAndes || '', + nombre: nombre, + apellido: apellido, + documento: professional.username, + profesion: professional.profesionGrado?.[0]?.profesion || '', + matricula: professional.enrollment || '', + especialidad: '' + }, + fechaRegistro: prescription.date, + fechaPrestacion: prescription.date, + idPrestacion: prescription._id.toString(), + idRegistro: prescription._id.toString(), + diagnostico: supplyInfo.diagnostic || 'Sin diagnóstico', + insumo: { + ...(supply.snomedConcept ? { + concepto: { + conceptId: supply.snomedConcept.conceptId, + term: supply.snomedConcept.term + } + } : { + generico: { + id: originalSupplyId.toString(), + nombre: supply.name || '' + } + }), + cantidad: supplyInfo.quantity || 1, + especificacion: ((supplyInfo.indication || '') + (supply.specification ? ` - Especificación: ${supply.specification}` : '')).trim() || 'Sin especificación', + diagnostico: supplyInfo.diagnostic || 'Sin diagnóstico' + }, + paciente: { + id: patient.idMPI, + nombre: patient.firstName, + apellido: patient.lastName, + documento: patient.dni, + sexo: patient.sex ? patient.sex.toLowerCase() : '', + fechaNacimiento: patient.fechaNac, + obraSocial: patient.obraSocial ? { + nombre: patient.obraSocial.nombre, + numeroAfiliado: patient.obraSocial.numeroAfiliado || '' + } : undefined + }, + origenExterno: { + id: prescription._id.toString(), + app: { + nombre: 'recetar' + }, + fecha: prescription.date + } + }; + + return payload; + } +} diff --git a/src/interfaces/supply.interface.ts b/src/interfaces/supply.interface.ts index 9ac71d1..7fec5cf 100644 --- a/src/interfaces/supply.interface.ts +++ b/src/interfaces/supply.interface.ts @@ -17,4 +17,7 @@ export default interface ISupply extends Document { secondPresentation: string; pharmaceutical_form: string; snomedConcept: ISnomedConcept; + type?: string; + requiresSpecification?: boolean; + specification?: string; } \ No newline at end of file diff --git a/src/models/prescription.model.ts b/src/models/prescription.model.ts index c76de4c..60793f8 100644 --- a/src/models/prescription.model.ts +++ b/src/models/prescription.model.ts @@ -36,6 +36,12 @@ const prescriptionSchema = new Schema({ type: String, enum: ['device', 'nutrition', 'magistral'] }, + requiresSpecification: { + type: Boolean + }, + specification: { + type: String + } }, quantity: Number, quantityPresentation: Number, diff --git a/src/models/supply.model.ts b/src/models/supply.model.ts index 9ea3113..913a258 100644 --- a/src/models/supply.model.ts +++ b/src/models/supply.model.ts @@ -26,11 +26,14 @@ export const supplySchema = new Schema({ secondPresentation: { type: String }, - snomedConcept: { - conceptId: String, - term: String, - fsn: String, - semanticTag: String + code: { + source: { type: String, enum: ['SIFAHO', 'SNOMED'] }, + value: String + }, + status: { + type: String, + enum: ['active', 'inactive'], + default: 'active' } }, { timestamps: true diff --git a/src/routes/private.ts b/src/routes/private.ts index cc00a81..28b4f41 100644 --- a/src/routes/private.ts +++ b/src/routes/private.ts @@ -20,6 +20,8 @@ import snomedSupplyController from '../controllers/snomed.controller'; import andesPrescriptionController from '../controllers/andesPrescription.controller'; import certificateController from '../controllers/certificate.controller'; import practiceController from '../controllers/practice.controller'; +import andesStockController from '../controllers/andesStock.controller'; +import stockController from '../controllers/stock.controller'; class PrivateRoutes { constructor(private router: Router = Router()) { } @@ -66,6 +68,10 @@ class PrivateRoutes { // Andes search this.router.get('/andes/professionals', hasPermissionIn('readAny', 'user'), andesPrescriptionController.searchProfessionals); this.router.get('/andes/pharmacies', hasPermissionIn('readAny', 'user'), andesPrescriptionController.searchPharmacies); + // Andes Stock + this.router.get('/stock/andes', andesStockController.getStock); + this.router.get('/stock/andes/search', andesStockController.search); + this.router.get('/stock', stockController.getStock); // practices this.router.get(`/practices/:id`, hasPermissionIn('readAny', 'prescription'), practiceController.getById); diff --git a/src/services/andesService.ts b/src/services/andesService.ts index 46de69f..fe5fa2f 100644 --- a/src/services/andesService.ts +++ b/src/services/andesService.ts @@ -228,6 +228,57 @@ class AndesService { } } + /** + * Busca stock de un insumo específico desde ANDES + */ + public async searchStock(params: { insumo: string, tipos?: string }): Promise { + try { + let url = `${this.baseURL}/modules/insumos?nombre=^${params.insumo}`; + + if (params.tipos) { + const tipos = params.tipos.split(','); + tipos.forEach(tipo => { + url += `&tipo=${tipo.trim()}`; + }); + } + + const response: AxiosResponse = await axios.get(url, { + headers: { + Authorization: this.token, + 'Content-Type': 'application/json' + } + }); + + return Array.isArray(response.data) ? response.data : [response.data]; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error al buscar stock desde ANDES:', error); + throw error; + } + } + + /** + * Obtiene todo el stock de insumos desde ANDES + */ + public async getAllStock(): Promise { + try { + const url = `${this.baseURL}/modules/insumos`; + + const response: AxiosResponse = await axios.get(url, { + headers: { + Authorization: this.token, + 'Content-Type': 'application/json' + } + }); + + return response.data; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error al obtener todo el stock desde ANDES:', error); + throw error; + } + } + /** * Obtiene las prescripciones de un profesional desde ANDES */ From 0ef67a928b78261fbf3ff597e5c7bf8ba5b8918c Mon Sep 17 00:00:00 2001 From: ma7payne Date: Tue, 12 May 2026 15:35:34 -0300 Subject: [PATCH 6/8] feat(REC): Implementa filtros para listado de recetas (#69) --- .../andesPrescription.controller.ts | 20 +++++- src/controllers/prescription.controller.ts | 66 +++++++++++++++---- src/routes/private.ts | 2 +- src/services/andesService.ts | 48 +++++++++++++- 4 files changed, 118 insertions(+), 18 deletions(-) diff --git a/src/controllers/andesPrescription.controller.ts b/src/controllers/andesPrescription.controller.ts index f119e36..d188efd 100644 --- a/src/controllers/andesPrescription.controller.ts +++ b/src/controllers/andesPrescription.controller.ts @@ -6,6 +6,22 @@ import User from '../models/user.model'; import IUser from '../interfaces/user.interface'; import prescriptionController from './prescription.controller'; import AndesService from '../services/andesService'; +import moment = require('moment'); +import needle from 'needle'; + +/** + * Función helper para obtener prescripciones de ANDES por DNI + * Esta función puede ser llamada desde otros controladores + */ +export const getAndesPrescriptionsByDni = async ( + dni: string, + sexo: string, + status?: string, + dateFrom?: string, + dateTo?: string +): Promise => { + return AndesService.getPrescriptionsByDni(dni, sexo, status, dateFrom, dateTo); +}; class AndesPrescriptionController implements BaseController { @@ -245,8 +261,8 @@ class AndesPrescriptionController implements BaseController { // Consulta a Andes por documento y conceptId utilizando el endpoint especifico const verificacionAndes = await AndesService.verificarRecetaExistente(dni as string, conceptId as string).catch(() => null); - const recetasAndes = verificacionAndes && verificacionAndes.existe && verificacionAndes.receta - ? [verificacionAndes.receta] + const recetasAndes = verificacionAndes && verificacionAndes.existe && verificacionAndes.receta + ? [verificacionAndes.receta] : []; // Combinar resultados diff --git a/src/controllers/prescription.controller.ts b/src/controllers/prescription.controller.ts index fe3055f..c0a1088 100644 --- a/src/controllers/prescription.controller.ts +++ b/src/controllers/prescription.controller.ts @@ -17,7 +17,7 @@ import needle from 'needle'; import axios from 'axios'; import AndesService from '../services/andesService'; import { AndesInsumoDTO } from '../dtos/andes-insumo.dto'; - +import { getAndesPrescriptionsByDni } from './andesPrescription.controller'; class PrescriptionController implements BaseController { @@ -381,30 +381,70 @@ class PrescriptionController implements BaseController { public getPrescriptionsByDateOrPatientId = async (req: Request, res: Response): Promise => { try { const filterPatient = req.params.patient_id; - const filterDate: string | null = req.params.date; + const { status, dateFrom, dateTo, sexo } = req.query; - // define a default date for retrieve all the documents if the date its not provided - const defaultStart = '1900-01-01'; - let startDate: Date = moment(defaultStart, 'YYYY-MM-DD').startOf('day').toDate(); + let startDate: Date = moment().subtract(1, 'month').toDate(); let endDate: Date = moment(new Date()).endOf('day').toDate(); - if (typeof (filterDate) !== 'undefined') { - startDate = moment(filterDate, 'YYYY-MM-DD').startOf('day').toDate(); - endDate = moment(filterDate, 'YYYY-MM-DD').endOf('day').toDate(); + if (dateFrom) { + startDate = moment(dateFrom, 'DD-MM-YYYY').startOf('day').toDate(); + } + + if (dateTo) { + endDate = moment(dateTo, 'DD-MM-YYYY').endOf('day').toDate(); } await this.updateStatuses('', filterPatient); - const prescriptions: IPrescription[] | null = await Prescription.find({ + const filters: any = { 'patient.dni': filterPatient, date: { $gte: startDate, $lt: endDate } - }).sort({ field: 'desc', date: -1 }); + }; - if (prescriptions) { - await this.ensurePrescriptionIds(prescriptions); + if (status) { + if (status !== 'todas') { + const validStatuses: Record = { + vigente: 'Vigente', + pendiente: 'Pendiente', + rechazada: 'Rechazada', + dispensada: 'Dispensada', + vencida: 'Vencida', + finalizada: 'Finalizada', + suspendida: 'Suspendida' + }; + + if (Object.keys(validStatuses).includes(status)) { + filters.status = validStatuses[status]; + } + } + } else { + filters.status = 'Vigente'; } - return res.status(200).json(prescriptions); + const localPrescriptions: IPrescription[] | null = await Prescription.find(filters) + .sort({ field: 'desc', date: -1 }); + + if (localPrescriptions) { + await this.ensurePrescriptionIds(localPrescriptions); + } + + // Obtener prescripciones de ANDES + const sexoParam = sexo ? (sexo as string).toLowerCase() : ''; + const andesPrescriptions = await getAndesPrescriptionsByDni( + filterPatient, + sexoParam, + status as string, + dateFrom as string, + dateTo as string + ); + + // Combinar prescripciones locales y de ANDES + const combinedPrescriptions = [ + ...(localPrescriptions || []), + ...andesPrescriptions + ]; + + return res.status(200).json(combinedPrescriptions); } catch (err) { // eslint-disable-next-line no-console console.log(err); diff --git a/src/routes/private.ts b/src/routes/private.ts index 28b4f41..cc962cf 100644 --- a/src/routes/private.ts +++ b/src/routes/private.ts @@ -39,7 +39,7 @@ class PrivateRoutes { this.router.get(`/prescriptions/`, hasPermissionIn('readAny', 'prescription'), prescriptionController.index); this.router.get('/prescriptions/user/:id', prescriptionController.getByUserId); this.router.get('/prescriptions/user/:id/search', prescriptionController.searchByTerm); - this.router.get('/prescriptions/find/:patient_id&:date?', prescriptionController.getPrescriptionsByDateOrPatientId); + this.router.get('/prescriptions/find/:patient_id', prescriptionController.getPrescriptionsByDateOrPatientId); this.router.get(`/prescriptions/:id`, hasPermissionIn('readAny', 'prescription'), prescriptionController.show); this.router.post(`/prescriptions/`, hasPermissionIn('createAny', 'prescription'), prescriptionController.create); this.router.post(`/prescriptions/get-csv/`, hasPermissionIn('readAny', 'prescription'), prescriptionController.getCsv); diff --git a/src/services/andesService.ts b/src/services/andesService.ts index fe5fa2f..285729c 100644 --- a/src/services/andesService.ts +++ b/src/services/andesService.ts @@ -297,8 +297,7 @@ class AndesService { queryParams.append('hasta', params.hasta); } queryParams.append('origenExternoApp', 'recetar'); - queryParams.append('excluirEstado', 'pendiente'); - queryParams.append('excluirEstado', 'eliminada'); + queryParams.append('excluirEstado', 'pendiente,eliminada'); const fullUrl = queryParams.toString() ? `${url}?${queryParams.toString()}` : url; @@ -368,4 +367,49 @@ class AndesService { throw error; } }; + + /** + * Obtiene prescripciones de ANDES por DNI con filtros + */ + public async getPrescriptionsByDni( + dni: string, + sexo: string, + status?: string, + dateFrom?: string, + dateTo?: string + ): Promise { + try { + let andesUrl = `${this.baseURL}/modules/recetas/filtros?documento=${dni}&sexo=${sexo}`; + + let estadoFiltro = 'vigente'; + const validEstados = ['pendiente', 'vigente', 'finalizada', 'vencida', 'suspendida', 'rechazada', 'dispensada', 'todas']; + + if (status && validEstados.includes(status)) { + estadoFiltro = status; + } + + if (dateFrom) { andesUrl += `&fechaInicio=${dateFrom}`; } + if (dateTo) { andesUrl += `&fechaFin=${dateTo}`; } + + andesUrl += `&estado=${estadoFiltro}`; + + const response: AxiosResponse = await axios.get(andesUrl, { + headers: { Authorization: this.token } + }); + + let andesPrescriptions = response.data; + if (andesPrescriptions) { + andesPrescriptions = andesPrescriptions.map((aPrescription: any) => { + aPrescription.idAndes = aPrescription._id; + return aPrescription; + }); + } + + return andesPrescriptions || []; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error al obtener prescripciones de ANDES por DNI:', error); + return []; + } + }; } export default new AndesService(); From b8261eb65d899e6fedd8e85674322b86fe1ecdeb Mon Sep 17 00:00:00 2001 From: ma7payne Date: Thu, 14 May 2026 15:37:05 -0300 Subject: [PATCH 7/8] (fix): agrega el sexo del paciente para verificar medicamento existente (#96) --- src/controllers/andesPrescription.controller.ts | 10 +++++++--- src/services/andesService.ts | 7 ++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/controllers/andesPrescription.controller.ts b/src/controllers/andesPrescription.controller.ts index d188efd..d469f8f 100644 --- a/src/controllers/andesPrescription.controller.ts +++ b/src/controllers/andesPrescription.controller.ts @@ -241,12 +241,16 @@ class AndesPrescriptionController implements BaseController { }; public verificarReceta = async (req: Request, res: Response): Promise => { - const { dni, conceptId } = req.query; + const { dni, conceptId, sexo } = req.query; if (!dni || !conceptId) { return res.status(400).json({ mensaje: 'Se requieren los parámetros dni y conceptId' }); } + if (!sexo) { + return res.status(400).json({ mensaje: 'Se requiere el parámetro sexo del paciente' }); + } + try { // Consulta la DB local por DNI del paciente (identificador común entre sistemas) const estadosValidos = ['vigente', 'pendiente']; @@ -259,8 +263,8 @@ class AndesPrescriptionController implements BaseController { 'estadoDispensaActual.tipo': { $nin: estadosDispensaExcluidos } }).lean(); - // Consulta a Andes por documento y conceptId utilizando el endpoint especifico - const verificacionAndes = await AndesService.verificarRecetaExistente(dni as string, conceptId as string).catch(() => null); + // Consulta a Andes por documento, conceptId y sexo utilizando el endpoint especifico + const verificacionAndes = await AndesService.verificarRecetaExistente(dni as string, conceptId as string, sexo as string).catch(() => null); const recetasAndes = verificacionAndes && verificacionAndes.existe && verificacionAndes.receta ? [verificacionAndes.receta] : []; diff --git a/src/services/andesService.ts b/src/services/andesService.ts index 285729c..ddc0b8d 100644 --- a/src/services/andesService.ts +++ b/src/services/andesService.ts @@ -319,21 +319,18 @@ class AndesService { /** * Verifica si existe una receta vigente para un paciente y medicamento (por conceptId SNOMED) en ANDES */ - public async verificarRecetaExistente(dni: string, conceptId: string): Promise { + public async verificarRecetaExistente(dni: string, conceptId: string, sexo: string): Promise { try { const url = `${this.baseURL}/modules/recetas/verificar`; - console.log('fullUrl', url); const response: AxiosResponse = await axios.get(url, { - params: { documento: dni, conceptId }, + params: { documento: dni, conceptId, sexo }, headers: { Authorization: this.token, 'Content-Type': 'application/json' } }); - console.log('response', response.data); - return response.data; } catch (error) { // eslint-disable-next-line no-console From 2b5fcd2dc97a42d053b8d8ad423f2764555325a7 Mon Sep 17 00:00:00 2001 From: Agustin Rodriguez Alvarez <63318331+agustin1996ra@users.noreply.github.com> Date: Mon, 18 May 2026 11:09:46 -0300 Subject: [PATCH 8/8] (fix)REC-216: Fecha de nacieminto (#88) --- src/controllers/prescription.controller.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/controllers/prescription.controller.ts b/src/controllers/prescription.controller.ts index c0a1088..260b17f 100644 --- a/src/controllers/prescription.controller.ts +++ b/src/controllers/prescription.controller.ts @@ -173,6 +173,17 @@ class PrescriptionController implements BaseController { } else { myPatient = await Patient.schema.methods.findOrCreate(patient, ambito); } + + // Completa fecha de nacimiento solo cuando el paciente existente no la tiene. + const payloadBirthDate = patient?.fechaNac || patient?.fechaNacimiento; + if (myPatient && !myPatient.fechaNac && payloadBirthDate) { + const parsedBirthDate = new Date(payloadBirthDate); + if (!Number.isNaN(parsedBirthDate.getTime())) { + myPatient.fechaNac = parsedBirthDate; + await myPatient.save(); + } + } + if (myProfessional && patient && myPatient) { try { const allPrescription: IPrescription[] = [];