Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions auth/permisos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export default [
child: [
{ key: 'tipoPrestacion', title: 'Tipo de prestación', type: 'prestacion', avoidAll: true },
{ key: 'validacion', title: 'Validación extras', type: 'prestacion', avoidAll: true },
{ key: 'prevalidacion', title: 'Pre-validación (doble firma)', type: 'prestacion', avoidAll: true },
{ key: 'servicio-intermedio', title: 'Servicio Intermedio', type: 'servicio-intermedio', avoidAll: true, visibility: 'restricted' },
{ key: 'plantilla', title: 'Cargar o editar plantillas', type: 'boolean', visibility: 'restricted' }
]
Expand Down
6 changes: 6 additions & 0 deletions config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ export const modules = {
export const defaultLimit = 50;
export const maxLimit = 1000;

// Configuración RUP
export const rup = {
// Tope de firmas de pre-validación (residentes) por informe. Ver docs/rup/0001-doble-firma-informe.md
maxFirmasPrevalidacion: 4
};

// Configuracion MPI
export const algoritmo = 'Levenshtein';

Expand Down
243 changes: 243 additions & 0 deletions docs/rup/0001-doble-firma-informe.md

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions modules/rup/controllers/prestacion.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ObjectId } from '@andes/core';
import { Types } from 'mongoose';
import { Auth } from '../../../auth/auth.class';
import { rup as rupConfig } from '../../../config';
import { userScheduler } from '../../../config.private';
import { AppCache } from '../../../connections';
import { PacienteCtr } from '../../../core-v2/mpi/paciente/paciente.routes';
Expand All @@ -11,6 +12,75 @@ import moment = require('moment');
import { tipoPrestacion } from '../../../core/tm/schemas/tipoPrestacion';


/**
* Deriva el rol del profesional firmante a partir de sus permisos (ADR 0001).
* Único punto de derivación: si en el futuro se agrega un atributo explícito de
* rol en Profesional, solo cambia esta función.
*
* - Con permiso de validación sobre el tipo de prestación -> 'validador' (médico de planta)
* - Sin ese permiso (solo pre-validación) -> 'prevalidador' (residente)
*
* @export
* @param {*} req request autenticado
* @param {string} tipoPrestacionId id del tipo de prestación
* @returns {'validador' | 'prevalidador'}
*/
export function deriveRolFirmante(req, tipoPrestacionId: string): 'validador' | 'prevalidador' {
return Auth.check(req, 'rup:validacion:' + tipoPrestacionId) ? 'validador' : 'prevalidador';
}

/**
* Configuración de la pre-validación que necesita la app (ADR 0001), para no duplicar el tope.
*/
export function configPrevalidacion() {
return { maxFirmasPrevalidacion: rupConfig.maxFirmasPrevalidacion };
}

/**
* Estados en los que una prestación admite agregar o quitar una firma de pre-validación (ADR 0001).
* La pre-validación no cambia el estado: la prestación sigue en ejecución con N firmas.
*/
export function admitePrevalidacion(tipoEstado: string): boolean {
return tipoEstado === 'ejecucion';
}

/**
* Matrículas vigentes del profesional en texto, con el mismo formato que el pie del
* informe PDF ("Título MP 123 - Especialidad ME 456"). Devuelve null si no hay ninguna.
*
* @export
* @param {*} infoMatriculas resultado de searchMatriculas
* @returns {(string | null)}
*/
export function formatearMatriculas(infoMatriculas): string | null {
const grado = (infoMatriculas?.formacionGrado || []).map(e => `${e.nombre} MP ${e.numero}`);
const posgrado = (infoMatriculas?.formacionPosgrado || []).map(e => `${e.nombre} ME ${e.numero}`);
const matriculas = [...grado, ...posgrado];
return matriculas.length ? matriculas.join(' - ') : null;
}

/**
* Usuario del request con el mismo shape que arma el AuditPlugin (usuario u app + organización).
* Se usa en las operaciones atómicas (findOneAndUpdate), donde el plugin no corre.
*/
export function usuarioAuditoria(req) {
return { ...(req.user.usuario || req.user.app), organizacion: req.user.organizacion };
}

/**
* Las operaciones atómicas no hacen data.save(), así que la entrada de historial que
* updateRegistroHistorialSolicitud agregó en memoria (solo prestaciones 'top') se perdería.
* Devuelve esa entrada lista para un $push, o null si no corresponde.
*/
export function historialParaUpdateAtomico(prestacion, usuario) {
const historial = prestacion.inicio === 'top' && prestacion.solicitud?.historial;
if (!historial?.length) {
return null;
}
const ultima = historial[historial.length - 1];
return { ...(ultima.toObject ? ultima.toObject() : ultima), createdAt: new Date(), createdBy: usuario };
}

/**
* Libera la referencia al turno dentro de la solicitud
*
Expand Down Expand Up @@ -60,6 +130,8 @@ export function updateRegistroHistorialSolicitud(solicitud, datos) {
suspenderTurno: 'Turno suspendido',
asignada: 'Profesional asignado',
validada: 'Validada',
prevalidacion: 'Pre-validada',
quitarPrevalidacion: 'Pre-validación quitada',
ejecucion: 'Ejecutada',
romperValidacion: 'Ejecutada',
anulada: 'Anulada',
Expand Down
100 changes: 100 additions & 0 deletions modules/rup/prevalidacion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { getFakeRequest } from '@andes/unit-test';
import { rup as rupConfig } from '../../config';
import { admitePrevalidacion, configPrevalidacion, deriveRolFirmante, formatearMatriculas, historialParaUpdateAtomico } from './controllers/prestacion';

/**
* Tests de la derivación de rol del firmante (ADR 0001).
* El rol NO se persiste en Profesional: se deriva de los permisos del usuario
* al momento de firmar y se guarda como snapshot en la firma.
*/
describe('deriveRolFirmante', () => {
const TIPO = '5a26e113291f463c1b982d98';

function reqConPermisos(permisos: string[]) {
const req: any = getFakeRequest();
req.user.permisos = permisos;
return req;
}

it('con permiso de validación sobre el tipo => validador (médico de planta)', () => {
const req = reqConPermisos(['rup:validacion:' + TIPO]);
expect(deriveRolFirmante(req, TIPO)).toBe('validador');
});

it('con wildcard de validación => validador', () => {
const req = reqConPermisos(['rup:validacion:*']);
expect(deriveRolFirmante(req, TIPO)).toBe('validador');
});

it('solo con permiso de pre-validación => prevalidador (residente)', () => {
const req = reqConPermisos(['rup:prevalidacion:' + TIPO]);
expect(deriveRolFirmante(req, TIPO)).toBe('prevalidador');
});

it('sin permisos relevantes => prevalidador', () => {
const req = reqConPermisos(['mpi:paciente:*']);
expect(deriveRolFirmante(req, TIPO)).toBe('prevalidador');
});

it('con ambos permisos gana validación (ADR: si valida, figura como validador)', () => {
const req = reqConPermisos(['rup:validacion:' + TIPO, 'rup:prevalidacion:' + TIPO]);
expect(deriveRolFirmante(req, TIPO)).toBe('validador');
});

it('permiso de validación de OTRO tipo no aplica a este tipo', () => {
const req = reqConPermisos(['rup:validacion:otroTipoId']);
expect(deriveRolFirmante(req, TIPO)).toBe('prevalidador');
});
});

describe('admitePrevalidacion', () => {
it('admite solo ejecución (la pre-validación no cambia el estado)', () => {
expect(admitePrevalidacion('ejecucion')).toBe(true);
});

it('no admite otros estados', () => {
expect(admitePrevalidacion('validada')).toBe(false);
expect(admitePrevalidacion('pendiente')).toBe(false);
expect(admitePrevalidacion('anulada')).toBe(false);
expect(admitePrevalidacion('pendienteValidacion')).toBe(false);
});
});

describe('formatearMatriculas', () => {
it('arma grado (MP) y posgrado (ME) con el formato del informe PDF', () => {
const info = {
formacionGrado: [{ nombre: 'Médico', numero: 1234 }],
formacionPosgrado: [{ nombre: 'Cardiología', numero: 55 }]
};
expect(formatearMatriculas(info)).toBe('Médico MP 1234 - Cardiología ME 55');
});

it('sin matrículas vigentes => null', () => {
expect(formatearMatriculas(null)).toBeNull();
expect(formatearMatriculas({ formacionGrado: [], formacionPosgrado: [] })).toBeNull();
});
});

describe('historialParaUpdateAtomico', () => {
const usuario = { username: '30000000' };

it('devuelve la última entrada de historial auditada en prestaciones top', () => {
const prestacion = { inicio: 'top', solicitud: { historial: [{ accion: 'creacion' }, { accion: 'prevalidacion' }] } };
const entrada: any = historialParaUpdateAtomico(prestacion, usuario);
expect(entrada.accion).toBe('prevalidacion');
expect(entrada.createdBy).toBe(usuario);
expect(entrada.createdAt).toBeInstanceOf(Date);
});

it('fuera de top no corresponde historial => null', () => {
const prestacion = { inicio: 'agenda', solicitud: { historial: [{ accion: 'prevalidacion' }] } };
expect(historialParaUpdateAtomico(prestacion, usuario)).toBeNull();
});
});

describe('configPrevalidacion', () => {
it('expone el tope de firmas definido en config', () => {
expect(configPrevalidacion()).toEqual({ maxFirmasPrevalidacion: rupConfig.maxFirmasPrevalidacion });
expect(rupConfig.maxFirmasPrevalidacion).toBe(4);
});
});
Loading
Loading