Alejandro Gomez Auad df87fc5ee7 Agregadas variantes de ITI-65 para MeOW e IT ademas de IPS
Se generaliza el controller para recibir el tipo de documento (LOINC)
en vez de hardcodear el de IPS, y se agregan las rutas y mapeos de
nginx para /fhir/MeOWTransaction, /fhir/MeOWDocument, /fhir/ITTransaction
y /fhir/ITDocument, siguiendo la misma logica que las rutas IPS existentes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 03:14:32 +00:00

293 lines
9.7 KiB
JavaScript

const axios = require('axios');
const createError = require('http-errors');
const config = require('../config');
const { getBusToken, createBusRequest } = require('../utils/busAuth');
const { findPatient } = require('../services/patient');
const { createDocumentReference, findDocumentReferenceById } = require('../services/documentReference');
const { logger } = require('../utils/logger');
const { v4: uuidv4 } = require('uuid');
const { getResourceByUrl, processDocumentBundleTransaction } = require('../services/fhir');
const DOCUMENT_REFERENCE_RESOURCE_TYPE = "DocumentReference";
const IPS_DOCUMENT_RESOURCE_TYPE = 'Bundle';
const PATIENT_RESOURCE_TYPE = 'Patient';
const LIST_RESOURCE_TYPE = 'List';
const NATIONAL_ID_SYSTEM = 'https://federador.msal.gob.ar/patient-id';
const DNI_SYSTEM = 'http://www.renaper.gob.ar/dni';
const CUSTODIAN_ID_SYSTEM = 'http://federador.msal.gob.ar/uri';
const MASTER_ID_SYSTEM = 'urn:ietf:rfc:3986'
/**
* Tipos de documento soportados por ITI-65, cada uno con su código LOINC
* para DocumentReference.type (y, en la variante "document", también para
* el DocumentReference local generado dentro de la transacción).
*/
const DOCUMENT_TYPES = {
IPS: {
label: 'IPS',
system: 'http://loinc.org',
code: '60591-5',
display: 'Patient Summary Document',
},
MEOW: {
label: 'MeOW',
system: 'http://loinc.org',
code: '56445-0',
display: 'Medication summary',
},
IT: {
label: 'IT',
system: 'http://loinc.org',
code: '57133-1',
display: 'Referral note',
},
};
function extractResource(resources, resourceType) {
return resources.filter(r => r.resourceType === resourceType)[0];
}
function extractLocalIdentifier(patient) {
return patient.identifier.filter(i => {
return i.system !== NATIONAL_ID_SYSTEM && ((!!i.use && i.use === 'official') || (!i.use))
})[0];
}
function generateDocumentReferenceResource(subjectReference, bundleUrl, docType) {
const documentRefernece = {
resourceType: 'DocumentReference',
status: 'current',
masterIdentifier: {
use: 'usual',
system: MASTER_ID_SYSTEM,
value: `urn:uuid:${uuidv4()}`
},
type: {
coding: [
{
system: docType.system,
code: docType.code,
display: docType.display,
},
],
},
date: new Date().toISOString(),
subject: {
reference: subjectReference
},
custodian: {
identifier: { system: CUSTODIAN_ID_SYSTEM, value: config.bus.issuer },
},
content: [
{
attachment: {
url: bundleUrl,
contentType: 'application/fhir+json'
},
},
],
};
return documentRefernece;
}
async function getResourcesFromTransactionResponse(transactionResponse) {
const promises = transactionResponse.entry.map(async (e) => {
const resource = await getResourceByUrl(`${config.fhir.url}/${e.response.location}`);
return resource;
});
return Promise.all(promises);
}
/**
* Construye un Bundle de tipo transaction a partir de un Bundle de tipo document
* (IPS, MeOW, IT, ...). Genera Patient, Bundle (documento), DocumentReference y
* List (SubmissionSet MHD).
* Usa urn:uuid: como fullUrl para que HAPI FHIR resuelva las referencias internas.
*/
function buildTransactionFromDocumentBundle(documentBundle, docType) {
if (documentBundle.type !== 'document') {
throw createError(400, 'Bundle must be of type document');
}
const patientEntry = (documentBundle.entry || []).find(
e => e.resource && e.resource.resourceType === 'Patient'
);
if (!patientEntry) {
throw createError(400, `${docType.label} Bundle must contain a Patient resource`);
}
const patientFullUrl = `urn:uuid:${uuidv4()}`;
const bundleFullUrl = `urn:uuid:${uuidv4()}`;
const documentReferenceFullUrl = `urn:uuid:${uuidv4()}`;
const now = new Date().toISOString();
const localDocumentReference = {
resourceType: 'DocumentReference',
status: 'current',
masterIdentifier: {
use: 'usual',
system: MASTER_ID_SYSTEM,
value: `urn:uuid:${uuidv4()}`
},
type: {
coding: [{
system: docType.system,
code: docType.code,
display: docType.display,
}]
},
date: now,
subject: { reference: patientFullUrl },
custodian: {
identifier: { system: CUSTODIAN_ID_SYSTEM, value: config.bus.issuer }
},
content: [{
attachment: {
url: bundleFullUrl,
contentType: 'application/fhir+json'
}
}]
};
const submissionSetList = {
resourceType: 'List',
status: 'current',
mode: 'working',
code: {
coding: [{
system: 'https://profiles.ihe.net/ITI/MHD/CodeSystem/MHDlistTypes',
code: 'submissionset'
}]
},
date: now,
subject: { reference: patientFullUrl },
source: {
identifier: { system: CUSTODIAN_ID_SYSTEM, value: config.bus.issuer }
},
entry: [
{ item: { reference: documentReferenceFullUrl } },
{ item: { reference: bundleFullUrl } }
]
};
return {
resourceType: 'Bundle',
type: 'transaction',
entry: [
{
fullUrl: patientFullUrl,
resource: patientEntry.resource,
request: { method: 'POST', url: 'Patient' }
},
{
fullUrl: bundleFullUrl,
resource: documentBundle,
request: { method: 'POST', url: 'Bundle' }
},
{
fullUrl: documentReferenceFullUrl,
resource: localDocumentReference,
request: { method: 'POST', url: 'DocumentReference' }
},
{
resource: submissionSetList,
request: { method: 'POST', url: 'List' }
}
]
};
}
/**
* Lógica central de ITI-65: persiste la transacción en HAPI FHIR, resuelve el
* paciente en el Bus y registra un DocumentReference apuntando al Bundle guardado.
*/
async function executeITI65(transaction, token, docType) {
const transactionResponse = await processDocumentBundleTransaction(transaction);
const resources = await getResourcesFromTransactionResponse(transactionResponse);
const localPatient = extractResource(resources, PATIENT_RESOURCE_TYPE);
const localIPSDocument = extractResource(resources, IPS_DOCUMENT_RESOURCE_TYPE);
const localPatientIdentifier = extractLocalIdentifier(localPatient);
const patientSearchset = await findPatient(token, { identifier: `${localPatientIdentifier.system}|${localPatientIdentifier.value}` });
if (patientSearchset.total == 0) {
throw createError(404, 'Patient does not exists');
}
const nationalPatientId = patientSearchset.entry[0].fullUrl;
const bundleReference = `${config.baseURL}/fhir/Bundle/${localIPSDocument.id}`;
const documentReference = generateDocumentReferenceResource(nationalPatientId, bundleReference, docType);
return createDocumentReference(token, documentReference);
}
async function acquireToken() {
return getBusToken(
config.bus.url,
config.bus.jwtSecret,
config.bus.issuer,
[config.bus.mpiScope, config.bus.documentRegistryScope].join(',')
);
}
/**
* ITI-65: Provide Document Bundle (MHD) — variante transacción
*
* Espera un Bundle de tipo transaction construido por el cliente.
*/
function provideTransaction(docType) {
return async function (req, res, next) {
try {
const transaction = req.body;
const token = await acquireToken();
const result = await executeITI65(transaction, token, docType);
return res.status(200).json(result);
} catch (err) {
next(err);
}
};
}
/**
* ITI-65: Provide Document Bundle (MHD) — variante document
*
* Espera un Bundle de tipo document. Genera internamente el Bundle transaction
* (Patient, Bundle, DocumentReference, List) y ejecuta el mismo flujo que
* provideTransaction.
*/
function provideDocumentBundle(docType) {
return async function (req, res, next) {
try {
const documentBundle = req.body;
if (!documentBundle || documentBundle.resourceType !== 'Bundle') {
throw createError(400, 'Request body must be a FHIR Bundle resource');
}
const transaction = buildTransactionFromDocumentBundle(documentBundle, docType);
const token = await acquireToken();
const result = await executeITI65(transaction, token, docType);
return res.status(200).json(result);
} catch (err) {
next(err);
}
};
}
const provideIPSTransaction = provideTransaction(DOCUMENT_TYPES.IPS);
const provideIPSDocumentBundle = provideDocumentBundle(DOCUMENT_TYPES.IPS);
const provideMeOWTransaction = provideTransaction(DOCUMENT_TYPES.MEOW);
const provideMeOWDocumentBundle = provideDocumentBundle(DOCUMENT_TYPES.MEOW);
const provideITTransaction = provideTransaction(DOCUMENT_TYPES.IT);
const provideITDocumentBundle = provideDocumentBundle(DOCUMENT_TYPES.IT);
module.exports = {
provideIPSTransaction,
provideIPSDocumentBundle,
provideMeOWTransaction,
provideMeOWDocumentBundle,
provideITTransaction,
provideITDocumentBundle,
};