Compare commits

..

No commits in common. "master" and "agomez_feature_gdhcn_activo" have entirely different histories.

29 changed files with 258 additions and 1893 deletions

View File

@ -4,35 +4,29 @@
# ============================================================================= # =============================================================================
# ============================================================================= # =============================================================================
# VARIABLES DE ENTORNO REQUERIDAS # NODO
# ============================================================================= # =============================================================================
# URL base del nodo (usada por bus-gateway internamente) # URL base del nodo (usada por bus-gateway internamente)
NODO_URL_BASE=http://localhost NODO_URL_BASE=http://localhost
# Zona horaria para todos los servicios
TZ=America/Argentina/Buenos_Aires
# =============================================================================
# BUS DE SALUD — Requeridas
# =============================================================================
# URL base del Bus de salud nacional
BUS_URL=https://bus-test.msal.gob.ar
# Secreto compartido para firmar tokens JWT (HS256) contra el Bus # Secreto compartido para firmar tokens JWT (HS256) contra el Bus
BUS_JWT_SECRET=your-shared-secret BUS_JWT_SECRET=your-shared-secret
# Identificador del emisor JWT (ej.: URL del repositorio del nodo) # Identificador del emisor JWT (ej.: URL del repositorio del nodo)
BUS_ISSUER=https://your-repositorio-url BUS_ISSUER=https://your-repositorio-url
# =============================================================================
# INFRAESTRUCTURA
# =============================================================================
# Zona horaria para todos los servicios
TZ=America/Argentina/Buenos_Aires
# =============================================================================
# BUS DE SALUD
# =============================================================================
# URL base del Bus de salud nacional
BUS_URL=https://bus-test.msal.gob.ar
# Scopes OAuth para el Master Patient Index # Scopes OAuth para el Master Patient Index
MPI_SCOPE=Patient/*.read,Patient/*.write MPI_SCOPE=Patient/*.read,Patient/*.write
@ -49,20 +43,9 @@ MPI_URL=https://bus-test.msal.gob.ar/masterfile-federacion-service/fhir/Patient
# URL del Document Registry si difiere del Bus principal # URL del Document Registry si difiere del Bus principal
DOCUMENT_REGISTRY_URL=https://bus-test.msal.gob.ar/fhir/DocumentReference DOCUMENT_REGISTRY_URL=https://bus-test.msal.gob.ar/fhir/DocumentReference
# ITI-67: si es true, no se envía el parámetro `type` en el GET /DocumentReference
# al índice de documentos; se pide todo por subject y se filtra por type localmente.
DOCUMENT_REGISTRY_FILTER_TYPE_LOCALLY=false
# Habilita logs detallados de requests/responses salientes al Bus (true | false) # Habilita logs detallados de requests/responses salientes al Bus (true | false)
BUS_DEBUG=false BUS_DEBUG=false
# =============================================================================
# NODO NACIONAL
# =============================================================================
# URL del nodo nacional (bundle-signer): ejecuta las transformaciones IPS ($ddcc, $dvc, $icvp2, $meow)
BUNDLE_SIGNER_URL=https://conectaton.msal.gob.ar/nodo/fhir
# ============================================================================= # =============================================================================
# NGINX # NGINX
# ============================================================================= # =============================================================================
@ -74,6 +57,19 @@ NGINX_CONF=http
# SSL_CERT_PATH=./certs/server.crt # SSL_CERT_PATH=./certs/server.crt
# SSL_KEY_PATH=./certs/server.key # SSL_KEY_PATH=./certs/server.key
# =============================================================================
# CERTIFICADOS / DOCKER SECRETS
# Los archivos se inyectan como Docker secrets.
# Los valores por defecto apuntan a ./certs/ — reemplazar en producción.
# =============================================================================
# Certificado de la red de confianza (TNG/GDHCN)
SIGNATURE_CERT_PATH=./certs/trust-network.pem
SIGNATURE_KEY_PATH=./certs/trust-network.key
# Clave privada para firma de documentos (Document Signing Certificate)
SSL_DCC_KEY_PATH=./certs/signature.key
# ============================================================================= # =============================================================================
# HAPI FHIR (Spring Boot) # HAPI FHIR (Spring Boot)
# ============================================================================= # =============================================================================

View File

@ -14,6 +14,7 @@ Internet / Red interna
│ /fhir/IPSDocument → bus-gateway:3000 │ │ /fhir/IPSDocument → bus-gateway:3000 │
│ /fhir/DocumentReference→ bus-gateway:3000 │ │ /fhir/DocumentReference→ bus-gateway:3000 │
│ /fhir/Patient → bus-gateway:3000 │ │ /fhir/Patient → bus-gateway:3000 │
│ /gdhcn/* → gdhcn-validator-service │
│ /fhir/* (resto) → hapi-fhir:8080 │ │ /fhir/* (resto) → hapi-fhir:8080 │
└──────────────────────────────────────────────┘ └──────────────────────────────────────────────┘
``` ```
@ -25,11 +26,13 @@ Internet / Red interna
| `hapi-fhir` | `hapiproject/hapi:latest` | 8080 (interno) | Servidor FHIR R4 (Spring Boot) con búsqueda Lucene | | `hapi-fhir` | `hapiproject/hapi:latest` | 8080 (interno) | Servidor FHIR R4 (Spring Boot) con búsqueda Lucene |
| `hapi-db` | `postgres:14.6` | 5433 (interno) | Base de datos PostgreSQL para HAPI FHIR | | `hapi-db` | `postgres:14.6` | 5433 (interno) | Base de datos PostgreSQL para HAPI FHIR |
| `bus-gateway` | `./bus-gateway` (Node.js) | 3000 (interno) | Gateway al Bus de salud nacional (MPI + Document Registry) | | `bus-gateway` | `./bus-gateway` (Node.js) | 3000 (interno) | Gateway al Bus de salud nacional (MPI + Document Registry) |
| `gdhcn-validator-service` | `createchile/gdhcn-validator_backend:v1.1` | 8182 | Validador de documentos de salud verificables (firmas digitales) |
| `gdhcn-db` | `postgres:16-alpine` | 5432 (interno) | Base de datos PostgreSQL para el validador GDHCN |
| `nginx` | `nginx:alpine` | 80 / 443 | Proxy inverso — punto de entrada HTTP/HTTPS | | `nginx` | `nginx:alpine` | 80 / 443 | Proxy inverso — punto de entrada HTTP/HTTPS |
**Red interna**: todos los servicios se comunican a través de la red Docker `hapi-network`. **Red interna**: todos los servicios se comunican a través de la red Docker `hapi-network`.
**Volúmenes persistentes**: `hapi-data` (PostgreSQL de HAPI). **Volúmenes persistentes**: `hapi-data` (PostgreSQL de HAPI), `gdhcn-data` (PostgreSQL de GDHCN).
## Transacciones IHE implementadas ## Transacciones IHE implementadas
@ -47,21 +50,9 @@ El `bus-gateway` implementa los siguientes perfiles de interoperabilidad:
## Requisitos ## Requisitos
### Software
- Docker >= 20.10.8 - Docker >= 20.10.8
- Docker Compose >= 1.29.2 - Docker Compose >= 1.29.2
### Hardware del servidor
| Recurso | Mínimo | Recomendado |
|---|---|---|
| CPU | 2 núcleos | 4 núcleos |
| Memoria RAM | 4 GB | 8 GB |
| Disco | 10 GB | 30 GB |
> El mayor consumo de recursos corresponde a **HAPI FHIR** (JVM + índice Lucene), que requiere al menos 2 GB de RAM para arrancar correctamente. El volumen de disco recomendado contempla el crecimiento de los datos clínicos almacenados en PostgreSQL.
## Instalación y configuración ## Instalación y configuración
### 1. Variables de entorno ### 1. Variables de entorno
@ -79,13 +70,8 @@ Los certificados se inyectan como **Docker secrets**. Las rutas se configuran en
| Archivo por defecto | Descripción | | Archivo por defecto | Descripción |
|---|---| |---|---|
| `./certs/server.crt` / `./certs/server.key` | Certificado TLS para nginx (solo modo HTTPS) | | `./certs/server.crt` / `./certs/server.key` | Certificado TLS para nginx (solo modo HTTPS) |
| `./certs/trust-network.pem` / `./certs/trust-network.key` | Certificado de la red de confianza (GDHCN/TNG) |
> **Importante:** Los archivos `ssl_cert` y `ssl_key` deben existir en el sistema de archivos para que Docker pueda montarlos como secrets, incluso si se usa el modo HTTP. Si no se cuenta con certificados reales, crear archivos vacíos: | `./certs/signature.key` | Clave privada para firma de documentos (DSC) |
>
> ```bash
> touch ./certs/server.crt
> touch ./certs/server.key
> ```
> Ver [certs/README.md](certs/README.md) para instrucciones de generación de certificados de prueba. > Ver [certs/README.md](certs/README.md) para instrucciones de generación de certificados de prueba.
@ -119,17 +105,7 @@ Los parámetros del servidor FHIR se ajustan en [hapi-config/application.yaml](h
docker compose up -d docker compose up -d
``` ```
HAPI FHIR tarda aproximadamente **3040 segundos** en inicializar. El `bus-gateway` espera a que el contenedor `hapi-fhir` esté iniciado, pero no verifica que el servidor FHIR haya completado su arranque. Antes de usar los servicios, verificar que HAPI FHIR inicializó correctamente: HAPI FHIR tarda aproximadamente **3040 segundos** en inicializar. El `bus-gateway` espera activamente a que el servidor FHIR esté listo antes de aceptar conexiones (hasta ~3 minutos de reintentos).
```bash
docker compose logs -f hapi-fhir
```
Buscar la línea que confirma el inicio exitoso:
```
Started Application in XX seconds
```
## Verificar el despliegue ## Verificar el despliegue
@ -140,8 +116,8 @@ docker compose ps
# CapabilityStatement FHIR a través de nginx # CapabilityStatement FHIR a través de nginx
curl http://localhost/fhir/metadata curl http://localhost/fhir/metadata
# Verificación de llegada al Bus mediante la consulta de un paciente. # Health check del Bus Gateway a través de nginx
curl --location 'http://localhost/fhir/Patient?identifier=http%3A%2F%2Fwww.renaper.gob.ar%2Fdni%7C30945027' curl http://localhost/fhir/Patient?_summary=count
# Logs de un servicio específico # Logs de un servicio específico
docker compose logs -f bus-gateway docker compose logs -f bus-gateway
@ -183,6 +159,7 @@ Todas las variables se definen en el archivo `.env` de la raíz del proyecto.
| Variable | Default | Descripción | | Variable | Default | Descripción |
|---|---|---| |---|---|---|
| `NODO_BASE_URL` | `http://localhost` | URL pública del nodo (usada por GDHCN para construir URLs) |
| `NODO_URL_BASE` | `http://localhost` | URL base del nodo (usada por bus-gateway) | | `NODO_URL_BASE` | `http://localhost` | URL base del nodo (usada por bus-gateway) |
| `TZ` | `America/Argentina/Buenos_Aires` | Zona horaria para todos los servicios | | `TZ` | `America/Argentina/Buenos_Aires` | Zona horaria para todos los servicios |
@ -200,6 +177,14 @@ Todas las variables se definen en el archivo `.env` de la raíz del proyecto.
| `SSL_CERT_PATH` | `./certs/server.crt` | Ruta al certificado TLS del servidor | | `SSL_CERT_PATH` | `./certs/server.crt` | Ruta al certificado TLS del servidor |
| `SSL_KEY_PATH` | `./certs/server.key` | Ruta a la clave privada TLS del servidor | | `SSL_KEY_PATH` | `./certs/server.key` | Ruta a la clave privada TLS del servidor |
### Certificados / Secrets
| Variable | Default | Descripción |
|---|---|---|
| `SIGNATURE_CERT_PATH` | `./certs/trust-network.pem` | Certificado de la red de confianza (TNG/GDHCN) |
| `SIGNATURE_KEY_PATH` | `./certs/trust-network.key` | Clave privada de la red de confianza |
| `SSL_DCC_KEY_PATH` | `./certs/signature.key` | Clave privada para firma de documentos (DSC) |
### HAPI FHIR / PostgreSQL ### HAPI FHIR / PostgreSQL
| Variable | Default | Descripción | | Variable | Default | Descripción |
@ -218,7 +203,7 @@ Todas las variables se definen en el archivo `.env` de la raíz del proyecto.
``` ```
ips-nodo-dominio/ ips-nodo-dominio/
├── docker-compose.yml # Orquestación principal (4 servicios) ├── docker-compose.yml # Orquestación principal (6 servicios)
├── .env.example # Plantilla de variables de entorno ├── .env.example # Plantilla de variables de entorno
├── hapi-config/ ├── hapi-config/
│ └── application.yaml # Configuración de HAPI FHIR (Spring Boot) │ └── application.yaml # Configuración de HAPI FHIR (Spring Boot)

View File

@ -1,4 +1,4 @@
NODO_URL_BASE=localhost NODO_BASE_URL=localhost
# Bus FHIR (base, usado como fallback si no se definen MPI_URL o DOCUMENT_REGISTRY_URL) # Bus FHIR (base, usado como fallback si no se definen MPI_URL o DOCUMENT_REGISTRY_URL)
BUS_URL=http://bus-host:8080 BUS_URL=http://bus-host:8080
@ -13,16 +13,9 @@ DOCUMENT_REGISTRY_URL=http://document-registry-host:8080
MPI_SCOPE=Patient/*.read,Patient/*.write MPI_SCOPE=Patient/*.read,Patient/*.write
DOCUMENT_REGISTRY_SCOPE=DocumentReference/*.read,DocumentReference/*.write DOCUMENT_REGISTRY_SCOPE=DocumentReference/*.read,DocumentReference/*.write
# ITI-67: si es true, no se envía el parámetro `type` en el GET /DocumentReference
# al índice de documentos; se pide todo por subject y se filtra por type localmente.
DOCUMENT_REGISTRY_FILTER_TYPE_LOCALLY=false
# HAPI FHIR Server # HAPI FHIR Server
FHIR_URL=http://hapi-fhir-host:8080/fhir FHIR_URL=http://hapi-fhir-host:8080/fhir
# Nodo nacional (bundle-signer): ejecuta las transformaciones IPS ($ddcc, $dvc, $icvp2, $meow)
BUNDLE_SIGNER_URL=https://conectaton.msal.gob.ar/nodo/fhir
LOG_LEVEL=debug LOG_LEVEL=debug
# Habilita logs de requests/responses salientes al Bus (true | false) # Habilita logs de requests/responses salientes al Bus (true | false)

View File

@ -14,21 +14,12 @@ El componente actúa exclusivamente como gateway: no contiene lógica de negocio
| Transacción | Método | Ruta | Descripción | | Transacción | Método | Ruta | Descripción |
|---|---|---|---| |---|---|---|---|
| ITI-65 | `POST` | `/fhir/IPSTransaction` | Provide Document Bundle (IPS): variante transacción — el cliente envía el Bundle transaction completo (Patient, Bundle document, DocumentReference, List). | | ITI-65 | `POST` | `/fhir/Bundle` | Provide Document Bundle: almacena el Bundle en HAPI FHIR, resuelve el ID nacional del paciente vía `$match` en el Bus y registra el DocumentReference en el Document Registry. |
| ITI-65 | `POST` | `/fhir/IPSDocument` | Provide Document Bundle (IPS): variante document — el cliente envía solo el Bundle IPS (`type: document`); el gateway arma la transacción internamente. | | ITI-67 | `GET` | `/fhir/DocumentReference` | Find Document References: busca el paciente por ID local en el MPI para obtener su ID nacional y consulta los DocumentReferences en el Document Registry. |
| ITI-65 | `POST` | `/fhir/MeOWTransaction` | Provide Document Bundle (MeOW - Medication Overview): variante transacción. |
| ITI-65 | `POST` | `/fhir/MeOWDocument` | Provide Document Bundle (MeOW - Medication Overview): variante document. |
| ITI-65 | `POST` | `/fhir/ITTransaction` | Provide Document Bundle (IT - Consultation Note / respuesta de interconsulta): variante transacción. |
| ITI-65 | `POST` | `/fhir/ITDocument` | Provide Document Bundle (IT - Consultation Note): variante document. |
| ITI-67 | `GET` | `/fhir/DocumentReference` | Find Document References: busca el paciente por ID local en el MPI para obtener su ID nacional y consulta los DocumentReferences en el Document Registry. Admite filtrar por tipo de documento con el parámetro opcional `type`, un token FHIR `system\|code` (ej. `http://loinc.org\|60591-5` para IPS, `http://loinc.org\|56445-0` para MeOW, `http://loinc.org\|57133-1` para IT). |
| ITI-78 | `GET` | `/fhir/Patient` | Patient Demographics Query: búsqueda de pacientes en el MPI. | | ITI-78 | `GET` | `/fhir/Patient` | Patient Demographics Query: búsqueda de pacientes en el MPI. |
| ITI-78 | `GET` | `/fhir/Patient/:id` | Patient Demographics Query: obtención de un paciente por ID en el MPI. | | ITI-78 | `GET` | `/fhir/Patient/:id` | Patient Demographics Query: obtención de un paciente por ID en el MPI. |
| ITI-104 | `POST` | `/fhir/Patient` | Patient Identity Feed: alta de paciente en el MPI. | | ITI-104 | `POST` | `/fhir/Patient` | Patient Identity Feed: alta de paciente en el MPI. |
| ITI-104 | `PUT` | `/fhir/Patient/:id` | Patient Identity Feed: actualización de paciente en el MPI. | | ITI-104 | `PUT` | `/fhir/Patient/:id` | Patient Identity Feed: actualización de paciente en el MPI. |
| — | `GET` | `/fhir/Bundle/:id/$ddcc` | Obtiene el Bundle IPS del HAPI FHIR local y delega la transformación DDCC al nodo nacional (bundle-signer). |
| — | `GET` | `/fhir/Bundle/:id/$dvc` | Obtiene el Bundle IPS del HAPI FHIR local y delega la transformación DVC al nodo nacional (bundle-signer). |
| — | `GET` | `/fhir/Bundle/:id/$icvp2` | Obtiene el Bundle IPS del HAPI FHIR local y delega la transformación ICVP (HCERT) al nodo nacional (bundle-signer). |
| — | `GET` | `/fhir/Bundle/:id/$meow` | Obtiene el Bundle del HAPI FHIR local y delega la transformación MeOW (HCERT) al nodo nacional (bundle-signer). |
## Variables de entorno ## Variables de entorno
@ -48,7 +39,6 @@ cp .env.example .env
| `MPI_URL` | No | URL base del servicio MPI (Master Patient Index). Si no se define, usa `BUS_URL`. | | `MPI_URL` | No | URL base del servicio MPI (Master Patient Index). Si no se define, usa `BUS_URL`. |
| `DOCUMENT_REGISTRY_URL` | No | URL base del Document Registry. Si no se define, usa `BUS_URL`. | | `DOCUMENT_REGISTRY_URL` | No | URL base del Document Registry. Si no se define, usa `BUS_URL`. |
| `FHIR_URL` | Sí | URL base del servidor HAPI FHIR local. | | `FHIR_URL` | Sí | URL base del servidor HAPI FHIR local. |
| `BUNDLE_SIGNER_URL` | Sí | URL base del nodo nacional (bundle-signer) donde se ejecutan las transformaciones IPS (`$ddcc`, `$dvc`, `$icvp2`, `$meow`). |
| `PORT` | No | Puerto en que escucha el servidor. Por defecto `3000`. | | `PORT` | No | Puerto en que escucha el servidor. Por defecto `3000`. |
### Usar la misma URL para todos los servicios del Bus ### Usar la misma URL para todos los servicios del Bus
@ -141,7 +131,7 @@ docker compose logs -f
## Headers requeridos por transacción ## Headers requeridos por transacción
### ITI-65 `POST /fhir/IPSTransaction`, `/fhir/IPSDocument`, `/fhir/MeOWTransaction`, `/fhir/MeOWDocument`, `/fhir/ITTransaction`, `/fhir/ITDocument` ### ITI-65 `POST /fhir/Bundle`
| Header | Descripción | | Header | Descripción |
|---|---| |---|---|

View File

@ -8,7 +8,6 @@ var iti65Router = require('./routes/iti65');
var iti67Router = require('./routes/iti67'); var iti67Router = require('./routes/iti67');
var iti78Router = require('./routes/iti78'); var iti78Router = require('./routes/iti78');
var iti104Router = require('./routes/iti104'); var iti104Router = require('./routes/iti104');
var bundleSignerRouter = require('./routes/bundleSigner');
var app = express(); var app = express();
@ -28,9 +27,6 @@ app.use('/fhir', iti78Router);
// ITI-104: Patient Identity Feed → POST /fhir/Patient, PUT /fhir/Patient/:id // ITI-104: Patient Identity Feed → POST /fhir/Patient, PUT /fhir/Patient/:id
app.use('/fhir', iti104Router); app.use('/fhir', iti104Router);
// Transformaciones IPS (nodo nacional) → GET /fhir/Bundle/:id/$ddcc, $dvc, $icvp2, $meow
app.use('/fhir', bundleSignerRouter);
// 404 // 404
app.use(function (req, res, next) { app.use(function (req, res, next) {
const host = req.get('host') || 'localhost'; const host = req.get('host') || 'localhost';

View File

@ -9,16 +9,10 @@ const config = {
issuer: process.env.BUS_ISSUER, issuer: process.env.BUS_ISSUER,
mpiScope: process.env.MPI_SCOPE, mpiScope: process.env.MPI_SCOPE,
documentRegistryScope: process.env.DOCUMENT_REGISTRY_SCOPE, documentRegistryScope: process.env.DOCUMENT_REGISTRY_SCOPE,
// Si es true, ITI-67 no envía el parámetro `type` al índice de documentos:
// pide todos los DocumentReference del subject y filtra por type localmente.
filterDocumentTypeLocally: process.env.DOCUMENT_REGISTRY_FILTER_TYPE_LOCALLY === 'true',
}, },
fhir: { fhir: {
url: process.env.FHIR_URL, url: process.env.FHIR_URL,
}, },
bundleSigner: {
url: process.env.BUNDLE_SIGNER_URL,
},
logging: { logging: {
enabled: true, enabled: true,
level: 'debug', level: 'debug',
@ -30,7 +24,7 @@ const config = {
errorKey: 'err', errorKey: 'err',
nestedKey: null, nestedKey: null,
}, },
baseURL: process.env.NODO_URL_BASE || 'http://localhost', baseURL: process.env.NODO_BASE_URL || 'http://localhost',
debug: process.env.BUS_DEBUG === 'true', debug: process.env.BUS_DEBUG === 'true',
}; };
@ -41,7 +35,6 @@ const required = [
['MPI_SCOPE', config.bus.mpiScope], ['MPI_SCOPE', config.bus.mpiScope],
['DOCUMENT_REGISTRY_SCOPE', config.bus.documentRegistryScope], ['DOCUMENT_REGISTRY_SCOPE', config.bus.documentRegistryScope],
['FHIR_URL', config.fhir.url], ['FHIR_URL', config.fhir.url],
['BUNDLE_SIGNER_URL', config.bundleSigner.url],
]; ];
const missing = required.filter(([, val]) => !val).map(([key]) => key); const missing = required.filter(([, val]) => !val).map(([key]) => key);

View File

@ -1,27 +0,0 @@
/**
* Tipos de documento soportados por ITI-65 (Provide Document Bundle) e ITI-67
* (Find Document References), cada uno con su código LOINC para
* DocumentReference.type.
*/
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',
},
};
module.exports = { DOCUMENT_TYPES };

View File

@ -1,83 +0,0 @@
const axios = require('axios');
const config = require('../config');
/**
* Fetches the Bundle from the local HAPI FHIR server and delegates the
* transformation to the national node (bundle-signer), which holds the
* signing keys. Returns the national node's response as-is.
*/
async function transformBundle(id, operation, query) {
const bundleUrl = `${config.fhir.url}/Bundle/${id}`;
const { data: bundle } = await axios.get(bundleUrl);
const response = await axios.post(
`${config.bundleSigner.url}/Bundle/$${operation}`,
bundle,
{ params: query },
);
return response;
}
/**
* GET /fhir/Bundle/:id/$ddcc
*/
async function getDDCC(req, res, next) {
try {
const response = await transformBundle(req.params.id, 'ddcc', req.query);
res.status(response.status).json(response.data);
} catch (err) {
next(err);
}
}
/**
* GET /fhir/Bundle/:id/$dvc
*/
async function getDVC(req, res, next) {
try {
const response = await transformBundle(req.params.id, 'dvc', req.query);
res.status(response.status).json(response.data);
} catch (err) {
next(err);
}
}
/**
* GET /fhir/Bundle/:id/$icvp
*/
async function getICVP(req, res, next) {
try {
const response = await transformBundle(req.params.id, 'icvp', req.query);
res.status(response.status).json(response.data);
} catch (err) {
next(err);
}
}
/**
* GET /fhir/Bundle/:id/$icvp2
*/
async function getICVP2(req, res, next) {
try {
const response = await transformBundle(req.params.id, 'icvp2', req.query);
res.status(response.status).json(response.data);
} catch (err) {
next(err);
}
}
/**
* GET /fhir/Bundle/:id/$meow
*/
async function getMeow(req, res, next) {
try {
const response = await transformBundle(req.params.id, 'meow', req.query);
res.status(response.status).json(response.data);
} catch (err) {
next(err);
}
}
module.exports = {
getDDCC, getDVC, getICVP, getICVP2, getMeow,
};

View File

@ -7,7 +7,6 @@ const { createDocumentReference, findDocumentReferenceById } = require('../servi
const { logger } = require('../utils/logger'); const { logger } = require('../utils/logger');
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
const { getResourceByUrl, processDocumentBundleTransaction } = require('../services/fhir'); const { getResourceByUrl, processDocumentBundleTransaction } = require('../services/fhir');
const { DOCUMENT_TYPES } = require('../constants/documentTypes');
const DOCUMENT_REFERENCE_RESOURCE_TYPE = "DocumentReference"; const DOCUMENT_REFERENCE_RESOURCE_TYPE = "DocumentReference";
@ -32,7 +31,7 @@ function extractLocalIdentifier(patient) {
})[0]; })[0];
} }
function generateDocumentReferenceResource(subjectReference, bundleUrl, docType) { function generateDocumentReferenceResource(subjectReference, bundleUrl) {
const documentRefernece = { const documentRefernece = {
resourceType: 'DocumentReference', resourceType: 'DocumentReference',
status: 'current', status: 'current',
@ -44,9 +43,9 @@ function generateDocumentReferenceResource(subjectReference, bundleUrl, docType)
type: { type: {
coding: [ coding: [
{ {
system: docType.system, system: 'http://loinc.org',
code: docType.code, code: '60591-5',
display: docType.display, display: 'Patient Summary Document',
}, },
], ],
}, },
@ -78,20 +77,19 @@ async function getResourcesFromTransactionResponse(transactionResponse) {
} }
/** /**
* Construye un Bundle de tipo transaction a partir de un Bundle de tipo document * Construye un Bundle de tipo transaction a partir de un Bundle de tipo document (IPS).
* (IPS, MeOW, IT, ...). Genera Patient, Bundle (documento), DocumentReference y * Genera Patient, Bundle (IPS), DocumentReference y List (SubmissionSet MHD).
* List (SubmissionSet MHD).
* Usa urn:uuid: como fullUrl para que HAPI FHIR resuelva las referencias internas. * Usa urn:uuid: como fullUrl para que HAPI FHIR resuelva las referencias internas.
*/ */
function buildTransactionFromDocumentBundle(documentBundle, docType) { function buildTransactionFromIPSDocument(ipsBundle) {
if (documentBundle.type !== 'document') { if (ipsBundle.type !== 'document') {
throw createError(400, 'Bundle must be of type document'); throw createError(400, 'Bundle must be of type document');
} }
const patientEntry = (documentBundle.entry || []).find( const patientEntry = (ipsBundle.entry || []).find(
e => e.resource && e.resource.resourceType === 'Patient' e => e.resource && e.resource.resourceType === 'Patient'
); );
if (!patientEntry) { if (!patientEntry) {
throw createError(400, `${docType.label} Bundle must contain a Patient resource`); throw createError(400, 'IPS Bundle must contain a Patient resource');
} }
const patientFullUrl = `urn:uuid:${uuidv4()}`; const patientFullUrl = `urn:uuid:${uuidv4()}`;
@ -110,9 +108,9 @@ function buildTransactionFromDocumentBundle(documentBundle, docType) {
}, },
type: { type: {
coding: [{ coding: [{
system: docType.system, system: 'http://loinc.org',
code: docType.code, code: '60591-5',
display: docType.display, display: 'Patient Summary Document',
}] }]
}, },
date: now, date: now,
@ -160,7 +158,7 @@ function buildTransactionFromDocumentBundle(documentBundle, docType) {
}, },
{ {
fullUrl: bundleFullUrl, fullUrl: bundleFullUrl,
resource: documentBundle, resource: ipsBundle,
request: { method: 'POST', url: 'Bundle' } request: { method: 'POST', url: 'Bundle' }
}, },
{ {
@ -180,7 +178,7 @@ function buildTransactionFromDocumentBundle(documentBundle, docType) {
* Lógica central de ITI-65: persiste la transacción en HAPI FHIR, resuelve el * 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. * paciente en el Bus y registra un DocumentReference apuntando al Bundle guardado.
*/ */
async function executeITI65(transaction, token, docType) { async function executeITI65(transaction, token) {
const transactionResponse = await processDocumentBundleTransaction(transaction); const transactionResponse = await processDocumentBundleTransaction(transaction);
const resources = await getResourcesFromTransactionResponse(transactionResponse); const resources = await getResourcesFromTransactionResponse(transactionResponse);
@ -195,73 +193,59 @@ async function executeITI65(transaction, token, docType) {
const nationalPatientId = patientSearchset.entry[0].fullUrl; const nationalPatientId = patientSearchset.entry[0].fullUrl;
const bundleReference = `${config.baseURL}/fhir/Bundle/${localIPSDocument.id}`; const bundleReference = `${config.baseURL}/fhir/Bundle/${localIPSDocument.id}`;
const documentReference = generateDocumentReferenceResource(nationalPatientId, bundleReference, docType); const documentReference = generateDocumentReferenceResource(nationalPatientId, bundleReference);
return createDocumentReference(token, documentReference); 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 * ITI-65: Provide Document Bundle (MHD) variante transacción
* *
* POST /fhir/iti65
*
* Espera un Bundle de tipo transaction construido por el cliente. * Espera un Bundle de tipo transaction construido por el cliente.
*/ */
function provideTransaction(docType) { async function provideIPSTransaction(req, res, next) {
return async function (req, res, next) {
try { try {
const transaction = req.body; const transaction = req.body;
const token = await acquireToken(); const token = await getBusToken(
const result = await executeITI65(transaction, token, docType); config.bus.url,
config.bus.jwtSecret,
config.bus.issuer,
[config.bus.mpiScope, config.bus.documentRegistryScope].join(',')
);
const result = await executeITI65(transaction, token);
return res.status(200).json(result); return res.status(200).json(result);
} catch (err) { } catch (err) {
next(err); next(err);
} }
};
} }
/** /**
* ITI-65: Provide Document Bundle (MHD) variante document * ITI-65: Provide Document Bundle (MHD) variante IPS document
* *
* Espera un Bundle de tipo document. Genera internamente el Bundle transaction * POST /fhir/Bundle
* (Patient, Bundle, DocumentReference, List) y ejecuta el mismo flujo que *
* provideTransaction. * Espera un Bundle de tipo document (IPS). Genera internamente el Bundle
* transaction y ejecuta el mismo flujo que provideDocumentBundle.
*/ */
function provideDocumentBundle(docType) { async function provideIPSDocumentBundle(req, res, next) {
return async function (req, res, next) {
try { try {
const documentBundle = req.body; const ipsBundle = req.body;
if (!documentBundle || documentBundle.resourceType !== 'Bundle') { if (!ipsBundle || ipsBundle.resourceType !== 'Bundle') {
throw createError(400, 'Request body must be a FHIR Bundle resource'); throw createError(400, 'Request body must be a FHIR Bundle resource');
} }
const transaction = buildTransactionFromDocumentBundle(documentBundle, docType); const transaction = buildTransactionFromIPSDocument(ipsBundle);
const token = await acquireToken(); const token = await getBusToken(
const result = await executeITI65(transaction, token, docType); config.bus.url,
config.bus.jwtSecret,
config.bus.issuer,
[config.bus.mpiScope, config.bus.documentRegistryScope].join(',')
);
const result = await executeITI65(transaction, token);
return res.status(200).json(result); return res.status(200).json(result);
} catch (err) { } catch (err) {
next(err); next(err);
} }
};
} }
const provideIPSTransaction = provideTransaction(DOCUMENT_TYPES.IPS); module.exports = { provideIPSTransaction, provideIPSDocumentBundle };
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,
};

View File

@ -4,6 +4,16 @@ const { getBusToken } = require('../utils/busAuth');
const { findPatient } = require('../services/patient'); const { findPatient } = require('../services/patient');
const { findDocumentReferenceByPatient } = require('../services/documentReference'); const { findDocumentReferenceByPatient } = require('../services/documentReference');
const NATIONAL_ID_SYSTEM = 'https://federador.msal.gob.ar/patient-id';
/**
* Extracts the Federador national identifier from a Patient resource.
*/
function extractNationalIdentifier(patient) {
const identifiers = Array.isArray(patient.identifier) ? patient.identifier : [];
return identifiers.find(id => id.system === NATIONAL_ID_SYSTEM) || null;
}
/** /**
@ -11,24 +21,15 @@ const { findDocumentReferenceByPatient } = require('../services/documentReferenc
* *
* GET /fhir/DocumentReference?subject=:localId * GET /fhir/DocumentReference?subject=:localId
* GET /fhir/DocumentReference?patient.identifier=:localId * GET /fhir/DocumentReference?patient.identifier=:localId
* GET /fhir/DocumentReference?subject=:localId&type=:type
* *
* Flow: * Flow:
* 1. Fetch the Patient from the Bus by local ID to obtain the national identifier. * 1. Fetch the Patient from the Bus by local ID to obtain the national identifier.
* 2. Search DocumentReferences in the Bus by that national identifier * 2. Search DocumentReferences in the Bus by that national identifier.
* (opcionalmente filtrando por tipo de documento).
* 3. Return the resulting Bundle. * 3. Return the resulting Bundle.
* *
* Required query parameter (one of): * Required query parameter (one of):
* subject local Patient ID in the Bus. * subject local Patient ID in the Bus.
* patient.identifier same semantics, alternate FHIR parameter name. * patient.identifier same semantics, alternate FHIR parameter name.
*
* Optional query parameter:
* type token FHIR de DocumentReference.type (`system|code`, `|code` o `code`),
* ej: `http://loinc.org|60591-5` (IPS), `http://loinc.org|56445-0` (MeOW),
* `http://loinc.org|57133-1` (IT). Se pasa tal cual al Bus, sin traducir
* alias propios, para no apartarse de la semántica estándar de búsqueda
* por token de FHIR. Si se omite, devuelve documentos de cualquier tipo.
*/ */
async function listDocumentReference(req, res, next) { async function listDocumentReference(req, res, next) {
try { try {
@ -36,7 +37,6 @@ async function listDocumentReference(req, res, next) {
if (!localPatientIdentifier) { if (!localPatientIdentifier) {
throw createError(400, 'Missing required query parameter: subject or patient.identifier'); throw createError(400, 'Missing required query parameter: subject or patient.identifier');
} }
const documentType = req.query.type;
const token = await getBusToken( const token = await getBusToken(
config.bus.url, config.bus.url,
config.bus.jwtSecret, config.bus.jwtSecret,
@ -47,15 +47,13 @@ async function listDocumentReference(req, res, next) {
].join(',') ].join(',')
); );
// 1. Obtengo el identificador nacional del paciente en forma de referencia // 1. Obtengo el identificador nacional del paciente en forma de referencia
// (fullUrl del recurso Patient en el Bus).
const patientSearchset = await findPatient(token, { identifier: localPatientIdentifier }); const patientSearchset = await findPatient(token, { identifier: localPatientIdentifier });
if (!patientSearchset || !Array.isArray(patientSearchset.entry) || patientSearchset.entry.length === 0) { if (!patientSearchset) {
throw createError(422, 'Could not resolve national identifier for the given patient'); throw createError(422, 'Could not resolve national identifier for the given patient');
} }
const patientNationalId = patientSearchset.entry[0].fullUrl; const patientNationalId = patientSearchset.entry[0].fullUrl
// 2. Obtengo el listado de entradas del indice de atenciones asociadada al id de paciente // 2. Obtengo el listado de entradas del indice de atenciones asociadada al id de paciente.
// (y, si se pidió, al tipo de documento). const documentReferenceSearchset = await findDocumentReferenceByPatient(token, patientNationalId)
const documentReferenceSearchset = await findDocumentReferenceByPatient(token, patientNationalId, documentType)
res.status(200).json(documentReferenceSearchset); res.status(200).json(documentReferenceSearchset);
} catch (err) { } catch (err) {

View File

@ -1,2 +0,0 @@
*.png
*.pdf

View File

@ -1,13 +1,13 @@
%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#1D4ED8', 'actorBorder': '#1E3A8A', 'actorTextColor': '#FFFFFF', 'actorLineColor': '#93C5FD', 'noteBkgColor': '#FEF3C7', 'noteTextColor': '#92400E', 'noteBorderColor': '#F59E0B', 'signalColor': '#1E293B', 'signalTextColor': '#1E293B', 'labelBoxBkgColor': '#7C3AED', 'labelBoxBorderColor': '#5B21B6', 'labelTextColor': '#FFFFFF'}}}%%
sequenceDiagram sequenceDiagram
autonumber autonumber
participant HIS_A participant HIS_A
participant NodoDominio as Nodo participant NodoDominio as NodoDominio (HIS_A)
box rgb(237,233,254) PINS participant BusNacion as BusNacion
participant FederadorNacion as Federador participant FederadorNacion as Federador
end Note over HIS_A,FederadorNacion: Gestion de Identidad (PIXm/PDQm)
Note over HIS_A,FederadorNacion: Gestion de Identidad HIS_A->>NodoDominio: ITI-104 (Crea/Actualiza)
HIS_A->>NodoDominio: ITI-104: Federa un nuevo paciente en el MPI <br /> [POST /Patient] NodoDominio->>BusNacion: Enruta petición FHIR (POST /Patient)
NodoDominio->>FederadorNacion: Federa un nuevo paciente en el Federador <br /> [POST /Patient] BusNacion->>FederadorNacion: Ejecuta operación en el Federador
FederadorNacion-->>NodoDominio: 201 (Created) FederadorNacion-->>BusNacion: 201 Created (Recurso)
NodoDominio-->>HIS_A: 200 OK (Patient) BusNacion-->>NodoDominio: Retorna Respuesta FHIR
NodoDominio-->>HIS_A: Confirma Operación

View File

@ -1,21 +1,20 @@
%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#1D4ED8', 'actorBorder': '#1E3A8A', 'actorTextColor': '#FFFFFF', 'actorLineColor': '#93C5FD', 'noteBkgColor': '#FEF3C7', 'noteTextColor': '#92400E', 'noteBorderColor': '#F59E0B', 'signalColor': '#1E293B', 'signalTextColor': '#1E293B', 'labelBoxBkgColor': '#7C3AED', 'labelBoxBorderColor': '#5B21B6', 'labelTextColor': '#FFFFFF'}}}%%
sequenceDiagram sequenceDiagram
autonumber autonumber
participant HIS_A participant HIS_A
participant NodoDominio as Nodo (Emisor) participant NodoDominio as "Nodo Dominio (emisor)"
box rgb(237,233,254) PINS participant BusNacion as "Bus Nacion"
participant FederadorNacion as Federador participant FederadorNacion as "Federador Nacion"
participant IndiceNacion as Indice Documentos participant IndiceNacion as "Indice Nacion"
end Note over HIS_A,IndiceNacion: 1. Recepcion Local del Documento Bundle (POST /Bundle)
Note over HIS_A,IndiceNacion: Variantes: /IPSTransaction, /IPSDocument, /MeOWTransaction, /MeOWDocument, /ITTransaction, /ITDocument HIS_A->>NodoDominio: ITI-65 (Crea/Actualiza)
Note over HIS_A,IndiceNacion: 1. Recepcion Local del Documento Bundle Note over NodoDominio,FederadorNacion: 2. Resolución de Identidad (ITI-83)
HIS_A->>NodoDominio: ITI65: Provee un nuevo documento (bundle) NodoDominio->>BusNacion: Get /Patient/$ihe-pix?sourceIdentifier=[ID_Local]
Note right of NodoDominio: El Nodo almacena físicamente el documento y extrae la metadata. BusNacion->>FederadorNacion: Consulta correspondencia
Note over NodoDominio,FederadorNacion: 2. Resolución de Identidad FederadorNacion-->>BusNacion: 200 OK (Retorna ID Nacional)
NodoDominio->>FederadorNacion: Busca al paciente por el su identificador local [GET /Patient?identifier=<ID_Local>] BusNacion-->>NodoDominio: Entrega ID Nacional
FederadorNacion-->>NodoDominio: 200 OK (Patient Searchset) Note over NodoDominio,IndiceNacion: 3. Enrutamiento de la Petición (POST /Bundle)
Note over NodoDominio,IndiceNacion: 3. Enrutamiento de la Petición NodoDominio->>BusNacion: Publica Document Reference (GET /DocumentRefereence?subject=[ID_Nacional])
Note right of NodoDominio: El Nodo genera la referencia al documento y la asocia al identificador nacional del paciente. BusNacion->>IndiceNacion: Registra Referencia
NodoDominio->>IndiceNacion: Guarda la referencia al documento en el índice de documentos [POST /DocumentReference] IndiceNacion-->>BusNacion: 201 (Created)
IndiceNacion-->>NodoDominio: 201 (Created) BusNacion-->>NodoDominio: Confirma operación
NodoDominio-->>HIS_A: 200 OK (DocumentReference) NodoDominio-->>HIS_A: 200 OK (Publicación exitosa)

View File

@ -1,23 +1,27 @@
%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#1D4ED8', 'actorBorder': '#1E3A8A', 'actorTextColor': '#FFFFFF', 'actorLineColor': '#93C5FD', 'noteBkgColor': '#FEF3C7', 'noteTextColor': '#92400E', 'noteBorderColor': '#F59E0B', 'signalColor': '#1E293B', 'signalTextColor': '#1E293B', 'labelBoxBkgColor': '#7C3AED', 'labelBoxBorderColor': '#5B21B6', 'labelTextColor': '#FFFFFF'}}}%%
sequenceDiagram sequenceDiagram
autonumber autonumber
participant HIS_A participant HIS_A
participant NodoDominio as Nodo A participant NodoDominio as "Nodo Dominio (emisor)"
box rgb(237,233,254) PINS participant BusNacion as "Bus Nacion"
participant FederadorNacion as Federador participant FederadorNacion as "Federador Nacion"
participant IndiceNacion as Indice Documentos participant IndiceNacion as "Indice Nacion"
end participant NodoDominio2 as "Nodo Dominio (receptor)"
participant NodoDominio2 as Nodo B Note over HIS_A,FederadorNacion: 1. Busqueda y resolución de Identidad (PIXm ITI-83)
Note over HIS_A,FederadorNacion: Tipos soportados (LOINC): 60591-5 IPS, 56445-0 MeOW, 57133-1 IT (parámetro opcional type=system|code) HIS_A->>NodoDominio: ITI-67: GET /DocumentReference?subject=[ID_Lcocal]
Note over HIS_A,FederadorNacion: 1. Busqueda y resolución de Identidad NodoDominio->>BusNacion: GET /Patient/$ihe-pix?sourceIdentifier=[ID_Local]
HIS_A->>NodoDominio: ITI67: Solicita los documentos asociados al paciente por su identificador local, opcionalmente filtrando por tipo <br /> [GET /DocumentReference?patient.identifier=<ID_Local>&type=<system|code>] BusNacion->>FederadorNacion: Consulta correspondencia en MPI
NodoDominio->>FederadorNacion: Busca el paciente por su identificador local <br /> [GET /Patient?identifier=<ID_Local>] FederadorNacion-->>BusNacion: 200 OK (Retorna ID Nacional)
FederadorNacion-->>NodoDominio: 200 OK (Patient Searchset) BusNacion-->>NodoDominio: Entrega ID Nacional
Note over NodoDominio,IndiceNacion: 2. Busqueda de Metadatos Note over NodoDominio,IndiceNacion: 2. Busqueda de Metadatos (MHD ITI-67 adaptada)
Note right of NodoDominio: El Nodo obtiene el identificador nacional del paciente NodoDominio->>BusNacion: GET /DocumentReference?subject=[ID_Nacional]
NodoDominio->>IndiceNacion: Busca los documentos asociados al paciente por su identificador nacional (y tipo, si se pidió) <br /> [GET /DocumentReference?patient=<ID_Nacional>&type=<system|code>] BusNacion->>IndiceNacion: Consulta en el índice de referencias
IndiceNacion-->>NodoDominio: 200 OK (DocumentReference Searchset) IndiceNacion-->>BusNacion: 200 OK (Bundle "searchset")
NodoDominio-->>HIS_A: 200 OK (DocumentReference Searchset) BusNacion-->>NodoDominio: Retorna Bundle con Document Reference
Note over HIS_A,NodoDominio2: 3. Descarga del documento (ITI-68) Note over HIS_A,NodoDominio2: 3. Descarga del documento (ITI-68 P2P)
HIS_A->>NodoDominio2: Obtiene el documento <br /> [GET <URL_Bundle>] HIS_A->>NodoDominio: Solicitar descarga de [URL_Directa]
NodoDominio2-->>HIS_A: 200 OK (Bundle) NodoDominio->>NodoDominio2: GET [URL_Directa] (Conexión P2P, bypass BusNación)
NodoDominio2-->>NodoDominio: 200 OK (Retorna el documento clinico)
NodoDominio-->>HIS_A: Entrega documento fisico

View File

@ -1,13 +1,13 @@
%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#1D4ED8', 'actorBorder': '#1E3A8A', 'actorTextColor': '#FFFFFF', 'actorLineColor': '#93C5FD', 'noteBkgColor': '#FEF3C7', 'noteTextColor': '#92400E', 'noteBorderColor': '#F59E0B', 'signalColor': '#1E293B', 'signalTextColor': '#1E293B', 'labelBoxBkgColor': '#7C3AED', 'labelBoxBorderColor': '#5B21B6', 'labelTextColor': '#FFFFFF'}}}%%
sequenceDiagram sequenceDiagram
autonumber autonumber
participant HIS_A participant HIS_A
participant NodoDominio as Nodo participant NodoDominio as "Nodo Dominio (Repositorio Local)"
box rgb(237,233,254) PINS participant BusNacion as "Bus Nacion"
participant FederadorNacion as Federador participant FederadorNacion as "Federador Nacion"
end Note over HIS_A,FederadorNacion: Gestion de Identidad (PIXm/PDQm)
Note over HIS_A,FederadorNacion: Gestion de Identidad HIS_A->>NodoDominio: ITI-78 (Consulta)
HIS_A->>NodoDominio: ITI-78: Busca un paciente en el MPI <br /> [GET /Patient?identifier=<ID_Local>] NodoDominio->>BusNacion: Enruta petición FHIR (GET /Partient)
NodoDominio->>FederadorNacion: Busca un paciente en el Federador <br /> [GET /Patient?identifier=<ID_Local>] BusNacion->>FederadorNacion: Ejecuta operación en el Federador
FederadorNacion-->>NodoDominio: 200 OK (PatientSearchset) FederadorNacion-->>BusNacion: 200 OK (Bundle)
NodoDominio-->>HIS_A: 200 OK (Patient Searchset) BusNacion-->>NodoDominio: Retorna Respuesta FHIR
NodoDominio-->>HIS_A: Confirma Operación

View File

@ -7,7 +7,7 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"axios": "1.14.0", "axios": "^1.3.5",
"cookie-parser": "~1.4.4", "cookie-parser": "~1.4.4",
"debug": "~2.6.9", "debug": "~2.6.9",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
@ -16,15 +16,13 @@
"jade": "~1.11.0", "jade": "~1.11.0",
"morgan": "~1.9.1", "morgan": "~1.9.1",
"pino": "^10.3.1", "pino": "^10.3.1",
"uuid": "9.0.1" "uuid": "^14.0.0"
}, },
"devDependencies": { "devDependencies": {
"jest": "^30.3.0" "jest": "^30.3.0"
}, },
"jest": { "jest": {
"testEnvironment": "node", "testEnvironment": "node",
"setupFiles": [ "setupFiles": ["./tests/setup.js"]
"./tests/setup.js"
]
} }
} }

View File

@ -1,15 +0,0 @@
const express = require('express');
const router = express.Router();
const {
getDDCC, getDVC, getICVP, getICVP2, getMeow,
} = require('../controllers/bundleSigner');
// Transformaciones IPS delegadas al nodo nacional (bundle-signer):
// obtienen el Bundle del HAPI FHIR local y postean al nodo nacional.
router.get('/Bundle/:id/([\$])ddcc', getDDCC);
router.get('/Bundle/:id/([\$])dvc', getDVC);
router.get('/Bundle/:id/([\$])icvp', getICVP);
router.get('/Bundle/:id/([\$])icvp2', getICVP2);
router.get('/Bundle/:id/([\$])meow', getMeow);
module.exports = router;

View File

@ -1,24 +1,11 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { const { provideIPSTransaction, provideIPSDocumentBundle } = require('../controllers/iti65');
provideIPSTransaction,
provideIPSDocumentBundle,
provideMeOWTransaction,
provideMeOWDocumentBundle,
provideITTransaction,
provideITDocumentBundle,
} = require('../controllers/iti65');
// ITI-65: Provide Document Bundle — IPS // ITI-65: Provide Document Bundle — variante transaction Bundle
router.post('/IPSTransaction', provideIPSTransaction); router.post('/IPSTransaction', provideIPSTransaction);
// ITI-65: Provide Document Bundle — variante IPS document Bundle (type: document)
router.post('/IPSDocument', provideIPSDocumentBundle); router.post('/IPSDocument', provideIPSDocumentBundle);
// ITI-65: Provide Document Bundle — MeOW (Medication Overview)
router.post('/MeOWTransaction', provideMeOWTransaction);
router.post('/MeOWDocument', provideMeOWDocumentBundle);
// ITI-65: Provide Document Bundle — IT (Consultation Note / respuesta de interconsulta)
router.post('/ITTransaction', provideITTransaction);
router.post('/ITDocument', provideITDocumentBundle);
module.exports = router; module.exports = router;

View File

@ -69,84 +69,22 @@ async function findDocumentReferenceBySubject(token, subjectSystem, subjectValue
} }
/** /**
* Parses a FHIR token search value (`system|code`, `|code` o `code`) into its parts. * Searches DocumentReferences in the document registry by patient internal ID
* `system === null` indica la forma `|code` (exige coding sin system). * (GET /fhir/DocumentReference?patient=value).
* `system === undefined` indica la forma `code` (sin restricción de system).
* @param {string} token
* @returns {{ system: string|null|undefined, code: string }}
*/
function parseTypeToken(token) {
const pipeIndex = token.indexOf('|');
if (pipeIndex === -1) {
return { system: undefined, code: token };
}
const system = token.slice(0, pipeIndex);
return { system: system === '' ? null : system, code: token.slice(pipeIndex + 1) };
}
/**
* Indica si el CodeableConcept `type` de un DocumentReference matchea el token FHIR dado,
* replicando la semántica estándar de búsqueda por token (`system|code`, `|code` o `code`)
* que el índice de documentos aplicaría del lado del servidor.
* @param {object} resource - DocumentReference resource.
* @param {string} token - Token FHIR de DocumentReference.type.
* @returns {boolean}
*/
function matchesTypeToken(resource, token) {
const { system, code } = parseTypeToken(token);
const codings = resource?.type?.coding ?? [];
return codings.some(coding => {
if (system === null) {
return !coding.system && coding.code === code;
}
if (system !== undefined) {
return coding.system === system && coding.code === code;
}
return coding.code === code;
});
}
/**
* Filtra las entries de un Bundle searchset de DocumentReference por type,
* ajustando `total` a la cantidad de entries resultantes.
* @param {object} bundle - FHIR Bundle (searchset).
* @param {string} type - Token FHIR de DocumentReference.type.
* @returns {object} Bundle filtrado.
*/
function filterBundleByType(bundle, type) {
if (!bundle || !Array.isArray(bundle.entry)) {
return bundle;
}
const entry = bundle.entry.filter(e => matchesTypeToken(e.resource, type));
return { ...bundle, entry, total: entry.length };
}
/**
* Searches DocumentReferences in the document registry by patient internal ID,
* opcionalmente filtrando por tipo de documento
* (GET /fhir/DocumentReference?patient=value[&type=system|code]).
*
* Si `config.bus.filterDocumentTypeLocally` está activo y se pidió `type`, el parámetro
* `type` no se envía al índice de documentos: se piden todos los DocumentReference del
* subject y el filtrado por tipo se hace localmente sobre el Bundle resultante.
* @param {string} documentRegistryUrl - Base URL of the document registry service. * @param {string} documentRegistryUrl - Base URL of the document registry service.
* @param {string} token - Bearer access token. * @param {string} token - Bearer access token.
* @param {string} patientId - Internal patient ID in the document registry. * @param {string} patientId - Internal patient ID in the document registry.
* @param {string} [type] - Token FHIR (`system|code` o `code`) de DocumentReference.type, ej: `http://loinc.org|60591-5`.
* @returns {Promise<object>} FHIR Bundle (searchset) with matching DocumentReferences. * @returns {Promise<object>} FHIR Bundle (searchset) with matching DocumentReferences.
*/ */
async function findDocumentReferenceByPatient(token, patientId, type) { async function findDocumentReferenceByPatient(token, patientId) {
const request = createBusRequest(token); const request = createBusRequest(token);
const filterLocally = Boolean(type) && config.bus.filterDocumentTypeLocally;
const params = { subject: patientId };
if (type && !filterLocally) {
params.type = type;
}
const response = await request.get( const response = await request.get(
URL.parse(INDICE_ATENCION_URL), URL.parse(INDICE_ATENCION_URL),
{ params } {
params: { subject: patientId },
}
); );
return filterLocally ? filterBundleByType(response.data, type) : response.data; return response.data;
} }
/** /**

View File

@ -1,6 +1,5 @@
require('dotenv').config(); require('dotenv').config();
const axios = require('axios'); const axios = require('axios');
const config = require('../../config');
const { getBusToken } = require('../../utils/busAuth'); const { getBusToken } = require('../../utils/busAuth');
const { const {
createDocumentReference, createDocumentReference,
@ -77,10 +76,6 @@ describe('findDocumentReferenceBySubject', () => {
}); });
describe('findDocumentReferenceByPatient', () => { describe('findDocumentReferenceByPatient', () => {
afterEach(() => {
config.bus.filterDocumentTypeLocally = false;
});
it('GETs /fhir/DocumentReference with patient ID as subject param', async () => { it('GETs /fhir/DocumentReference with patient ID as subject param', async () => {
const token = await acquireToken(); const token = await acquireToken();
const bundle = { resourceType: 'Bundle', entry: [] }; const bundle = { resourceType: 'Bundle', entry: [] };
@ -94,55 +89,6 @@ describe('findDocumentReferenceByPatient', () => {
); );
expect(result).toEqual(bundle); expect(result).toEqual(bundle);
}); });
it('sends the type param when filterDocumentTypeLocally is disabled', async () => {
config.bus.filterDocumentTypeLocally = false;
const token = await acquireToken();
const bundle = { resourceType: 'Bundle', entry: [] };
mockRequest.get.mockResolvedValue({ data: bundle });
await findDocumentReferenceByPatient(token, 'http://mpi/fhir/Patient/123', 'http://loinc.org|60591-5');
expect(mockRequest.get).toHaveBeenCalledWith(
expect.anything(),
{ params: { subject: 'http://mpi/fhir/Patient/123', type: 'http://loinc.org|60591-5' } }
);
});
it('omits the type param and filters the bundle locally when filterDocumentTypeLocally is enabled', async () => {
config.bus.filterDocumentTypeLocally = true;
const token = await acquireToken();
const ipsEntry = {
resource: { resourceType: 'DocumentReference', type: { coding: [{ system: 'http://loinc.org', code: '60591-5' }] } },
};
const meowEntry = {
resource: { resourceType: 'DocumentReference', type: { coding: [{ system: 'http://loinc.org', code: '56445-0' }] } },
};
const bundle = { resourceType: 'Bundle', total: 2, entry: [ipsEntry, meowEntry] };
mockRequest.get.mockResolvedValue({ data: bundle });
const result = await findDocumentReferenceByPatient(token, 'http://mpi/fhir/Patient/123', 'http://loinc.org|60591-5');
expect(mockRequest.get).toHaveBeenCalledWith(
expect.anything(),
{ params: { subject: 'http://mpi/fhir/Patient/123' } }
);
expect(result).toEqual({ resourceType: 'Bundle', total: 1, entry: [ipsEntry] });
});
it('matches the bare `code` token form regardless of coding system', async () => {
config.bus.filterDocumentTypeLocally = true;
const token = await acquireToken();
const matching = {
resource: { resourceType: 'DocumentReference', type: { coding: [{ system: 'http://other-system.org', code: '60591-5' }] } },
};
const bundle = { resourceType: 'Bundle', total: 1, entry: [matching] };
mockRequest.get.mockResolvedValue({ data: bundle });
const result = await findDocumentReferenceByPatient(token, 'http://mpi/fhir/Patient/123', '60591-5');
expect(result.entry).toEqual([matching]);
});
}); });
describe('searchDocumentReference', () => { describe('searchDocumentReference', () => {

View File

@ -1,43 +0,0 @@
@startuml
skinparam backgroundColor #FFFFFF
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
title Diagrama de Contenedores para la Conectatón Argentina
System_Ext(historiaClinica, 'HCI', 'Registtra la historia clinica y genera resumenes de historia clinica (IPS)')
System_Boundary(nodo, 'Nodo'){
Container(nginx, 'nginx', 'Nginx', 'Direcciona las llamadas a los contenedores correspondientes')
Container(hapiFHIR, 'hapi-fhir', 'Java', 'Servidor HAPI FHIR. Brinda soporte para el almacenamiento de documentos IPS')
Container(busGateway, 'bus-gateway', 'NodeJS/Express', 'Orquesta la comunicación con los servicios del Ministerio de Salud de la nación')
ContainerDb(hapiDB, 'hapi-db', 'Postgres', 'Almacena datos del servidor HAPI FHIR')
}
System_Boundary(pins, 'PINS'){
Container(federador, 'Federador', 'Java/SpringBoot', 'Brinda la funcionalidad para la federación, busqueda y obtención de pacientes')
Container(indiceDocumentos, 'Indice de Documentos', 'Java/SpringBoot', 'Servicio de registro, busqueda y obtencion de referencias a documentos de salud')
}
Rel(historiaClinica, nginx, 'Provee documento IPS')
Rel(historiaClinica, nginx, 'Consulta el indice de documentos')
Rel(historiaClinica, nginx, 'Obtiene documentos IPS')
Rel(historiaClinica, nginx, 'Federa pacientes')
Rel(nginx, busGateway, 'Delega ITI65, ITI67, ITI104 e ITI78')
Rel(nginx, hapiFHIR, 'Delega ITI68')
Rel(busGateway, hapiFHIR, 'Guarda documento IPS')
Rel(hapiFHIR, hapiDB, 'Persiste datos')
Rel(busGateway, federador, 'Federa, busca y recupera pacientes')
Rel(busGateway, indiceDocumentos, 'Registra, busca y repuera referencias a documentos')
@enduml

View File

@ -3,8 +3,7 @@ services:
container_name: hapi-fhir container_name: hapi-fhir
image: "hapiproject/hapi:latest" image: "hapiproject/hapi:latest"
depends_on: depends_on:
hapi-db: - hapi-db
condition: service_healthy
volumes: volumes:
- ./hapi-config:/data/hapi - ./hapi-config:/data/hapi
environment: environment:
@ -38,11 +37,47 @@ services:
retries: 5 retries: 5
command: -p 5433 command: -p 5433
gdhcn-validator-service:
container_name: gdhcn-validator-service
image: createchile/gdhcn-validator_backend:v1.1
ports:
- 8182:8080
secrets:
- trust-network-cert
- trust-network-key
- signature-key
environment:
- POSTGRES_DB=gdhcn-validator
- SERVER_PORT=8080
- SPRING_DATASOURCE_URL=jdbc:postgresql://gdhcn-db:5432/gdhcn-validator
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=root
- TNG_TLS_PEM=/run/secrets/trust-network-cert
- TNG_TLS_KEY=/run/secrets/trust-network-key
- TNG_DSC_PRIVATEKEY=/run/secrets/signature-key
- TNG_COUNTRY=AR
- TNG_DSC_PRIVATEKEY_KID=I1BAX8FATLs=
- GDHCN_BASEURL=${NODO_URL_BASE:-http://localhost}/gdhcn
networks:
- hapi-network
volumes:
- ./json:/json
gdhcn-db:
container_name: gdhcn-db
image: postgres:16-alpine
restart: always
volumes:
- gdhcn-data:/var/lib/postgresql/data
networks:
- hapi-network
environment:
- POSTGRES_DB=gdhcn-validator
- POSTGRES_USER=root
- POSTGRES_PASSWORD=root
bus-gateway: bus-gateway:
container_name: bus-gateway container_name: bus-gateway
depends_on:
hapi-fhir:
condition: service_started
build: build:
context: ./bus-gateway context: ./bus-gateway
environment: environment:
@ -55,9 +90,7 @@ services:
DOCUMENT_REGISTRY_URL: ${DOCUMENT_REGISTRY_URL} DOCUMENT_REGISTRY_URL: ${DOCUMENT_REGISTRY_URL}
MPI_SCOPE: ${MPI_SCOPE} MPI_SCOPE: ${MPI_SCOPE}
DOCUMENT_REGISTRY_SCOPE: ${DOCUMENT_REGISTRY_SCOPE} DOCUMENT_REGISTRY_SCOPE: ${DOCUMENT_REGISTRY_SCOPE}
DOCUMENT_REGISTRY_FILTER_TYPE_LOCALLY: ${DOCUMENT_REGISTRY_FILTER_TYPE_LOCALLY:-false}
FHIR_URL: http://hapi-fhir:8080/fhir FHIR_URL: http://hapi-fhir:8080/fhir
BUNDLE_SIGNER_URL: ${BUNDLE_SIGNER_URL}
BUS_DEBUG: ${BUS_DEBUG} BUS_DEBUG: ${BUS_DEBUG}
ports: ports:
- 9229:9229 - 9229:9229
@ -69,10 +102,8 @@ services:
container_name: nginx container_name: nginx
image: nginx:alpine image: nginx:alpine
depends_on: depends_on:
hapi-fhir: - hapi-fhir
condition: service_started - bus-gateway
bus-gateway:
condition: service_started
environment: environment:
TZ: ${TZ:-America/Argentina/Buenos_Aires} TZ: ${TZ:-America/Argentina/Buenos_Aires}
ports: ports:
@ -95,8 +126,17 @@ volumes:
hapi-data: hapi-data:
name: hapi-data name: hapi-data
driver: local driver: local
gdhcn-data:
name: gdhcn-data
driver: local
secrets: secrets:
ssl_cert: ssl_cert:
file: ${SSL_CERT_PATH:-./certs/server.crt} file: ${SSL_CERT_PATH:-./certs/server.crt}
ssl_key: ssl_key:
file: ${SSL_KEY_PATH:-./certs/server.key} file: ${SSL_KEY_PATH:-./certs/server.key}
trust-network-cert:
file: ${SIGNATURE_CERT_PATH:-./certs/trust-network.pem}
trust-network-key:
file: ${SIGNATURE_KEY_PATH:-./certs/trust-network.key}
signature-key:
file: ${SSL_DCC_KEY_PATH:-./certs/signature.key}

View File

@ -35,46 +35,6 @@ http {
proxy_read_timeout 90s; proxy_read_timeout 90s;
} }
# ITI-65: Provide Document Bundle — MeOW (Medication Overview) transaction Bundle
location /fhir/MeOWTransaction {
proxy_pass http://bus_gateway;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90s;
}
# ITI-65: Provide Document Bundle — MeOW (Medication Overview) document Bundle
location /fhir/MeOWDocument {
proxy_pass http://bus_gateway;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90s;
}
# ITI-65: Provide Document Bundle — IT (Consultation Note) transaction Bundle
location /fhir/ITTransaction {
proxy_pass http://bus_gateway;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90s;
}
# ITI-65: Provide Document Bundle — IT (Consultation Note) document Bundle
location /fhir/ITDocument {
proxy_pass http://bus_gateway;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90s;
}
# ITI-67: Find Document References # ITI-67: Find Document References
location /fhir/DocumentReference { location /fhir/DocumentReference {
proxy_pass http://bus_gateway; proxy_pass http://bus_gateway;
@ -96,10 +56,9 @@ http {
proxy_read_timeout 90s; proxy_read_timeout 90s;
} }
# Transformaciones de bundle (nodo nacional, via bus-gateway/bundleSigner): # Resto de /fhir/* va a hapi-fhir
# /fhir/Bundle/:id/$ddcc, $dvc, $icvp2, $meow location /fhir/ {
location ~ ^/fhir/Bundle/[^/]+/\$(ddcc|dvc|icvp|icvp2|meow)$ { proxy_pass http://hapi_fhir;
proxy_pass http://bus_gateway;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -107,9 +66,8 @@ http {
proxy_read_timeout 90s; proxy_read_timeout 90s;
} }
# Resto de /fhir/* va a hapi-fhir location /gdhcn/ {
location /fhir/ { proxy_pass http://gdhcn-validator-service:8080/;
proxy_pass http://hapi_fhir;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@ -51,46 +51,6 @@ http {
proxy_read_timeout 90s; proxy_read_timeout 90s;
} }
# ITI-65: Provide Document Bundle — MeOW (Medication Overview) transaction Bundle
location /fhir/MeOWTransaction {
proxy_pass http://bus_gateway;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90s;
}
# ITI-65: Provide Document Bundle — MeOW (Medication Overview) document Bundle
location /fhir/MeOWDocument {
proxy_pass http://bus_gateway;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90s;
}
# ITI-65: Provide Document Bundle — IT (Consultation Note) transaction Bundle
location /fhir/ITTransaction {
proxy_pass http://bus_gateway;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90s;
}
# ITI-65: Provide Document Bundle — IT (Consultation Note) document Bundle
location /fhir/ITDocument {
proxy_pass http://bus_gateway;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90s;
}
# ITI-67: Find Document References # ITI-67: Find Document References
location /fhir/DocumentReference { location /fhir/DocumentReference {
proxy_pass http://bus_gateway; proxy_pass http://bus_gateway;
@ -112,10 +72,9 @@ http {
proxy_read_timeout 90s; proxy_read_timeout 90s;
} }
# Transformaciones de bundle (nodo nacional, via bus-gateway/bundleSigner): # Resto de /fhir/* va a hapi-fhir
# /fhir/Bundle/:id/$ddcc, $dvc, $icvp2, $meow location /fhir/ {
location ~ ^/fhir/Bundle/[^/]+/\$(ddcc|dvc|icvp|icvp2|meow)$ { proxy_pass http://hapi_fhir;
proxy_pass http://bus_gateway;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -123,9 +82,8 @@ http {
proxy_read_timeout 90s; proxy_read_timeout 90s;
} }
# Resto de /fhir/* va a hapi-fhir location /gdhcn/ {
location /fhir/ { proxy_pass http://gdhcn-validator-service:8080/;
proxy_pass http://hapi_fhir;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

File diff suppressed because one or more lines are too long

View File

@ -1,121 +0,0 @@
{
"resourceType": "Bundle",
"type": "document",
"timestamp": "2026-04-23T07:50:00-03:00",
"entry": [
{
"fullUrl": "urn:uuid:5ca8da35-efb1-498a-93bc-ed34524a360f",
"resource": {
"resourceType": "Patient",
"identifier": [
{
"system": "urn:oid:2.16.858.1.858.1.1.1",
"value": "123456789"
}
],
"active": true,
"name": [
{
"use": "official",
"family": "Pérez",
"given": [
"Juan"
]
}
]
}
},
{
"fullUrl": "urn:uuid:4d58cb4f-1f8b-4f94-9877-bb5a0de47955",
"resource": {
"resourceType": "Composition",
"status": "final",
"type": {
"coding": [
{
"system": "http://loinc.org",
"code": "11488-4",
"display": "Consultation note"
}
]
},
"subject": {
"reference": "urn:uuid:5ca8da35-efb1-498a-93bc-ed34524a360f"
},
"date": "2026-04-23T07:50:00-03:00",
"author": [
{
"display": "Dra. Especialista - Nodo Panamá"
}
],
"title": "Informe de Contra-referencia Dermatológica",
"section": [
{
"title": "Motivo de Interconsulta",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "42349-1",
"display": "Reason for referral"
}
]
},
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>Se solicita segunda opinión para evaluación de lesión pigmentada en región escapular, con el fin de descartar malignidad tras hallazgos clínicos iniciales en Uruguay.</p></div>"
}
},
{
"title": "Revisión de Evidencia",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "30954-2",
"display": "Relevant diagnostic tests"
}
]
},
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>Se re-evaluaron los resultados del IPS (Bundle/2313), específicamente el DiagnosticReport/biopsia-001 y las imágenes dermatoscópicas adjuntas.</p></div>"
}
},
{
"title": "Juicio Clínico",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "51848-0",
"display": "Assessment note"
}
]
},
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>Tras el análisis de la evidencia, se concluye que la morfología de la lesión es compatible con un Nevus Displásico. No se identifican criterios histológicos de melanoma.</p></div>"
}
},
{
"title": "Recomendaciones",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "18776-5",
"display": "Plan of care"
}
]
},
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><ul><li>Control dermatoscópico cada 6 meses.</li><li>Uso de fotoprotección (FPS 50+).</li><li>No se requiere intervención quirúrgica inmediata.</li></ul></div>"
}
}
]
}
}
]
}

View File

@ -1,177 +0,0 @@
{
"resourceType": "Bundle",
"id": "medication-overview-example",
"meta": {
"profile": [
"http://profiles.ihe.net/PHARM/MEOW/StructureDefinition/MedicationOverview"
]
},
"identifier": {
"system": "urn:ietf:rfc:3986",
"value": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a71"
},
"type": "document",
"timestamp": "2025-06-17T18:36:18+00:00",
"entry": [
{
"fullUrl": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a72",
"resource": {
"resourceType": "Composition",
"id": "composition-example",
"meta": {
"profile": [
"http://profiles.ihe.net/PHARM/MEOW/StructureDefinition/MedicationOverviewComposition"
]
},
"status": "final",
"type": {
"coding": [
{
"system": "http://loinc.org",
"code": "56445-0",
"display": "Medication summary"
}
]
},
"subject": {
"reference": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a73"
},
"date": "2025-06-17T18:36:18+00:00",
"author": [
{
"reference": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a73"
}
],
"title": "Medication Overview",
"section": [
{
"title": "Medicamentos",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "55112-7",
"display": "Document summary"
}
]
},
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>Resumen de medicamentos</p></div>"
},
"entry": [
{
"reference": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a74"
},
{
"reference": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a75"
}
]
}
]
}
},
{
"fullUrl": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a73",
"resource": {
"resourceType": "Patient",
"id": "patient-example",
"identifier": [
{
"system": "http://hospital.example.org",
"value": "12345"
}
],
"name": [
{
"family": "Doe",
"given": [
"John"
]
}
],
"gender": "male",
"birthDate": "1970-01-01"
}
},
{
"fullUrl": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a74",
"resource": {
"resourceType": "MedicationStatement",
"id": "medication-statement-1",
"status": "active",
"medicationCodeableConcept": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "1222004",
"display": "Metronidazole-containing product"
}
],
"text": "Metronidazole-containing product"
},
"subject": {
"reference": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a73"
},
"effectiveDateTime": "2025-01-01",
"effectivePeriod": {
"start": "2025-01-01",
"end": "2025-12-31"
},
"dateAsserted": "2025-06-17T18:36:18+00:00",
"dosage": [
{
"text": "500 mg daily",
"route": {
"text": "Oral"
}
}
],
"reasonCode": [
{
"text": "Infection"
}
]
}
},
{
"fullUrl": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a75",
"resource": {
"resourceType": "MedicationStatement",
"id": "medication-statement-2",
"status": "active",
"medicationCodeableConcept": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "796001",
"display": "Digoxin-containing product"
}
],
"text": "Digoxin-containing product"
},
"subject": {
"reference": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a73"
},
"effectiveDateTime": "2025-01-01",
"effectivePeriod": {
"start": "2025-01-01"
},
"dateAsserted": "2025-06-17T18:36:18+00:00",
"dosage": [
{
"text": "0.125 mg daily",
"route": {
"text": "Oral"
}
}
],
"reasonCode": [
{
"text": "Heart failure"
}
]
}
}
]
}

View File

@ -1,188 +0,0 @@
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"fullUrl": "urn:uuid:5ca8da35-efb1-498a-93bc-ed34524a360f",
"resource": {
"resourceType": "Patient",
"identifier": [
{
"system": "urn:oid:2.16.858.1.858.1.1.1",
"value": "123456789"
}
],
"active": true,
"name": [
{
"use": "official",
"family": "Pérez",
"given": [
"Juan"
]
}
]
},
"request": {
"method": "POST",
"url": "Patient"
}
},
{
"fullUrl": "urn:uuid:15f62e6b-f4d9-43d8-91f5-ee963a34339c",
"resource": {
"resourceType": "DocumentReference",
"status": "current",
"docStatus": "final",
"type": {
"coding": [
{
"system": "http://loinc.org",
"code": "57133-1",
"display": "Referral note"
}
],
"text": "Informe de Contra-referencia Dermatológica"
},
"subject": {
"reference": "urn:uuid:5ca8da35-efb1-498a-93bc-ed34524a360f"
},
"date": "2026-04-23T07:50:00-03:00",
"author": [
{
"display": "Dra. Especialista - Nodo Panamá"
}
],
"description": "Respuesta a interconsulta - Caso Dermatología",
"content": [
{
"attachment": {
"contentType": "application/fhir+json",
"url": "urn:uuid:03e4a890-b152-481a-9ca5-685e03550a08"
}
}
],
"context": {
"related": [
{
"identifier": {
"system": "https://salud.gub.uy/interconsultas",
"value": "UY-PA-2026-998877"
}
}
]
}
},
"request": {
"method": "POST",
"url": "DocumentReference"
}
},
{
"fullUrl": "urn:uuid:03e4a890-b152-481a-9ca5-685e03550a08",
"resource": {
"resourceType": "Bundle",
"type": "document",
"timestamp": "2026-04-23T07:50:00-03:00",
"entry": [
{
"fullUrl": "urn:uuid:4d58cb4f-1f8b-4f94-9877-bb5a0de47955",
"resource": {
"resourceType": "Composition",
"status": "final",
"type": {
"coding": [
{
"system": "http://loinc.org",
"code": "11488-4",
"display": "Consultation note"
}
]
},
"subject": {
"reference": "urn:uuid:5ca8da35-efb1-498a-93bc-ed34524a360f"
},
"date": "2026-04-23T07:50:00-03:00",
"author": [
{
"display": "Dra. Especialista - Nodo Panamá"
}
],
"title": "Informe de Contra-referencia Dermatológica",
"section": [
{
"title": "Motivo de Interconsulta",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "42349-1",
"display": "Reason for referral"
}
]
},
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>Se solicita segunda opinión para evaluación de lesión pigmentada en región escapular, con el fin de descartar malignidad tras hallazgos clínicos iniciales en Uruguay.</p></div>"
}
},
{
"title": "Revisión de Evidencia",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "30954-2",
"display": "Relevant diagnostic tests"
}
]
},
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>Se re-evaluaron los resultados del IPS (Bundle/2313), específicamente el DiagnosticReport/biopsia-001 y las imágenes dermatoscópicas adjuntas.</p></div>"
}
},
{
"title": "Juicio Clínico",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "51848-0",
"display": "Assessment note"
}
]
},
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>Tras el análisis de la evidencia, se concluye que la morfología de la lesión es compatible con un Nevus Displásico. No se identifican criterios histológicos de melanoma.</p></div>"
}
},
{
"title": "Recomendaciones",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "18776-5",
"display": "Plan of care"
}
]
},
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><ul><li>Control dermatoscópico cada 6 meses.</li><li>Uso de fotoprotección (FPS 50+).</li><li>No se requiere intervención quirúrgica inmediata.</li></ul></div>"
}
}
]
}
}
]
},
"request": {
"method": "POST",
"url": "Bundle"
}
}
]
}

View File

@ -1,258 +0,0 @@
{
"resourceType": "Bundle",
"id": "iti65-transaction-medication",
"type": "transaction",
"entry": [
{
"fullUrl": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a73",
"resource": {
"resourceType": "Patient",
"id": "patient-example",
"identifier": [
{
"system": "http://hospital.example.org",
"value": "12345"
}
],
"name": [
{
"family": "Doe",
"given": [
"John"
]
}
],
"gender": "male",
"birthDate": "1970-01-01"
},
"request": {
"method": "POST",
"url": "Patient"
}
},
{
"fullUrl": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a80",
"resource": {
"resourceType": "DocumentReference",
"status": "current",
"docStatus": "final",
"type": {
"coding": [
{
"system": "http://loinc.org",
"code": "56445-0",
"display": "Medication summary"
}
],
"text": "Medication Overview"
},
"subject": {
"reference": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a73"
},
"date": "2025-06-17T18:36:18+00:00",
"author": [
{
"display": "Dra. Especialista"
}
],
"description": "Medication Overview - Patient Medication Summary",
"content": [
{
"attachment": {
"contentType": "application/fhir+json",
"url": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a71"
}
}
]
},
"request": {
"method": "POST",
"url": "DocumentReference"
}
},
{
"fullUrl": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a71",
"resource": {
"resourceType": "Bundle",
"id": "medication-overview-example",
"meta": {
"profile": [
"http://profiles.ihe.net/PHARM/MEOW/StructureDefinition/MedicationOverview"
]
},
"identifier": {
"system": "urn:ietf:rfc:3986",
"value": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a71"
},
"type": "document",
"timestamp": "2025-06-17T18:36:18+00:00",
"entry": [
{
"fullUrl": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a72",
"resource": {
"resourceType": "Composition",
"id": "composition-example",
"meta": {
"profile": [
"http://profiles.ihe.net/PHARM/MEOW/StructureDefinition/MedicationOverviewComposition"
]
},
"status": "final",
"type": {
"coding": [
{
"system": "http://loinc.org",
"code": "56445-0",
"display": "Medication summary"
}
]
},
"subject": {
"reference": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a73"
},
"date": "2025-06-17T18:36:18+00:00",
"author": [
{
"reference": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a73"
}
],
"title": "Medication Overview",
"section": [
{
"title": "Medicamentos",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "55112-7",
"display": "Document summary"
}
]
},
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>Resumen de medicamentos</p></div>"
},
"entry": [
{
"reference": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a74"
},
{
"reference": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a75"
}
]
}
]
}
},
{
"fullUrl": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a73",
"resource": {
"resourceType": "Patient",
"id": "patient-example",
"identifier": [
{
"system": "http://hospital.example.org",
"value": "12345"
}
],
"name": [
{
"family": "Doe",
"given": [
"John"
]
}
],
"gender": "male",
"birthDate": "1970-01-01"
}
},
{
"fullUrl": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a74",
"resource": {
"resourceType": "MedicationStatement",
"id": "medication-statement-1",
"status": "active",
"medicationCodeableConcept": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "1222004",
"display": "Metronidazole-containing product"
}
],
"text": "Metronidazole-containing product"
},
"subject": {
"reference": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a73"
},
"effectiveDateTime": "2025-01-01",
"effectivePeriod": {
"start": "2025-01-01",
"end": "2025-12-31"
},
"dateAsserted": "2025-06-17T18:36:18+00:00",
"dosage": [
{
"text": "500 mg daily",
"route": {
"text": "Oral"
}
}
],
"reasonCode": [
{
"text": "Infection"
}
]
}
},
{
"fullUrl": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a75",
"resource": {
"resourceType": "MedicationStatement",
"id": "medication-statement-2",
"status": "active",
"medicationCodeableConcept": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "796001",
"display": "Digoxin-containing product"
}
],
"text": "Digoxin-containing product"
},
"subject": {
"reference": "urn:uuid:68c15694-52f1-4171-bc01-e63d666d2a73"
},
"effectiveDateTime": "2025-01-01",
"effectivePeriod": {
"start": "2025-01-01"
},
"dateAsserted": "2025-06-17T18:36:18+00:00",
"dosage": [
{
"text": "0.125 mg daily",
"route": {
"text": "Oral"
}
}
],
"reasonCode": [
{
"text": "Heart failure"
}
]
}
}
]
},
"request": {
"method": "POST",
"url": "Bundle"
}
}
]
}