Find Document References ahora acepta un parametro opcional 'type' (IPS, MeOW, IT, o un token FHIR system|code) para buscar solo el tipo de documento pedido. Se extrae el mapa de tipos de documento a constants/documentTypes.js para compartirlo entre ITI-65 e ITI-67. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
268 lines
9.1 KiB
JavaScript
268 lines
9.1 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_TYPES } = require('../constants/documentTypes');
|
|
|
|
|
|
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'
|
|
|
|
|
|
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,
|
|
};
|