Salud · Localización por país + editor de plantillas
Plan de implementación. Escrito 2026-08-20. Continúa PROGRAMAMED-ANALISIS.md
(que definió F1–F6) y no lo sustituye: aquí se abren dos ejes nuevos que lo
cruzan.
Los dos pedidos
- Localización agregable. Hoy el módulo de salud es genérico y las exigencias de Ecuador (MSP, ACESS, LOPDP) están fuera. Hay que meterlas sin clavarlas: mañana entra Perú o Colombia y debe ser añadir un paquete, no editar el motor.
- Editor de plantillas de verdad. Hoy una plantilla es texto plano con tokens. El usuario quiere colores, logo, tipografía, tablas — y lo mismo para las recetas, cuyo diseño está hoy escrito a mano en un componente.
Son independientes: se pueden hacer en cualquier orden. Se planifican juntos porque comparten una pieza — el documento imprimible — y porque el paquete de Ecuador necesita plantillas rígidas, que solo tienen sentido cuando existe un editor flexible del que protegerse.
Alcance decidido (2026-08-20): hasta L6 completo. No es "un buen sistema clínico" sino "un sistema que pasa una inspección de la ACESS". Eso arrastra tres consecuencias que el resto del documento desarrolla: aparece una fase P0 de plataforma (autenticación, 2FA, retención) que antes era opcional; L5 y L6 dejan de ser un epígrafe y se detallan en §13.2 y §13.3. La firma electrónica queda fuera de alcance (§19, decidido 2026-08-21): el sistema genera el documento y el médico lo firma aparte con su certificado acreditado.
Parte IV (§18–§20) es de lectura obligatoria antes de empezar L1. Buena parte de lo que aquí se llama "módulo de salud" no es de salud: el motor de plantillas, las series numeradas y la firma son piezas de plataforma. Dónde se construyen se decide antes de construirlas, no después.
Parte I — Localización como agregable
1. Vocabulario: idioma ≠ jurisdicción
En el proyecto ya se llama "localización" a dos cosas distintas y conviene separarlas antes de escribir una línea:
| Qué varía | Ya resuelto por | Ejemplo | |
|---|---|---|---|
| Idioma (i18n) | cómo se dice | i18next, src/locales/, fallbackLng: 'es' | "Prescription" / "Receta" |
| Jurisdicción (l10n) | qué es legal y obligatorio | nada todavía | la receta de psicotrópicos necesita folio de la ACESS |
Este plan es de jurisdicción. Un tenant español y uno ecuatoriano ven la
interfaz en el mismo español y aun así rellenan formularios distintos. El eje es
el país de la organización (tenants.country, que ya existe), no el idioma
del navegador.
2. Estado actual
Lo que ya es localizable y sirve de precedente
frontend/src/lib/identity-documents.ts— catálogo de documentos de identidad por país, con_FALLBACKgenérico y una lista corta a propósito ("crece cuando hay un cliente real en ese país"). Este es el patrón: registro en código, clave por país, degradación honesta. Todo lo demás se construye a su imagen.tenants.countryya se edita en la configuración del workspace (actions/workspace.ts) y valida contraSUPPORTED_COUNTRIES.- La plataforma de módulos (manifiestos +
tenant_modules+ siembra al activar) es exactamente el mecanismo Odool10n_ec. Está construida y funciona.
Lo que está clavado y bloquea Ecuador (referencias al SRS adjunto)
| SRS | Exigencia | Hoy |
|---|---|---|
| §2.1 | prohibido UPDATE destructivo sobre lo firmado; corrección por adenda | health_save_encounter hace UPDATE public.clinical_encounters en sitio cuando recibe p_encounter_id |
| §2.3 | versión encadenada con previous_version_id | solo lo tienen las plantillas, no los registros clínicos |
| §3 | firma electrónica .p12, PAdES, 2FA | no hay generación de PDF en servidor (se imprime desde el navegador) ni 2FA |
| §4 | CIE-10 obligatorio, DCI obligatoria | diagnóstico es texto libre; health_medications es catálogo auto-alimentado por texto |
| §5 | formularios MSP 001/002/005/024 | no existe motor de formularios estructurados |
| §6.2 | folios ACESS para psicotrópicos | no hay series numeradas |
| §7 | días de reposo en letras y cifras, QR antifraude | field.reposo_* son texto libre; el QR es F3 pendiente |
| §8 | disparador EPI-1 por CIE-10 | no existe |
| §9.3 | recepción / médico / enfermería con accesos distintos | solo hay health.records.read y health.records.manage |
Dos observaciones honestas antes de seguir:
- §9.3 no es un problema de Ecuador. Que recepción no lea el Form. 002 es buen diseño en cualquier país. El permiso granular va al módulo de salud, no al paquete EC.
- §3 y §9.1 (2FA, cifrado en reposo) no son del módulo de salud. Son de
plataforma y chocan con un hueco conocido: el backend NestJS no autentica
salvo en
scheduling. Como el alcance llega hasta L6, entran igualmente: son la fase P0 (§13.1). No se pueden dejar fuera y llamar a esto "cumple".
3. Arquitectura: el paquete de localización
3.1 Tres capas, una regla
CONTRATO shared/src/modules/health-l10n.ts solo tipos: qué ranuras existen
────────────────────────────────────────────────────────────────────
MOTOR health_* (SQL) + sections/health/… genérico, no sabe de países
────────────────────────────────────────────────────────────────────
PAQUETE lib/health/l10n/ec.ts + semilla SQL datos + validadores de un país
Regla invariante (la misma que ya tiene la plataforma de módulos): el motor
nunca menciona un país. Ni una tabla ec_*, ni un if (country === 'EC')
fuera de un paquete. Si al implementar hace falta un if de país en el motor, es
que falta una ranura.
3.2 Las ranuras
Un HealthLocalePack es un objeto plano. Cada ranura tiene un valor genérico
que es lo que el sistema hace hoy, así que el paquete genérico no cambia nada de
lo existente.
| Ranura | Qué decide | Genérico (hoy) | EC |
|---|---|---|---|
identity | tipos de documento + validador | documento / pasaporte, sin validar | cédula (módulo 10), RUC (13 díg.), pasaporte |
professional | campos obligatorios del prescriptor | registro libre | registro ACESS y SENESCYT |
diagnosisCoding | de dónde sale un diagnóstico | texto libre | ICD10, obligatorio, sin texto libre |
drugNaming | cómo se nombra un fármaco | libre | DCI obligatoria, aviso si está fuera del CNMB |
immutability | qué pasa al corregir | update | amend_only una vez firmado |
documentTemplates | plantillas de sistema | las 6 actuales | variantes EC + certificado IESS rígido |
clinicalForms | formularios estructurados | ninguno | MSP 001, 002, 005, 024 |
numbering | series controladas | ninguna | block ACESS por médico para psicotrópicos |
verification | QR público de validez | opcional | obligatorio en certificados |
signature | cómo se firma | imagen escaneada | firma fuera del sistema con certificado acreditado; el documento reserva su sitio y admite el PDF firmado de vuelta (§19) |
epidemiology | avisos disparados por diagnóstico | ninguno | EPI-1 sobre lista de CIE-10 notificables |
retention | retención y anonimización | por defecto | LOPDP: reportes sin nombre ni cédula |
formatting | fecha, número en letras | ISO | dd/mm/aaaa, tres (3) días |
formatting.numberToWords es del idioma, no del país — pero la obligación
de escribirlo es del país. El paquete declara la obligación y delega en un
helper por idioma; así Perú reutiliza el mismo helper sin copiarlo.
3.3 Resolución: de qué país es este tenant
tenants.country ──┐
├─→ resolveHealthLocale(country, override) ──→ HealthLocalePack
override del ─┘ si no hay paquete: GENERIC (nunca null)
tenant (opcional)
- Fuente primaria:
tenants.country. - Override por tenant en la configuración de salud: una clínica privada en
Ecuador que no factura al IESS puede querer el paquete genérico. Se guarda en
tenant_modules.configdel módulohealth(ya esjsonby ya hay caché de 60 s enmodules.service.ts), no en una tabla nueva. - Nunca devuelve nulo. Un país sin paquete cae a
GENERIC, igual queidentityDocumentsFor().
resolveHealthLocale vive dos veces, una por lado (frontend y backend), sobre
tipos compartidos — no hay runtime compartido: @xenpia/shared es solo tipos (la
nota al pie de shared/src/modules/types.ts es explícita). Es el mismo
compromiso que ya hace DOCUMENT_TOKENS: el tipo en shared, los valores en
frontend/src/sections/health/documents/tokens.ts.
3.4 El manifiesto health_l10n_ec
El paquete se activa como módulo, con el mecanismo que ya existe:
export const healthL10nEcManifest: ModuleManifest = {
code: 'health_l10n_ec',
name: 'Salud · Ecuador',
description: 'Formularios MSP, CIE-10, folios ACESS y reglas de la HCU.',
icon: 'solar:document-medicine-bold',
category: 'vertical',
version: '0.1.0',
dependsOn: ['health'],
permissions: ['health.forms.manage', 'health.series.manage'],
menus: [
{ code: 'health.forms', path: '/dashboard/health/forms', section: 'health', sort: 40, sectionSort: 50, permission: 'health.clinical.read' },
{ code: 'health.series', path: '/dashboard/health/series', section: 'health', sort: 50, sectionSort: 50, permission: 'health.series.manage' },
],
extensionPoints: ['contact.detail.tabs'],
sort: 31,
};
Qué gana esto frente a un simple if (country === 'EC'):
- La activación siembra permisos, menús y datos del país en una operación
idempotente, que es lo que ya hace
modules.service.ts. - La pantalla de Aplicaciones lo muestra y lo explica: el cliente ve que tiene el paquete Ecuador activo, y puede desactivarlo.
dependsOn: ['health']bloquea la incoherencia de activar el paquete sin la vertical, gratis.
Trampa conocida: los menús solo se siembran al activar. En el tenant demo,
que ya tiene health activo, cualquier menú nuevo del módulo de salud no
aparece hasta re-activar el módulo. Cada fase que añada un menú debe decir
"re-activar el módulo" en su comprobación.
3.5 Reglas de base de datos
- Ninguna tabla con prefijo de país. El discriminador es una columna
locale_code text(NULL = genérico), nunca el nombre de la tabla. - Las tablas de catálogo no son PHI (CIE-10, CNMB, definiciones de
formulario): pueden llevar política de
SELECTparaauthenticated. Las de instancias (formularios rellenados, folios consumidos) sí son PHI y siguen el patrón obligatorio del módulo: RLS +REVOKE ALL, sin política de SELECT, acceso solo por funcioneshealth_*SECURITY DEFINERque compruebanuser_has_permissiony auditan. - Nunca llamar a
health_auditen un camino que pueda correr con service-role.audit_logs.user_idesNOT NULLy revienta la función entera (ya pasó conhealth_resolve_delivery). - Las migraciones se aplican a mano en el editor SQL. Cada archivo termina con su bloque de comprobación comentado, como los actuales.
Tablas nuevas previstas (todas neutrales):
health_code_systems (code, name, locale_code, version) -- ICD10, CNMB
health_code_entries (system_code, code, display, parent, flags) -- flags: {notifiable:true}
health_form_definitions (locale_code, code, version, title, schema jsonb, is_active)
health_form_instances (tenant, contact, encounter, definition_version, data jsonb,
status draft|signed, previous_version_id, signed_by, signed_at)
health_amendments (target_kind, target_id, tenant, body, reason, created_by)
health_number_series (tenant, locale_code, kind, owner_profile_id, prefix,
range_from, range_to, next_value, authority_ref, is_active)
health_number_assignments (series_id, value, target_kind, target_id, assigned_at)
Y columnas añadidas a lo existente:
health_document_templates + locale_code text -- NULL = genérica
health_document_template_versions + doc jsonb, schema_version int -- Parte II
health_documents + layout_snapshot jsonb -- Parte II
4. Añadir un país nuevo — la prueba de que es agregable
Meter Perú, cuando llegue el cliente, debe ser exactamente esto:
shared/src/modules/health-l10n.ts— nada. El contrato ya está.frontend/src/lib/health/l10n/pe.ts(+ gemelo en backend) — el objetoHealthLocalePackde Perú. Si necesita una ranura que no existe, ahí se descubre; se añade al contrato con valor genérico por defecto, para no tocar EC.supabase/migrations/2026XXXX_l10n_pe_seed.sql— plantillas de sistema conlocale_code = 'PE', definiciones de formulario, catálogos.backend/src/modules/manifests.ts— un manifiesto más enALL_MODULE_MANIFESTS+ su fila enpublic.app_modules.src/locales/**— las cadenas nuevas en es y en.
Cero cambios en el motor. Si algún paso obliga a tocar un health_*
genérico, el diseño falló y hay que abrir la ranura que faltaba en vez de
parchear.
5. El paquete Ecuador: SRS → ranura → fase
| SRS | Ranura | Fase |
|---|---|---|
| §2.1 no borrado / adendas | immutability | L4 |
§2.2 audit trail (VIEW, PRINT, EXPORT, IP, user-agent) | motor (no del país) | L4 |
| §2.3 versionado encadenado | immutability | L4 |
§3 firma .p12 / PAdES | signature | fuera de alcance (§19): la aplica el médico con su herramienta |
| §3 2FA | plataforma (no del país) | P0 |
| §4 CIE-10 | diagnosisCoding | L3 |
| §4 DCI / CNMB | drugNaming | L3 |
| §5.1 MSP 001 admisión | clinicalForms + identity | L5 |
| §5.2 MSP 002 consulta externa | clinicalForms | L5 |
| §5.3 MSP 005 evolución SOAP | clinicalForms | L5 |
| §5.4 MSP 024 consentimiento | clinicalForms + signature | L5 |
| §6.1 receta común (membrete, QR) | documentTemplates | L1 + L2 |
| §6.2 folio ACESS psicotrópicos | numbering | L6 |
| §7 certificados (letras+cifras, QR) | formatting, verification | L2 + L6 |
| §8 EPI-1 | epidemiology | L6 |
| §9.2 anonimización de reportes | retention | P0 (no hay reportería aún: nace anonimizada, que es lo barato) |
| §9.3 RBAC recepción/médico/enfermería | motor (no del país) | L4 |
Parte II — Editor de plantillas
6. Por qué hoy es texto plano, y qué sí se puede cambiar
La decisión actual está documentada en la cabecera de
20260826_health_documents.sql y era correcta para lo que había: el cuerpo es
texto con tokens, no HTML, porque el proyecto no tiene sanitizador y volcar
HTML de usuario en una vista que muestra PHI es un XSS almacenado.
El error sería concluir "entonces no se puede enriquecer". Lo que no se puede es guardar una cadena de marcado que alguien interprete. Sí se puede guardar un árbol de datos cuyos nodos el renderizador conoce uno a uno.
La garantía de seguridad se aplica al renderizar, no al guardar. El renderizador tiene una tabla
tipo de nodo → componente. Un nodo que no está en la tabla se descarta. Un color que no case^#[0-9a-f]{6}$se ignora. Una tipografía fuera de la lista blanca cae a la del tema. Así, aunque alguien llame a la RPC de guardado a mano y meta basura en eljsonb, no hay nada que interpretar. En ningún punto del sistema aparecedangerouslySetInnerHTMLnigenerateHTML. Esa línea es la invariante de toda la Parte II.
7. Modelo de documento por bloques (v2)
health_document_template_versions gana doc jsonb y schema_version int.
body se queda: las versiones v1 existentes se siguen renderizando con
DocumentBody tal cual, y un documento emitido en 2026 se reimprime idéntico en
2030. Nada se migra a la fuerza.
Nodos de bloque
| Nodo | Para qué |
|---|---|
paragraph, heading | texto, con align y lineHeight |
bulletList / orderedList | listas |
table | la tabla que pide el usuario: filas, columnas, cabecera, anchos, bordes, relleno |
image | logo o imagen, por id de activo, nunca por URL libre |
divider, spacer, pageBreak | maquetación |
signatureBlock | firma: línea, nombre, credenciales, imagen de firma |
qr | QR de verificación (contenido calculado, no tecleado) |
prescriptionItems | tabla de medicamentos, alimentada por los datos estructurados de la receta |
columns | dos o tres columnas (membretes tipo "paciente / fecha") |
Marcas de texto: bold, italic, underline, color, fontSize,
fontFamily, highlight. Y un nodo inline atómico token, que es la mejora
más útil del día a día: hoy {{patient.nombre}} es texto y se rompe borrando una
llave; como átomo se inserta y se borra entero, y se pinta como chip.
Límites duros (se comprueban al renderizar, no solo al guardar): profundidad
máxima de anidamiento, número máximo de nodos, número máximo de celdas de tabla.
Una plantilla no es un lienzo infinito y un jsonb de 40 MB no debe poder tumbar
la vista de impresión de una receta.
8. Tema, membrete y activos
Colores, tipografía y logo no se meten nodo a nodo. Van en una capa declarativa que se hereda, porque el pedido real no es "quiero pintar este párrafo de azul" sino "quiero que mis documentos se vean míos".
health_brand_profiles
theme: { fontFamily, baseSize, lineHeight, colors: { text, muted, accent, rule } }
page: { size: A4|Letter, orientation, margins }
header: [ bloques ] ← logo, nombre, dirección, RUC, teléfonos
footer: [ bloques ] ← pie legal, web, paginación
watermark: { text | assetId, opacity }
- Un perfil por tenant, con override opcional por profesional
(
owner_profile_id): el médico que tiene su propio membrete y su firma escaneada. Es la misma jerarquíaprofessional → tenant → sistemaque ya usan las plantillas; no se inventa un concepto nuevo. page_configde la versión de plantilla ya existe y hoy está a'{}': ahí van los ajustes propios de esa plantilla, que pisan al perfil del tenant.- Tipografías: lista blanca. Las cinco ya instaladas por
@fontsource(DM Sans, Inter, Nunito Sans, Public Sans, Barlow) más las que se añadan a propósito. Nada defont-familylibre: una fuente que no está en el bundle no se imprime igual en el equipo del médico que en el del auditor. - Activos (
health_template_assets): logo, firma escaneada, sello. Bucket privado de Supabase, filas inmutables (subir de nuevo = fila nueva, nunca sobrescribir), tipo MIME y tamaño validados,kindexplícito. La firma escaneada de un médico es un dato sensible: no va a un bucket público.
9. El editor
Recomendación: TipTap (ProseMirror) con esquema restringido. Ya estaba anticipado en la nota de la migración F2 ("si algún día se quiere WYSIWYG con chips atómicos, añadir TipTap"). Encaja porque:
- Su
getJSON()es el árbol de bloques descrito arriba; no hay conversión ni parser de HTML de por medio. - El esquema es una lista blanca por construcción: lo que no se registra como extensión, no existe en el documento.
- Los tokens como nodos atómicos son un caso de manual (
atom: true).
Extensiones: StarterKit (recortado), Table, TextStyle + Color, FontFamily,
TextAlign, Highlight, más tres propias — Token, AssetImage, PageBreak — y
dos de bloque de dominio — SignatureBlock, PrescriptionItems.
Validación al guardar con zod, que ya es dependencia del frontend, dentro del
server action saveTemplateVersion ('use server', corre en servidor). Es
defensa en profundidad, no la garantía: la garantía sigue siendo el
renderizador.
Alternativa si TipTap se considera demasiado peso: un editor de bloques propio sobre MUI (cada bloque un formulario, sin edición enriquecida en línea). Más barato de auditar y notablemente peor para redactar prosa médica. Mi recomendación es TipTap; la alternativa queda escrita por si la respuesta es no.
Lo que se conserva de la pantalla actual: el panel lateral de tokens agrupado
por origen, la vista previa en vivo con SAMPLE_CONTEXT, el clonado de plantilla
de sistema y el guardado como versión nueva. Todo eso ya funciona y no se toca;
cambia la caja donde se escribe.
10. Plantillas rígidas — donde se cruzan las dos partes
El SRS §7 es tajante: "el sistema debe proveer una plantilla rígida", porque un certificado alterado produce glosas del IESS. Un editor libre y una plantilla rígida no se contradicen si la rigidez la declara el paquete del país:
{
code: 'certificado_medico_iess',
localeCode: 'EC',
layout: 'flexible', // membrete, colores y tipografía SÍ se tocan
requiredNodes: [
{ token: 'field.cie10' }, // sin CIE-10 no es homologable en el IESS
{ type: 'signatureBlock' },
{ type: 'qr' },
],
lockedNodes: ['legal_notice'],
}
Al guardar una versión se comprueba que los nodos obligatorios siguen ahí; si no,
error con código (missing_required_node) que la interfaz traduce, como todo el
resto del proyecto. El usuario personaliza cuanto quiera alrededor de lo que
la ley fija.
11. Recetas por el mismo motor
Hoy prescription-print-view.tsx son 267 líneas de maquetación escrita a mano:
el médico no puede cambiarle ni el logo. Se convierte en una plantilla más, de
tipo receta, cuyo cuerpo contiene un nodo prescriptionItems que recibe los
ítems estructurados que ya guarda health_prescription_items (dosis, cantidad,
frecuencia, duración, instrucciones).
Con eso, "colores, logo y tipografía" cae también sobre las recetas sin escribir
un segundo sistema — que es literalmente lo que pide el encargo. Y el paquete EC
puede sembrar la variante receta con DCI obligatoria y el bloque de folio.
12. Congelado y compatibilidad
Un documento emitido hoy debe reimprimirse idéntico dentro de diez años. Hoy
se congela body_snapshot + context_snapshot; con tema, logo y tipografía por
medio eso ya no basta: cambiar el logo del tenant cambiaría certificados
antiguos.
Por eso health_documents gana layout_snapshot jsonb con el tema, el
page_config y los ids de activo resueltos en el momento de emitir. Como los
activos son filas inmutables, no hace falta empotrar la imagen en base64 en cada
documento: basta con no borrar nunca un activo referenciado (borrado lógico).
En la ruta pública /d/[token], que corre sin sesión con createAdminSupabase,
el activo se resuelve en el servidor a URL firmada de vida corta antes de
renderizar. Nunca se expone el bucket.
Compatibilidad: el renderizador despacha por schema_version (1 = texto con
DocumentBody, 2 = bloques). Nada se migra de golpe; una plantilla v1 se
convierte a v2 la primera vez que alguien la abre en el editor nuevo y guarda
— es decir, cuando ya iba a crear una versión nueva de todos modos.
Parte III — Fases, riesgos y decisiones
13. Fases
Nomenclatura L para no chocar con las F1–F6 de PROGRAMAMED-ANALISIS.md. F3
(folio + QR) y F5 (formularios MSP) quedan absorbidas por L6 y L5
respectivamente, ahora con un motor genérico debajo.
| Fase | Alcance | Cierra | Depende de | Tamaño |
|---|---|---|---|---|
| P0 | Autenticación real en todo el backend, 2FA para roles con PHI, retención y consentimiento LOPDP, copias verificadas | §3 (2FA), §9.1, §10 del marco legal | — | M |
| L0 | Contrato HealthLocalePack, registro + GENERIC + esqueleto EC, resolveHealthLocale, override en tenant_modules.config, manifiesto health_l10n_ec | La arquitectura. Sin cambio visible | — | S |
| L1 | Editor v2: doc jsonb, renderizador de bloques, TipTap, tema + membrete + activos, receta por plantilla. Se construye como módulo documents, no dentro de salud (§18) | El segundo pedido, entero | — | L |
| L2 | locale_code en plantillas, orden de resolución, nodos obligatorios y bloqueados, semilla de plantillas EC | Plantillas rígidas del §6/§7 | L0, L1 | S — ✅ hecho; migraciones 20260841 y 20260842 SIN APLICAR |
| L3 | health_code_systems/entries, selector de diagnóstico, DCI/CNMB en receta, credenciales del profesional | §4, §6.1 | L0 | M — ✅ motor hecho (20260843 y 20260844 sin aplicar). Los catálogos siguen SIN CARGAR: falta la fuente oficial (§16) |
| L4a | Inmutabilidad + adendas: draft/signed, firmar bloquea, fe de erratas, políticas por jurisdicción | §2.1 | L0 | ✅ hecho; 20260846 sin aplicar |
| L4b | Auditoría con IP, agente y acción de impresión | §2.2 | L4a | ✅ hecho; 20260847 sin aplicar |
| L4c | Permisos granulares (recepción / médico / enfermería) con compatibilidad | §9.3 | L4a | pendiente |
| L5 | Motor de formularios estructurados + definiciones MSP 001/002/005/024, consentimiento con captura de trazo u OTP | §5 | L0, L3, L4 | XL |
| L6 | Series y folios ACESS (→ sequences), QR de verificación pública, PDF en servidor, retorno del PDF firmado por el médico (§19 variante B), disparador EPI-1 | §6.2, §7, §8 | todas | M — la firma propia salió de alcance (§19) |
Estado — ✅ L0 hecho (2026-08-21). Contrato HealthLocalePack en
shared/src/modules/health-l10n.ts (13 ranuras), paquetes GENERIC y EC en
frontend/src/lib/health/l10n/, resolveHealthLocale, lectura del override en
actions/health-locale.ts, manifiesto healthL10nEcManifest y migración
20260835_health_l10n_ec_module.sql (✅ aplicada, verificada contra la base).
identity-documents.ts gana validadores de cédula y RUC ecuatorianos y pasa a
ser la fuente de la ranura identity. Sin cambio visible, como estaba previsto.
Certificado contra la base el 2026-08-21: migración aplicada, fila del catálogo
coincidente con el manifiesto, RLS comprobada (la clave anónima ve 0 filas),
cierre de dependencias health_l10n_ec → health → scheduling, tsc y eslint
limpios, y 45 comprobaciones de lógica en verde. Y una sorpresa útil: F1–F3
también están aplicadas y en uso (119 fichas, 104 encuentros, 42 entregas), lo
que cierra el riesgo nº1 de §14 y estrecha la ventana de §18 — ver la nota allí.
Estado — ✅ L1 completo y aplicado. Se entregó por trozos, para poder revisar cada uno:
| Alcance | Estado | |
|---|---|---|
| L1.1 | doc jsonb + schema_version, congelado del aspecto, doc_assets, doc_brand_profiles, bucket privado | ✅ hecho y aplicado |
| L1.2 | Renderizador de bloques: lista blanca, saneo, límites, aplanado a texto | ✅ hecho y verificado |
| L1.3 | Editor TipTap con esquema restringido y tokens atómicos | ✅ hecho |
| L1.4a | Editor montado en la pantalla de plantillas, RPC v2, migración v1→v2 al guardar | ✅ hecho y aplicado (20260837) |
| L1.4b | Marca: tema, página, logo y activos, con pestaña propia en la configuración de salud | ✅ hecho |
| L1.5a | El árbol de punta a punta: emisión, congelado del aspecto e impresión del médico | ✅ hecho y aplicado (20260838) |
| L1.5b | El enlace del paciente con activos, y la receta como una plantilla más | ✅ hecho y aplicado (20260839, 20260840) |
Validación de L0–L3 (2026-08-23). 130 comprobaciones contra la base real y
sobre la lógica pura, todas en verde, más tsc, eslint y next build. Salió
un fallo de verdad, del tipo que solo aparece mirando el conjunto: las
plantillas ecuatorianas declaraban bloques signatureBlock y qr que ninguna
vista de impresión rellenaba. El nodo estaba en el árbol, el renderizador
reservaba el hueco y nadie lo llenaba, así que el certificado salía sin firma. Se
corrigió rellenando la ranura de firma (signature-block.tsx) y se retiró la
exigencia de qr hasta L6, que es quien la hace real — el mismo criterio que ya
se había aplicado al CIE-10. Migración correctora: 20260845. Detalle en §21.
Corrección al montar el editor (2026-08-21). Al instalar TipTap se comprobó
que los nombres reales no coincidían con los que había supuesto §7, y el esquema
se plegó a los suyos: horizontalRule en vez de divider, textAlign en vez de
align, y —la importante— color, tamaño y tipografía no son marcas propias:
TipTap las guarda como atributos de una sola marca textStyle. Mantener nombres
propios habría obligado a convertir el árbol en cada guardado y en cada carga,
que es justo la fragilidad que se evitaba eligiendo TipTap. Se corrigió antes de
que existiera ningún documento v2, así que no hubo datos que migrar.
Decisión tomada al empezar L1: la extracción del módulo documents se parte.
Las piezas nuevas nacen con nombre neutral (doc_*, components/document/), y el
renombrado de las dos tablas heredadas queda como paso propio y aislado —
reescribir RPC que hoy sirven datos reales no debe mezclarse con una
funcionalidad nueva.
Tamaños relativos, no fechas: S ≈ días, M ≈ una a dos semanas, L ≈ tres a cuatro, XL ≈ más de un mes — para una persona que ya conoce el repositorio, con las migraciones aplicándose a mano. L5 y L6 son XL cada una y juntas pesan más que todo lo demás sumado; ahí está el grueso del compromiso de llegar hasta L6.
Orden recomendado: P0 → L0 → L1 → L2 → L3 → L4 → L5 → L6. L1 va pronto porque es el pedido explícito y no depende de nada. L4 es el más delicado (cambia el comportamiento de guardar una evolución, que ya está en uso en la demo) y por eso va después de que el editor esté estable.
Detalle de L4 — permisos granulares. Los actuales health.records.read y
health.records.manage no distinguen recepción de médico. Se añaden al
manifiesto de salud: health.admission.manage (Form. 001), health.clinical.read,
health.clinical.write, health.vitals.write, health.prescriptions.manage,
health.documents.emit, health.templates.manage. Para no romper los tenants
vivos, la migración concede los nuevos a quien ya tenga los viejos, y durante una
versión las funciones health_* aceptan cualquiera de los dos (OR). Los antiguos
se retiran cuando ya no queden llamantes.
13.1 P0 — prerrequisitos de plataforma
Antes eran "riesgos"; con L6 como meta son alcance. Ninguno es del módulo de salud, y por eso es fácil que se caigan de la lista hasta que un auditor pregunta.
| Punto | Qué hay que hacer | Nota |
|---|---|---|
| Autenticación del backend | Extender PermissionGuard a bots, contacts, channels, data-sources y a todo endpoint nuevo de salud (scheduling, kiosk, invoicing y modules ya lo llevan) | /api/webhooks/* y /api/bots/message son públicos a propósito (widget de webchat) y deben seguir siéndolo. CORS no protege nada: cualquier curl lo ignora |
| 2FA (§3) | Supabase Auth trae MFA TOTP con niveles aal1/aal2 en el JWT. Exigir aal2 para roles con acceso a PHI, en el guard del backend y en el middleware del frontend | Es la vía barata: no hay que construir 2FA, solo obligarlo. Cuidado con el alta: un médico que pierde el TOTP no puede quedar fuera de la historia clínica de sus pacientes → política de recuperación por administrador, auditada |
| Cifrado en reposo (§9.1) | El SRS pide TDE. Postgres no tiene TDE nativo y el cifrado por columna de Supabase (pgsodium/TCE) está en desuso | La respuesta defendible hoy es: cifrado de disco gestionado + control de acceso por RLS + auditoría, documentado. Si el auditor exige cifrado por columna, se acota a diagnóstico y se paga en consultabilidad. Verificar con Supabase antes de prometer nada |
| Consentimiento LOPDP | Registro de consentimiento explícito del paciente para el tratamiento de datos de salud: quién, cuándo, qué versión del texto, por qué medio | Tabla propia; se reutiliza la misma mecánica de no-repudio del MSP 024 (§13.2) |
| Retención y anonimización (§9.2) | Política de retención declarada + reportes que omitan nombre y documento | No hay reportería todavía, así que es barato hacerlo bien: nace anonimizada |
Ya no es bloqueante: era el prerrequisito para custodiar el .p12, y la firma salió de alcance (§19). Sigue siendo buena práctica para otros secretos | Vuelve a ser obligatorio solo si se retoma §19.z | |
| Copias y continuidad | Copias verificadas y un procedimiento de restauración probado | Una inspección lo pregunta, y "está en Supabase" no es una respuesta |
13.2 L5 en detalle — motor de formularios + serie MSP
Es la fase más grande. La tentación es escribir cuatro pantallas React (una por formulario) y acabar; sería un error: el MSP cambia sus formatos por resolución ministerial y quedaríamos atados a un despliegue de código por cada cambio.
a) La definición es un dato. health_form_definitions.schema es un jsonb
con secciones → campos → tipo, obligatoriedad, validación y dependencias
(mostrar si otro campo vale X). Versionada: publicar una corrección del MSP es
insertar la versión 2, no editar la 1 — misma disciplina que las plantillas.
b) Un renderizador genérico que recorre el esquema y pinta MUI, con su validador derivado del mismo esquema (zod). Un formulario nuevo = una fila nueva.
c) Instancias con estado. draft → signed. Al firmar, se congela; corregir
crea una instancia nueva con previous_version_id (el mecanismo ya lo trae L4) y
la interfaz muestra la original marcada como corregida con la adenda debajo, que
es literalmente lo que pide §2.1 del SRS.
d) MSP 001 — Admisión. Aquí hay una decisión que evita un desastre de datos:
el 001 no duplica la ficha del contacto. Nombres, documento, celular, correo
y dirección ya viven en contacts; el formulario los referencia y solo aporta
lo que hoy no existe (autoidentificación étnica, nivel de instrucción, ocupación,
nacionalidad, seguro RPIS/RPC, familiar de contacto). Si se copian, a la semana
hay dos verdades y ninguna es la buena. Es además el formulario de recepción,
el que justifica el permiso health.admission.manage de L4.
e) MSP 002 — Consulta externa. Dos piezas que no pueden ser jsonb
suelto porque se consultan:
- Signos vitales → tabla propia, con IMC calculado en la base (columna generada), no en el cliente. Los usa enfermería (§9.3) y alimentan gráficas.
- Diagnósticos →
health_encounter_diagnoses(encuentro, código CIE-10, tipo presuntivo/definitivo, orden). Estructurados porque de ahí salen el disparador EPI-1 (§8), el certificado homologable al IESS (§7) y cualquier estadística.
El resto (motivo, enfermedad actual, antecedentes, examen físico por segmentos) sí cabe como instancia de formulario.
f) MSP 005 — Evolución SOAP. Encaja sobre clinical_encounters, que ya
existe: se le añade la estructura S/O/A/P y firma independiente por entrada
(la de las 08:00 y la de las 14:00 son documentos distintos). Es el punto donde
más se nota L4: hoy una evolución se re-escribe encima.
g) MSP 024 — Consentimiento informado. Es el único con requisitos de no-repudio:
- Registrar que el paciente leyó: versión del texto, hash del documento mostrado, marca de tiempo del servidor.
- Aceptación por trazo (canvas → PNG a bucket privado, nunca público) u OTP al celular — y aquí hay una reutilización valiosa: el envío por WhatsApp ya está montado para los recordatorios de cita (outbox + canales). El OTP sale casi gratis.
- Guardar método, IP y user-agent junto a la aceptación.
h) Impresión. Los formularios se imprimen con el motor de plantillas de L1; no se escribe una segunda vista de impresión.
13.3 L6 en detalle — folios, QR, firma y EPI-1
a) Series y folios ACESS (§6.2). La ACESS vende blocks físicos numerados a
un médico concreto; el software registra el rango y va descontando. El mecanismo
no es de salud — es el ir.sequence de Odoo — y vive como servicio de
plataforma (§18); lo de Ecuador es solo la configuración del block.
health_number_seriespor profesional (no por tenant), con rango, valor siguiente y referencia de la autorización.- El consumo es transaccional (
UPDATE ... RETURNINGsobre la fila de la serie, que serializa) +UNIQUE (series_id, value)enhealth_number_assignments. Dos recetas simultáneas no pueden llevar el mismo folio ni saltárselo. - Anular una receta no libera el folio: queda consumido y marcado como anulado. Un folio reutilizado es exactamente lo que la norma quiere evitar.
- Aviso cuando queden pocos folios, y bloqueo cuando se agoten: el médico tiene que comprar otro block, y el sistema no puede inventarse números.
b) QR de verificación pública (§7). Ruta /verify/[folio] que devuelve
solo: folio, tipo, fecha de emisión, profesional, estado (vigente/anulado) y
coincidencia de hash. Nunca datos clínicos ni el nombre completo del paciente
— el empleador comprueba que el certificado es real, no qué tiene su empleado.
Se copia el patrón de /d/[token] (cliente admin en servidor + RPC), con la
trampa ya conocida: esa ruta corre sin sesión, así que la función no puede
llamar a health_audit (audit_logs.user_id es NOT NULL y revienta); se traza
en la propia fila.
c) PDF en servidor. Sigue haciendo falta aunque la firma salga de alcance: el QR, el folio y el envío al paciente necesitan un archivo, no una ventana de impresión.
ruta /print/... (la que ya existe) ──Playwright/Chromium──▶ PDF
Un solo renderizador — el de React que ya se está construyendo en L1 — y la
fidelidad de lo que el médico ve en pantalla, gratis. El coste es operativo:
Chromium en la imagen del backend (~300 MB) y un servicio más que vigilar.
pdf-lib dibujando desde el árbol de bloques sigue siendo la alternativa si ese
coste se considera inaceptable, pero es un segundo renderizador que habrá que
mantener sincronizado con el primero para siempre. Recomiendo Chromium.
d) Firma electrónica (§3). Fuera de alcance (§19). El sistema genera el PDF; el médico lo firma con su herramienta y su certificado acreditado, y lo sube de vuelta para que el expediente quede completo. Lo único que hay que construir aquí es ese retorno: un adjunto marcado como "versión firmada".
e) EPI-1 (§8). Es la fase más barata de las cuatro porque llega la última:
necesita el catálogo CIE-10 con flags.notifiable (L3), los diagnósticos
estructurados (L5) y el motor de formularios (L5). Lo propio es el disparador —
al guardar un diagnóstico notificable, un aviso modal — y la ficha EPI-1
preconsolidada, dejando libres solo fecha de inicio de síntomas, lugar probable
de contagio y estado de laboratorio.
Punto a verificar antes de estimarla: si el SIVE acepta envío electrónico o
si la notificación es en papel/portal. Cambia entre "generar un PDF" e "integrar
con un tercero", que no se parecen en nada.
13.4 Camino crítico y qué puede ir en paralelo
P0 ──┬──────────────────────────────────────────────▶ L4 ──┬──▶ L5 ──▶ L6
│ │
L0 ──┼──▶ L1 ──▶ L2 ────────────────────────────────▶──────┤
│ │
└──▶ L3 ─────────────────────────────────────────────▶┘
- L1 (editor) y L3 (catálogos) no se estorban: tocan zonas distintas. Dos personas pueden ir a la vez desde el día siguiente a L0.
- P0 y L0/L1 tampoco: P0 es backend y auth, L1 es frontend y SQL de salud.
- L4 es la barrera. Cambia cómo se guarda lo clínico y toca permisos vivos; conviene que no haya otra cosa a medias cuando entre.
- L5 → L6 es estrictamente secuencial: sin diagnósticos estructurados no hay EPI-1 ni certificado homologable.
- El camino crítico real es P0 → L4 → L5 → L6. L1 y L2, que son lo que el cliente ve, están fuera de él: se pueden entregar pronto y sin culpa.
Archivos previstos
shared/src/modules/health-l10n.ts contrato (solo tipos)
shared/src/modules/health.ts + tipos de doc por bloques
frontend/src/lib/health/l10n/{index,generic,ec}.ts registro + paquetes
frontend/src/lib/identity-documents.ts absorbido por el paquete (re-export)
frontend/src/sections/health/documents/render/ node-*.tsx + tabla de despacho
frontend/src/sections/health/documents/editor/ TipTap + extensiones propias
frontend/src/sections/health/branding/ perfil de marca + activos
backend/src/modules/health/l10n/ gemelo del registro
backend/src/modules/manifests.ts + healthL10nEcManifest
supabase/migrations/2026XXXX_health_template_blocks.sql
supabase/migrations/2026XXXX_health_branding_assets.sql
supabase/migrations/2026XXXX_health_locale_core.sql
supabase/migrations/2026XXXX_l10n_ec_seed.sql
— L4 en adelante —
frontend/src/sections/health/forms/ renderizador genérico + instancias
frontend/src/sections/health/consent/ trazo / OTP del MSP 024
frontend/src/sections/health/series/ blocks ACESS del profesional
frontend/src/app/verify/[folio]/page.tsx verificación pública (sin PHI)
backend/src/modules/health/pdf/ render (Chromium) + firma (@signpdf)
backend/src/modules/health/forms/ definiciones e instancias
backend/src/modules/permission.guard.ts extendido al resto de controladores
supabase/migrations/2026XXXX_health_immutability.sql adendas + versionado + permisos
supabase/migrations/2026XXXX_health_forms.sql
supabase/migrations/2026XXXX_health_vitals_diagnoses.sql
supabase/migrations/2026XXXX_health_number_series.sql
14. Prerrequisitos y riesgos
Migraciones sin aplicar.Cerrado el 2026-08-21: F1, F2 y F3 están aplicadas y en uso, verificado contra la base. Se descubrió de rebote que la plataforma de módulos (20260813_module_platform.sql) tampoco lo estaba, y fallaba en silencio —getEnabledModulesse traga el error y devuelve lista vacía. Lección que sí queda viva: una migración pendiente aquí no rompe, degrada, así que hay que comprobarla contra la base y no fiarse de que la pantalla se vea bien.- PDF en servidor. Resuelto en §13.3.c: Chromium renderiza la ruta
/print/...que ya existe, y no hace falta un segundo renderizador. El coste es operativo (Chromium en la imagen del backend), no de diseño. Se necesita igualmente aunque la firma haya salido de alcance. - Backend sin autenticación. Ya no es un riesgo aparcable: es la fase P0
(§13.1). Con L6 como meta, un backend que acepta el
tenant_idque le manden no pasa una inspección, y todo lo que se construya encima hereda el agujero. - Riesgo legal. Los formatos del MSP cambian por resolución ministerial. Por
eso las definiciones de formulario son datos versionados
(
health_form_definitions.version), no componentes React: actualizar un formulario debe ser una migración de semilla, no un despliegue de código. Y una advertencia honesta: este plan interpreta un SRS, no es asesoría legal; antes de anunciar "cumple MSP" hace falta que lo valide alguien que responda por ello. - Rendimiento y abuso del editor. Ver los límites duros del §7. Se comprueban al renderizar.
- i18n obligatoria. Toda cadena nueva en
esyencomo mínimo; el resto cae al fallback español, como ya hace todo el módulo de salud.
15. Decisiones
Resueltas
¿Hasta dónde llega Ecuador?→ hasta L6 completo (2026-08-20). De ahí salen P0, §13.2 y §13.3.¿Chromium o→ Chromium para generar,pdf-lib?@signpdfpara firmar; no eran alternativas (§13.3.c).¿Custodia de la clave de firma? ¿Qué TSA? ¿Token o archivo?→ todas cerradas de golpe: la firma sale de alcance (§19, 2026-08-21). El sistema genera el documento y el médico lo firma fuera. Queda solo el retorno del PDF firmado como adjunto (variante B). El análisis descartado está en §19.z.
Abiertas, por orden de urgencia
- ¿Se extrae
documentsahora? (§18). Es la decisión más cara de aplazar: hacerlo al empezar L1 no cuesta casi nada; hacerlo después de que salud, CRM y agenda hayan escrito encima, muchísimo. Recomiendo extraer. - ¿TipTap? Añade una dependencia grande. La alternativa (editor de bloques propio sobre MUI) es más pobre para redactar. Recomiendo TipTap. Bloquea L1, que es lo primero que se ve.
- ¿Quién valida legalmente? Este plan interpreta un SRS; no es asesoría legal. Antes de decirle a un cliente "cumple MSP" hace falta alguien que responda por esa frase. Conviene saber quién es ahora, no al final de L6.
- ¿Qué país entra segundo? Si ya se sabe, conviene bosquejar su paquete en L0 aunque quede vacío: dos paquetes reales validan el contrato, uno solo lo confirma por casualidad.
16. Qué hay que averiguar fuera del código
Cuatro cosas que este plan asume y no puede comprobar solo. Cada una puede mover una estimación de forma importante, así que conviene resolverlas pronto:
| Qué | Por qué importa | Cuándo hace falta |
|---|---|---|
| Cerrado: la firma salió de alcance (§19). Vuelve a abrirse solo si el validador legal dice que el §3 del SRS exige que firme el sistema | — | |
| Si el SIVE acepta notificación electrónica o es papel/portal | Es la diferencia entre "generar un PDF" e "integrar con un tercero" | Antes de estimar L6.e |
| La fuente oficial y actualizada del CIE-10 del MSP y del CNMB | La semilla de L3 sale de ahí; una lista sacada de cualquier sitio no es defendible ante un auditor | Antes de L3 |
| Cómo son de verdad los blocks de la ACESS: por médico, rango, vigencia, qué pasa al agotarse | El modelo de health_number_series está diseñado sobre el SRS, no sobre un block real en la mano | Antes de L6.a |
17. Comprobación de cumplimiento
Lista para seguir el avance con el cliente y, llegado el caso, para preparar la inspección. Cada fila apunta a la fase que la cierra.
| Exigencia | Fase | Cómo se demuestra |
|---|---|---|
| La historia clínica no se puede editar ni borrar tras firmar | L4 | Intentar corregir una evolución firmada: solo ofrece adenda |
| Toda lectura, impresión y exportación queda registrada | L4 | audit_logs con usuario, IP y user-agent |
| Recepción no puede leer el Form. 002 | L4 | Entrar con un usuario de recepción |
| Diagnóstico por CIE-10, no texto libre | L3 | El campo no acepta texto suelto |
| Prescripción por DCI | L3 | El buscador prioriza principio activo |
| Formularios MSP 001/002/005/024 completos | L5 | Emitirlos y compararlos con el formato oficial |
| Consentimiento con no-repudio | L5 | Trazo u OTP + hash + marca de tiempo del servidor |
| Receta de psicotrópicos con folio ACESS descontado | L6 | Emitir dos y ver que los folios son consecutivos e irrepetibles |
| Certificado con QR verificable sin ver la historia | L6 | Abrir el QR en una sesión anónima |
| El documento firmado por el médico está en el expediente | L6 | Abrir la ficha y ver el PDF firmado adjunto a su documento (§19 variante B) |
| 2FA obligatorio para médicos y administradores | P0 | Iniciar sesión sin segundo factor |
Parte IV — Qué sale de salud y se vuelve módulo
18. La regla de extracción
Odoo no mete la firma dentro de la app médica: tiene sign aparte, tiene
documents aparte y tiene ir.sequence en base. La intuición es correcta, y va
más lejos que la firma: buena parte de lo que este plan llama "salud" no es de
salud.
La plataforma ya tiene el mecanismo entero — manifiestos, dependsOn, activación
por tenant, siembra de permisos y menús. No hay que inventar nada: hay que decidir
dónde se escribe cada cosa antes de escribirla.
Regla: se extrae cuando hay un segundo consumidor real, o cuando extraerlo más tarde costaría datos. No antes — extraer sobre un solo consumidor es adivinar, y se acierta poco.
La ventana barata se estrechó, pero sigue abierta (medido el 2026-08-21). F1–F3 ya están aplicadas y en uso, así que renombrar dejó de ser un buscar-y-reemplazar. Aun así el coste real es bajo, y conviene ver por qué:
| Tabla | Filas | ¿Se mueve a documents? |
|---|---|---|
health_document_templates | 8 | Sí → doc_templates |
health_document_template_versions | 8 | Sí → doc_template_versions |
health_documents | 4 | No: es PHI, se queda en salud |
health_document_deliveries | 42 | No: apunta a documentos emitidos |
Solo se mueven las dos de plantillas, con 8 filas cada una, y ALTER TABLE … RENAME TO conserva los datos. El trabajo está en reescribir las RPC que las
nombran y las acciones del frontend — un rato, no una semana. Cada mes que
pase con más tenants encima, más caro.
| Pieza | ¿Hay un segundo consumidor? | Decisión |
|---|---|---|
| Motor de plantillas, editor de bloques, marca y activos (L1) | Sí, evidente: presupuestos y contratos del CRM, confirmaciones de agenda, facturas | Módulo documents, desde el día uno |
Entrega por enlace firmado /d/[token] (F3) | Sí: cualquier módulo que mande un documento a un cliente | Dentro de documents |
| Series numeradas y folios (L6.a) | Sí: facturas, presupuestos, cualquier correlativo legal | Servicio de plataforma (core), no módulo: es una tabla y una función, y la necesitan todos |
| Firma criptográfica | — | No se construye (§19). Si algún día se retoma, nace como módulo sign sobre documents |
| Motor de formularios (L5) | No, todavía | Dentro de salud, con esquema neutral y cero columnas de salud, listo para salir cuando aparezca el segundo |
| Paquetes de localización | Sí, el CRM tendrá su l10n fiscal | El contrato es por módulo: se copia el patrón, no se comparte el objeto |
Mapa
core
└─ sequences servicio: series numeradas y folios
documents plantillas · bloques · marca · activos · emisión · entrega
├─ health dependsOn ['scheduling', 'documents']
└─ crm (futuro) dependsOn ['documents']
(`sign` iría aquí si se retomara §19.z)
health_l10n_ec dependsOn ['health']
La ranura signature del paquete EC declara que Ecuador exige firma y la pantalla
lo recuerda al emitir —"este documento debe firmarse antes de entregarse"— aunque
el sistema no la aplique (§19). Regla general para el futuro: dependencia dura
(dependsOn) solo cuando sin ella el módulo no funciona; si funciona pero
incompleto, se declara en la ranura y se avisa.
La frontera que hay que cuidar: PHI
documents aporta el motor; el documento emitido se queda en salud. Parece
un detalle y es lo que separa esta extracción de una ingenua:
- Una plantilla, una marca y un logo no son datos de paciente: pueden vivir en
tablas con política de
SELECTnormal. - Un certificado emitido sí es PHI y tiene que seguir bajo el régimen del
módulo de salud: RLS +
REVOKE ALL, sin política de SELECT, acceso solo por funcioneshealth_*auditadas.
Por eso health_documents no se mueve, y cuando el CRM emita presupuestos tendrá
su crm_documents. La alternativa —una tabla común de documentos emitidos con una
columna module— obligaría a meter tres regímenes de seguridad distintos dentro
de una sola tabla, que es exactamente como se filtran las historias clínicas.
19. La firma: fuera de alcance
Decidido 2026-08-21: el sistema no produce firmas criptográficas. Genera el
documento; el médico lo firma aparte, con su herramienta y su certificado
acreditado. Se descartan N2s y N2c, y con ellos la custodia del .p12, la TSA,
el perfil B-LT, el almacén de raíces y el módulo sign entero. El análisis que
llevó hasta ahí queda archivado en §19.z, sin borrar: si algún día se retoma,
no hay que volver a derivarlo.
Lo que sí se queda: N0, el QR de verificación. Pasa a ser el único mecanismo antifraude que aporta el software, así que no se toca — sigue en L6 tal cual.
Ahora bien, hay un hueco que la decisión abre y que conviene tapar, porque es barato:
"Que firme en otro software" no falla al firmar; falla al volver. El médico exporta el PDF, lo abre en FirmaEC o en la herramienta de su certificador, lo firma, lo guarda… y el documento legalmente válido termina en el escritorio de su computadora. En el sistema queda la copia sin firmar. La historia clínica deja de ser el expediente y pasa a ser un borrador de lo que está en otra parte — justo lo que un auditor no quiere encontrarse. Y a cuarenta recetas al día, nadie repite ese baile.
Hay tres formas de vivir con eso, y la diferencia de coste entre las dos primeras es casi nula:
| Qué hacemos | Coste | Qué queda en el expediente | |
|---|---|---|---|
| A · solo exportar | Generamos el PDF y ahí acaba nuestra parte | ~0 | La copia sin firmar. El documento válido vive fuera |
| B · exportar + volver a subir | El médico sube el PDF ya firmado y lo adjuntamos al documento como la versión válida. No validamos la firma: la guardamos | muy bajo — es un adjunto, y F4 ya traía health_attachments | El expediente completo |
| C · B + validación | Además comprobamos que la firma es buena y de quién dice ser | medio: obliga al almacén de raíces y a una librería de validación | Lo mismo que B, pero demostrable |
Recomendación: B. Cuesta casi lo mismo que A y evita que la historia clínica se convierta en un borrador de lo que está en el escritorio del médico. No hay criptografía, ni KMS, ni TSA, ni raíces que mantener: es subir un archivo y marcarlo como la versión firmada. C se puede añadir después, el día que un auditor pregunte "¿y cómo saben que ese PDF está firmado de verdad?".
19.1 Qué desaparece del plan con esta decisión
| Pieza | Estado |
|---|---|
Módulo sign | fuera. Los módulos documents y sequences de §18 siguen: no dependían de la firma |
Custodia del .p12 y sus obligaciones (§19.b) | fuera |
| KMS en P0 | deja de ser bloqueante. Sigue siendo buena práctica para otros secretos, pero ya no frena nada |
| TSA, RFC 3161, PAdES B-LT (§19.d) | fuera |
| Almacén de raíces de las ECI | fuera, salvo que se haga C |
| Preguntas de §16 sobre certificadores y sellos | cerradas: ya no aplican |
| Tamaño de L6 | de L a M: quedan folios, QR, PDF en servidor y EPI-1 |
19.2 Lo que hay que decir con honestidad sobre el cumplimiento
El §3 del SRS pide que el sistema selle el PDF con PAdES. Con esta decisión, el sistema no lo hace: lo hace el médico con su herramienta. La firma sigue siendo legalmente válida —es un certificado acreditado— pero la frase que se le puede decir a un cliente cambia, y conviene tenerla escrita antes de una inspección:
El sistema produce los documentos y lleva su trazabilidad; la firma electrónica la aplica el profesional con su certificado acreditado, fuera del sistema.
Si eso basta o no para la ACESS es exactamente la pregunta de §15.3 — quien valide legalmente. Es la primera cosa que hay que preguntarle, porque si la respuesta es que no basta, hay que volver a §19.z y el coste vuelve entero.
Para generar el PDF en servidor sigue haciendo falta Chromium (§13.3.c): eso no era de la firma, era de producir el archivo.
19.z Archivado — el diseño de firma propia
Nada de lo que sigue está en alcance desde el 2026-08-21. Se conserva porque el análisis costó y porque la decisión puede volver a abrirse si el validador legal dice que el §3 del SRS exige que firme el sistema. Leer solo en ese caso.
Y "construirla entera" no es la barbaridad que escribí en la primera versión de
esta sección. Ahí sostuve que custodiar el .p12 era indefendible. Comprobado
en Odoo, el cuadro es otro y conviene tenerlo exacto:
- Odoo Sign (la app genérica) no custodia el certificado de nadie: el firmante dibuja, teclea o sube su rúbrica, y lo que da validez es el rastro —identidad por correo, IP, sello de tiempo, hash del contenido— más el certificado de auditoría. Es una firma electrónica simple bajo eIDAS y ESIGN; la propia documentación de Odoo dice que para escenarios estrictos (firma cualificada) hacen falta pasos adicionales de verificación de identidad.
- Odoo
l10n_ec_edi(la localización ecuatoriana) sí guarda el.p12y su contraseña, en Contabilidad → Certificados SRI, y con eso firma el XML de cada factura y la manda al webservice del SRI.
O sea: la custodia existe en Odoo, pero vive en la localización, es de la
empresa y es para documentos que emite una máquina — no en la app genérica de
firma. Y en Ecuador es la norma del mercado: cualquier producto de facturación
electrónica guarda el .p12 del contribuyente. Negarse por principio sería estar
fuera de lo que el cliente espera.
Queda una diferencia real que no se puede borrar: en la factura el certificado es de la empresa, y la empresa lo guarda en su propio sistema. En una receta el certificado es de una persona —el médico— y firma en su nombre profesional. Eso no lo hace imposible; cambia lo que hay que exigir alrededor (§19.b).
Decisión: se construye completa, con los tres modos como configuración del
módulo sign. El límite no lo pone la arquitectura —los tres comparten motor— lo
pone el volumen de cada tipo de documento.
| Nivel | Qué hace | Custodia de clave | Cuándo |
|---|---|---|---|
| N0 · QR de verificación | El documento lleva un QR contra una URL pública que confirma folio, tipo, fecha, profesional y estado. Sin criptografía | ninguna | L6, ya planificado |
| N1 · firma externa con retorno | El sistema exporta el PDF; el médico lo firma con su herramienta; lo vuelve a subir. El módulo valida la firma (cadena, integridad, que el firmante sea ese profesional) y la guarda como la versión buena | ninguna | L6, recomendado |
N2s · firma en el servidor (modelo l10n_ec_edi) | El médico deposita su .p12 una vez; el servidor firma al emitir. Es lo que hace Odoo con las facturas del SRI | sí, del .p12 del médico | L6 · modo principal, elegido 2026-08-21 — el único viable para volumen alto |
| N2c · firma diferida en el cliente | El servidor genera el PDF con hueco de firma y devuelve solo el digest; el navegador o un ayudante local lo firma con el token; el servidor incrusta la firma | ninguna | fuera de alcance (2026-08-21: solo certificado en archivo). Queda escrito por si algún día entra un token en hardware |
Una inversión que conviene ver antes de estimar: N2s es el más fácil de
programar de los tres (Chromium ya produce el PDF, @signpdf lo firma con el
.p12 y ya está) y el más difícil de operar (§19.b). N2c era exactamente al
revés: no obliga a custodiar nada, pero es criptografía en el navegador más un
ayudante de escritorio — y es justo lo que se evita dejándolo fuera. Lo caro de
N2s no es el código.
19.a Por qué N0 y N1 siguen existiendo si se hace N2
Hacer N2 no vuelve inútiles los otros dos: los tres cubren huecos distintos, y un tenant puede tenerlos activos a la vez según el documento.
- El QR cubre el día a día. Lo que se quiere evitar con la firma en una receta común o un certificado de reposo es la falsificación, y un QR contra una URL del sistema la resuelve sin criptografía ninguna.
- N1 arregla el fallo del camino "otro software": el retorno. Es la diferencia entre un archivo perdido y un expediente completo, y cuesta poco — validar una firma PAdES es mucho más barato que producirla, y no se custodia nada.
- El volumen alto y el peso legal no caen en los mismos documentos. Recetas comunes y evoluciones son decenas al día y no se van a firmar a mano; la historia clínica exportada para un juzgado, el certificado que un empleador va a discutir o el consentimiento de una cirugía son unos pocos al mes, y ahí firmar fuera es perfectamente tolerable.
Orden de construcción dentro de L6: N0 → N2s → N1. (N2c queda fuera: ver la nota de "solo archivo" al final de §19.b.)
Ayer escribí que N1 tenía que ir antes que N2s "porque para firmar hay que saber validar". Es verdad a medias y conviene precisarlo: lo que N2s necesita es la biblioteca de validación —para comprobar en pruebas que lo que produce está bien sellado— no el flujo de N1, que es una pantalla donde el médico sube un PDF que firmó por fuera. Así que se puede ir directo a N2s: se escribe la validación como pieza interna y N1 sale después casi gratis, porque solo le falta la pantalla.
N1 no se descarta, y ahora importa más: con N2c fuera, es la única salida para el médico que no quiera depositar su certificado. Cuesta poco una vez hecho N2s —solo le falta la pantalla— y es lo que hace que "solo archivo" sea una decisión segura en vez de un callejón.
19.b Lo que obliga la custodia
Guardar el .p12 de un médico no es una columna más: es un compromiso operativo.
Si se hace N2s, se hace entero. Nada de esto es opcional:
| Requisito | Por qué |
|---|---|
La clave que cifra el .p12 no vive en la misma base (variable de entorno o KMS) | Un volcado de la base robado no debe bastar para firmar |
| La contraseña del certificado nunca en logs, ni en trazas de error, ni en la respuesta de una API | Es el segundo factor del archivo |
| Delegación explícita y revocable del médico: qué tipos de documento autoriza a firmar automáticamente, desde cuándo, y un botón para retirarla | El certificado es personal y firma en su nombre; el consentimiento tiene que ser suyo y poder deshacerse |
| Cada uso, auditado: qué documento, cuándo, desde qué sesión | Sin esto no se puede responder "¿quién firmó esto?" |
| Aviso de caducidad con antelación (los certificados ecuatorianos duran uno o dos años) | Un certificado vencido rompe la emisión el peor día |
| Baja inmediata al revocar, y borrado real del material | La revocación tiene que significar algo |
| Plan de brecha escrito antes de guardar el primer certificado | Si se filtra, hay que avisar a cada médico y a la autoridad. Improvisarlo es peor |
Supuesto declarado: solo certificado en archivo (decidido 2026-08-21). El
sistema soporta .p12 / .pfx y no soporta token en hardware, que es lo que
habría obligado a construir N2c. Es lo que abarata L6.
Como todo supuesto, hay que escribir qué pasa cuando se rompa: si mañana llega un médico con la firma en token, no puede depositarla, y la salida es N1 — firma con su herramienta y sube el PDF. Es manual, pero funciona desde el primer día y no obliga a abrir un proyecto nuevo. Por eso N1 no es opcional.
Nota de alcance: esto engorda P0, no L6 — la gestión de secretos (KMS o
equivalente) es infraestructura de plataforma, y hasta que exista no se debe
guardar el primer .p12.
19.c Diseño de N2s
El almacén. Tabla sign_certificates: tenant, owner_profile_id, alias, el
.p12 cifrado, la contraseña cifrada, referencia a la clave del KMS, y —en
claro, porque hacen falta para avisar y auditar— sujeto, emisor, número de serie
y valid_from / valid_to. Estado active | revoked | expired. El material
descifrado existe solo en memoria durante la firma; no se escribe en disco, ni
en caché, ni en un log.
La regla que más reduce el daño no es criptográfica. Se firma únicamente
dentro de una petición autenticada del propio médico, con sesión aal2: nunca
desde una cola, un cron, un webhook ni una herramienta de bot. Es la diferencia
entre "el servidor puede firmar cuando el médico lo pide" y "el servidor puede
firmar". Cuesta una comprobación y quita de la mesa el escenario feo — que alguien
que entre al backend emita recetas de estupefacientes de madrugada.
Ojo con la consecuencia: eso excluye firmar desde el bot de WhatsApp. Es deliberado, y es el mismo muro que ya bloquea el doctor-assist por canal: un número de teléfono no prueba que quien escribe sea el médico.
19.d Sellado de tiempo — resuelto, con una corrección
Confirmado (2026-08-21): las ECI acreditadas en Ecuador prestan el servicio de Autoridad de Sellado de Tiempo (TSA) sobre TSP, RFC 3161. Es el estándar internacional del IETF, está bien soportado y deja de ser una incógnita.
Corrección a lo que escribí antes. Dije "PAdES-T" y me quedé corto para este caso. PAdES-T (baseline B-T) prueba cuándo se firmó, pero no que el certificado estuviera vigente en ese momento: eso se demuestra con los datos de revocación (OCSP o CRL) del instante de la firma. Y esos datos hay que incrustarlos al firmar, porque dentro de diez años el respondedor OCSP de un certificado muerto hace años no va a contestar. Con retención a diez años el objetivo correcto es PAdES B-LT — firma + sello + material de validación dentro del propio PDF (diccionario DSS). Añadirlo después es imposible sin tocar el documento, que es justo lo que L4 prohíbe: nace B-LT o no nace.
Parámetros que quedan fijados, y que no conviene dejar al criterio del que implemente:
| Punto | Valor |
|---|---|
| Protocolo | TSP · RFC 3161 sobre HTTP |
| Hash del message imprint | SHA-256. SHA-1 no |
certReq | true — que la TSA devuelva su certificado, para que el token se pueda verificar solo, sin llamar a nadie |
| Nonce | uno distinto por petición, y comprobar que vuelve igual |
| Dónde va el token | atributo no firmado id-aa-signatureTimeStampToken del CMS |
| Perfil objetivo | PAdES B-LT: sello + OCSP/CRL embebidos en el DSS |
| Verificación al recibir | que el imprint coincide, que el nonce coincide, y que el certificado de la TSA encadena a una raíz de confianza |
La TSA es una dependencia de red dentro del camino de firma, y eso obliga a
decidir el modo de fallo. La respuesta correcta es la estricta: firma y sello son
un solo paso atómico. Si la TSA no responde, el documento no se emite
firmado — se queda en borrador y se le dice al médico. Nunca un documento
marcado como firmado sin su sello, porque "lo sellamos luego" es exactamente el
UPDATE sobre lo firmado que L4 prohíbe.
Cada sello se paga. Las TSA cobran por estampilla o por bolsa, así que hay una decisión de producto detrás de una técnica: qué tipos de documento llevan firma criptográfica. Es la misma separación por volumen de §19, ahora con un coste unitario real — recetas comunes a decenas por día contra unos pocos certificados al mes. Conviene que el tipo de documento declare si exige firma, y que el paquete EC ponga los valores por defecto.
La configuración es del país, no del módulo. URL de la TSA, OID de política,
algoritmo y credenciales van en la ranura signature del paquete EC y en la
configuración del tenant — cada clínica puede contratar una ECI distinta. El
módulo sign se queda genérico: habla RFC 3161 y no sabe en qué país está. Las
credenciales de la TSA son otro secreto para el KMS de P0.
19.e Quién contrata qué (y quién paga)
Una ECI vende cosas distintas, y conviene no confundirlas porque cambian de dueño:
| Pieza | Quién la contrata | Coste | Nuestro papel |
|---|---|---|---|
Certificado de firma (.p12) | El médico, a su nombre | anual, por persona | Ninguno. Lo recibimos ya emitido y lo custodiamos (§19.b) |
| Sellos de tiempo (TSA) | Decisión abierta: el tenant o la plataforma | por estampilla o por bolsa | Consumirlos al firmar |
| Raíces e intermedios de las ECI acreditadas | Nadie: son públicos | 0 | Nuestro: mantener el almacén de confianza al día |
El certificado no es negocio nuestro y no debe serlo: es personal del médico y firma en su identidad profesional. Los sellos sí hay que decidirlos, porque los consume el servidor en el momento de firmar y alguien tiene que tener la cuenta:
- El tenant trae su TSA (recomendado por defecto). La clínica ya tiene trato comercial con una ECI —ahí compran los certificados sus médicos— y sus credenciales van a su configuración, como cualquier otra integración. La plataforma no adelanta un coste variable ni acaba revendiendo un servicio regulado.
- La plataforma pone una TSA común. Quita fricción de alta —pedirle a una clínica pequeña que contrate una TSA antes de emitir su primera receta firmada es una barrera real— a cambio de un coste por documento que hay que recuperar en el precio.
Recomendación: construirlo como configuración del tenant (soporta los dos), y dejar la TSA común como decisión comercial, no técnica, para cuando se sepa el precio por estampilla. Y comprobar una cosa antes: algunos paquetes de certificado de las ECI ya incluyen una cuota de sellos, en cuyo caso el médico o la clínica ya los tienen pagados y solo hay que pedir las credenciales.
Almacén de raíces: es gratis y es nuestro sí o sí. Sin los certificados raíz e
intermedios de las ECI acreditadas, la validación no puede distinguir un
certificado bueno de uno autofirmado — ni el .p12 que deposita el médico, ni el
PDF que sube en N1, ni el token que devuelve la TSA.
De dónde salen, en dos pasos que no se pueden juntar:
- Quién está acreditado → el registro de ARCOTEL
(
arcotel.gob.ec/entidades-de-certificacion-firma-electronica/) yfirmadigital.gob.ec. Esta es la lista con valor oficial; ninguna otra. - El archivo del certificado → el sitio de cada ECI, en su centro de
descargas. El del Banco Central está en
eci.bce.ec, con la AC raíz y la AC subordinada separadas. Las demás publican lo suyo en su propio dominio.
Cuidado con los revendedores. Hay "terceros vinculados" registrados en ARCOTEL (Eclipsoft, Datilmedia, Argosdata y otros) que comercializan certificados respaldados por una ECI acreditada. Un médico puede haber comprado a cualquiera de ellos: lo que hay que meter en el almacén es la raíz de la ECI que emite, no la marca a la que le pagó.
Disciplina de mantenimiento, porque un almacén de confianza mal llevado es peor que no tenerlo:
- Descarga por HTTPS desde el dominio de la propia ECI, nunca desde un enlace de
un tercero ni desde el certificado que se está validando (
AIA/caIssuerssirve para construir la cadena, jamás para decidir en quién confiar: es circular). - Verificar la huella SHA-256 contra una fuente independiente antes de aceptarla, y dejarla escrita en un manifiesto junto al archivo.
- Los certificados se versionan en el repositorio y cambiarlos es un evento de seguridad: PR con revisión humana, nunca actualización automática desde la red.
- Vigilar caducidades. Las raíces duran diez o veinte años; las subordinadas rotan bastante más.
Atajo útil para arrancar: un .p12 real suele traer dentro su cadena de
intermedios, así que el primer certificado que deposite un médico sirve para
sembrar el almacén — pero la raíz se ancla siempre desde la fuente oficial, no
desde el archivo que llegó.
Nota de implementación: verificar si la versión de @signpdf en uso trae
soporte de TSA; si no, la petición y la respuesta TSP se construyen con PKI.js
(tiene TimeStampReq / TimeStampResp) y el token se incrusta como atributo no
firmado. La parte B-LT —recoger OCSP/CRL y escribir el DSS— es trabajo aparte y
hay que contarlo en la estimación de L6.
La delegación es un documento, no una casilla. El médico autoriza —en el propio sistema, firmando con N0/N1— qué tipos de documento pueden firmarse con su certificado y desde cuándo. Revocable con un botón; revocar no invalida lo ya firmado, solo impide firmar más.
Antes de la primera firma real, cinco cosas, en este orden:
- KMS operativo (P0). Sin esto no se guarda ningún
.p12. - Plan de brecha escrito: a quién se avisa, en cuánto tiempo, con qué texto.
- El texto de la delegación, revisado por quien responda legalmente (§15.3).
- Un certificado real de prueba de un certificador ecuatoriano — no uno autofirmado, que no prueba nada de la cadena.
- La prueba de manipulación: alterar un byte del PDF firmado y comprobar que la validación falla. Es la fila de §17 que un auditor va a querer ver en vivo.
20. Qué cambia en las fases
Nada del alcance; sí el sitio donde se escribe cada cosa, y el tamaño de L6.
- L1 se construye como módulo
documents. Mismo trabajo, distinto paquete y un manifiesto más. El sobrecoste hoy son horas; dentro de seis meses, con salud, CRM y agenda escribiendo encima, son semanas. - L6.a (folios) pasa a
sequences, servicio de core. Lo de Ecuador se reduce a la pantalla donde el médico registra el rango de su block. - El módulo
signno se construye (§19). De L6.c/d queda solo el QR y el retorno del PDF que el médico firmó fuera, que es un adjunto. L6 baja a M. Chromium sigue haciendo falta para generar el PDF: eso no era de la firma. - L5 se queda dentro de salud — un solo consumidor — pero con el esquema de formularios neutral, sin una sola columna que diga "paciente", para que salir sea un renombrado el día que el CRM quiera formularios de calificación.
- El orden no cambia: P0 → L0 → L1 → L2 → L3 → L4 → L5 → L6.
Glosario
| Sigla | Qué es |
|---|---|
| ACESS | Agencia de Aseguramiento de la Calidad de los Servicios de Salud. Audita expedientes, da permisos de funcionamiento y controla los recetarios de psicotrópicos |
| ARCOTEL | Agencia de Regulación y Control de las Telecomunicaciones. Acredita a las ECI |
| CIE-10 | Clasificación Internacional de Enfermedades, 10.ª revisión. El catálogo de diagnósticos |
| CNMB | Cuadro Nacional de Medicamentos Básicos |
| DCI | Denominación Común Internacional: el principio activo (paracetamol), no la marca |
| ECI | Entidad de Certificación de Información. El nombre ecuatoriano de una Autoridad de Certificación (CA): emite certificados de firma con validez legal y presta servicios como el sellado de tiempo. BCE, Security Data, ANF, UANATACA, Consejo de la Judicatura |
| EPI-1 | Ficha de notificación epidemiológica obligatoria, del sistema SIVE |
| HCU | Historia Clínica Única. La serie de formularios del MSP |
| IESS | Instituto Ecuatoriano de Seguridad Social. Homologa los certificados de reposo |
| KMS | Servicio de gestión de claves. Guarda la clave que cifra los secretos, fuera de la base de datos |
| LOPDP | Ley Orgánica de Protección de Datos Personales. Clasifica los datos de salud como sensibles |
| MSP | Ministerio de Salud Pública |
| PAdES | Perfil de firma electrónica para PDF. B-T añade sello de tiempo; B-LT añade además los datos de revocación, para que la firma se sostenga a largo plazo |
| PHI | Protected Health Information: cualquier dato clínico identificable |
| RPIS / RPC | Red Pública Integral de Salud (MSP, IESS, ISSFA, ISSPOL) / Red Privada Complementaria |
| SRI | Servicio de Rentas Internas. Aparece aquí solo como referencia: es a quien firma facturas el l10n_ec_edi de Odoo |
| TSA / TSP | Autoridad de Sellado de Tiempo y su protocolo, RFC 3161 |
aal2 | Nivel de garantía de autenticación 2: la sesión pasó por segundo factor |
.p12 / .pfx | El archivo que contiene el certificado y su clave privada, protegido por contraseña |
21. Validación de L0–L3 (2026-08-23)
130 comprobaciones: 71 contra la base real (solo lectura y funciones puras, sin
escribir una fila de datos del cliente) y 59 sobre la lógica pura del frontend.
0 fallos. Más tsc de los dos paquetes, eslint y next build completo.
| Bloque | Qué se comprobó | Resultado |
|---|---|---|
| Migraciones | Las 11 aplicadas, con sus tablas, columnas y funciones | 8/8 |
| Contratos RPC ↔ tipos | Que cada RPC devuelve las claves que el frontend declara | 5/5 |
| Reglas de negocio | Deduplicado de activos, bloques obligatorios (positivos y negativos, incluido anidado hondo), semillas EC | 12/12 |
| Jurisdicción | Los 24 tenants resuelven la plantilla que les toca | 24/24 |
| Datos intactos | 8 versiones en v1 y 3 en v2, ninguna sin texto, coherencia v1⇄v2 | 5/5 |
| Seguridad | La clave anónima bloqueada en 6 tablas con PHI o configuración | 6/6 |
| Catálogos (L3) | Registrados, vacíos, búsqueda degradando, importación cerrada | 13/13 |
| Lógica pura | Cédula/RUC, resolución de paquete, saneo, límites, ida y vuelta v1⇄v2, tokens | 59/59 |
21.1 El fallo que encontró
Las plantillas ecuatorianas declaraban signatureBlock y qr, el renderizador
reservaba el hueco correctamente… y ninguna vista de impresión pasaba esas
ranuras: solo prescriptionItems. Resultado: el certificado ecuatoriano se
habría impreso sin espacio de firma y sin QR, cumpliendo formalmente sus
required_nodes y siendo inservible en la práctica.
Es un fallo de contrato entre dos capas que por separado funcionaban, y no lo habría cazado ningún test unitario de ninguna de las dos.
Se arregló en tres frentes:
- La ranura de firma se rellena (
sections/health/documents/signature-block.tsx), en las dos vistas de impresión. Es la línea, el nombre y las credenciales; la firma electrónica sigue siendo externa (§19). - El
qrdeja de ser obligatorio hasta L6. Exigir un bloque que el sistema no puede pintar es el mismo error que se evitó a conciencia con el CIE-10: el usuario no lo puede quitar y no hace nada. El nodo se queda marcando el sitio. - Las plantillas EC pasan a
professional.credenciales, que imprime ACESS y SENESCYT; conprofessional.registrosolo salía la licencia antigua. Y la receta EC, que no tenía ningún token de profesional, ya lleva su nombre y sus registros bajo la firma.
21.2 Lo que NO se pudo validar, y por qué
Honestidad sobre el alcance: verde aquí no quiere decir probado en producción.
- El camino feliz con sesión. Casi todas las funciones
health_*compruebanuser_has_permission, que resuelve conauth.uid(); con la clave de servicio eso es NULL y la función rechaza. Se comprobó que rechaza —lo cual es en sí una prueba de seguridad— pero emitir un documento de principio a fin exige un navegador con sesión. - El disparador de DCI. Comprobarlo obligaría a insertar una receta en datos reales del cliente. Se sabe que la migración aplicó; su comportamiento se verá con la primera receta que se emita.
- Nada se ha visto en pantalla. Compila y las rutas se generan, pero no se ha pulsado un botón. El editor, la marca y las credenciales están sin probar en navegador.
vitestsigue sin arrancar con Node 20.11.1 (necesita ≥20.12). Los tests están escritos; la misma lógica se verificó compilando y ejecutando con Node.
21.3 Pendiente antes de que Ecuador sirva de algo
- Ningún tenant tiene país configurado, así que hoy todos resuelven la
plantilla genérica. Manta Hospital Center necesita
country = 'EC'en la configuración del workspace para que aparezcan las ecuatorianas. - Los catálogos CIE-10 y CNMB están vacíos — bloqueado por §16.
- Nadie tiene credenciales cargadas: hasta que se rellenen, los documentos imprimen la licencia antigua del recurso.