Ir al contenido

Modelo de datos

El modelo de datos de EMA Clinic refleja la dualidad arquitectónica del producto: la información clínica vive como recursos FHIR canónicos en Aidbox, mientras que la configuración multi-tenancy, la operación de caja y las extensiones específicas de Chile viven en Supabase. Esta página documenta ambos lados —los schemas operacionales y los recursos FHIR utilizados— y cierra con las state machines que gobiernan los recursos con más estados visibles para el usuario.

Clinic usa dos schemas dentro del mismo proyecto Supabase: emashared para datos cross-product (compartidos con Lab, Vault, etc.) y emaclinic para datos propios del producto.

El schema emashared no pertenece exclusivamente a Clinic; lo escriben Vault (al provisionar tenants) y lo leen todos los productos del ecosistema. Las cinco tablas más relevantes para Clinic son:

TablaPropósito
tenantsDefinición compartida del tenant: id, slug, country, is_active
user_rolesBridge entre user y tenant más rol; aplica RLS tenant_id = auth.tenant_id
tenant_aidbox_credentialsCredenciales OAuth de Aidbox encriptadas con AES-GCM y encryption_key_version para rotación
tenant_emi_accessActivación EMI por tenant; opera como gate del proxy
usersFlag is_platform_admin que distingue staff de EMA Health

El schema emaclinic agrupa lo operacional —caja, audit, pricing, scheduling, urgencia— en módulos identificados por número de migración. La estructura se mantiene estable desde la migración 007:

Core (001):
tenant_config # JSON {auth, dte, imed, urgencia, etc.}
appointment_config # max_slots_per_day, etc.
cash_registers # tenant_id, location_id, balance, date_opened/closed
cash_movements # register_id, amount, payment_method, reference_id
charge_item_config # tenant_id, charge_item_id, local_code, pricing_level
Audit (002):
audit_logs # tenant_id, user_id, action, resource_type, changes JSON
Pricing (003):
service_charge_items # tenant_id, charge_item_id, base_price_clp
pricing_agreements # tenant_id, organization_id, agreement_type
agreement_prices # agreement_id, charge_item_id, price_override
invoice_encounter # FK bridge: Invoice (FHIR) ↔ Encounter (FHIR)
integration_config # tenant_id, integration_type, url, credentials_encrypted
integration_log # tenant_id, integration_id, status, request/response JSON
Scheduling (004):
schedule_patterns # rrule (RRULE FREQ=DAILY;BYHOUR=...), max_daily_slots
schedule_overrides # date, reason, slot_count_override
scheduling_config # min/max advance days, cancellation window
scheduling_exceptions # date_from/to, exception_type, resolution_status
Urgencia (S4, S8 partial):
triage_esi_audit # F190 — triage_id, suggested_esi, final_esi, override_reason
vital_signs_reference_ranges # per-tenant baseline by age_range
Patient sequence (007):
patient_sequence # tenant_id, next_number INT (Patient.identifier generator)

Todas las tablas con columna tenant_id aplican la misma política de Row Level Security: tenant_id = (select tenant_id from user_roles where user_id = auth.uid()). Es la segunda capa de aislamiento entre tenants, complementaria al ORGBAC que opera sobre Aidbox.

El siguiente diagrama muestra las relaciones entre las tablas más usadas. Los recursos FHIR Invoice y Encounter aparecen como nodos externos porque viven en Aidbox, pero el bridge invoice_encounter los relaciona via FK desde Supabase:

tiene

1 config

opera

factura

negocia

registra

detalla

precio override

FK

FK

tenants

uuid

id

PK

text

slug

text

country

user_roles

uuid

user_id

FK

uuid

tenant_id

FK

text

role

admin · medico · enfermera · secretaria · cajero · profesional

bool

is_tenant_admin

tenant_config

cash_registers

uuid

id

PK

uuid

tenant_id

FK

uuid

location_id

timestamptz

date_opened

timestamptz

date_closed

numeric

balance

service_charge_items

uuid

id

PK

uuid

tenant_id

FK

text

charge_item_id

code FHIR

numeric

base_price_clp

pricing_agreements

uuid

id

PK

uuid

tenant_id

FK

uuid

organization_id

text

agreement_type

particular · fonasa · isapre · convenio

cash_movements

agreement_prices

Invoice

invoice_encounter

text

invoice_fhir_id

PK

text

encounter_fhir_id

uuid

tenant_id

Encounter

Clinic trabaja con 29 recursos R4 distintos, todos con perfiles del CL Core o del EMA IG aplicados según corresponda. El SDK emafhir se encarga de inyectar meta.tag = urn:ema:tenant|<tenantId> en cada recurso al momento de crearlo o actualizarlo, lo que sostiene una de las tres capas de aislamiento (ORGBAC, RLS y tag).

Los recursos se agrupan en cuatro familias por dominio funcional.

Los recursos clínicos básicos cubren paciente, encuentro, diagnóstico, observación y prescripción. Son los recursos que un médico toca en cada consulta:

RecursoPerfilCampos clave
Patientemaig.cl/core#Patientidentifier (RUT), telecom, address, birthDate, gender, maritalStatus, managingOrganization
Encounteremaig.cl/core#Encounterclass (AMB/VR/HOSP), serviceType, subject, diagnosis, reasonReference, appointment
Conditionemaig.cl/core#Conditioncode (SNOMED), verificationStatus (provisional → confirmed), onsetDateTime
Observationemaig.cl/core#Observationcode (LOINC), value, referenceRange, status (final/amended)
ServiceRequestemaig.cl/core#ServiceRequestcode, subject, encounter, basedOn (Appointment), performer, specimen
DiagnosticReportemaig.cl/core#DiagnosticReportcode, subject, encounter, performer, result (Observation[])
Procedureemaig.cl/core#Procedurecode, subject, encounter, status, performer
AllergyIntoleranceFHIR R4code, patient, criticality, reaction[]
MedicationRequestemaig.cl/core#MedicationRequestmedication, subject, encounter, dosage, status, requester
MedicationFHIR R4code (SNOMED), manufacturer

El bloque de agendamiento incluye los recursos que modelan disponibilidad y reservas. La extensión más relevante respecto del estándar FHIR es la integración con schedule_patterns (Supabase), porque el Schedule FHIR no soporta RRULE de forma nativa:

RecursoPerfilNotas
Appointmentemaig.cl/core#Appointmentstatus (booked → fulfilled), participant[], slot multi-resource, basedOn
ScheduleFHIR R4actor (Practitioner / Location / Device), planningHorizon
SlotFHIR R4schedule ref, status (free / busy), generado a partir de schedule_patterns
HealthcareServiceFHIR R4category, providedBy, location[], specialty, hoursOfOperation

Los recursos de facturación FHIR conviven con las tablas service_charge_items, pricing_agreements y cash_movements en Supabase. Las primeras representan el documento legal del cobro; las segundas, la operación de caja:

RecursoPerfilNotas
Invoiceemaig.cl/core#Invoicesubject, lineItem (ChargeItem[]), status (draft → issued → balanced)
ChargeItemFHIR R4code, occurrence, performingOrganization, priceOverride
ChargeItemDefinitionemaig.cl/core#ChargeItemDefinitionpropertyGroup (precios por Coverage), applicability
CoverageFHIR R4beneficiary, payor, class (Fonasa-tramo o Isapre)
DocumentReferenceemaig.cl/core#DocumentReferencecategory (clinical-note / dte / scanned-order / consent / discharge-summary)

Los recursos restantes cubren cuestionarios estructurados (SDC), tasks con SLA, definiciones de planes de cuidado y la estructura institucional:

RecursoPerfilNotas
QuestionnaireFHIR R4item[] (preguntas y grupos), extension subQuestionnaire
QuestionnaireResponseFHIR R4questionnaire ref, item[] (respuestas)
Taskemaig.cl/core#Taskcode (ges-declaration / sdc-form / eno-notification), restriction.period (deadline)
PlanDefinitionFHIR R4type (order-set / workflow-definition), action[]
ActivityDefinitionFHIR R4code, topic, performer, resource (specimen type)

A esto se suman los recursos que modelan estructura institucional y comunicación: Organization, Location y Device para el inventario físico; Practitioner y PractitionerRole para el staff; Flag para alertas (GES, alergias críticas); Communication para notificaciones y recordatorios; RelatedPerson para contactos del paciente; Specimen para órdenes de laboratorio; y Consent para consentimiento informado bajo Ley 20.584.

Los recursos Patient y los registros de vault_users no se mapean uno-a-uno por motivos de arquitectura: un mismo paciente puede ser también staff (un médico que se atiende en su propia clínica), y un mismo email puede tener un Patient en clínica A y otro distinto en clínica B. El sistema separa los flujos de acceso para mantener la distinción clara:

Paciente

OTP signInWithOtp

JWT con patient_id claim

Patient FHIR resource

Staff

Supabase Auth user UUID

user_roles · tenant_id · role

Practitioner FHIR

mapping manual

El staff entra con email y password —o el proveedor OAuth que tenga habilitado su tenant. El JWT que recibe no incluye tenant_id: es el tenantMiddleware el que lo resuelve consultando user_roles. Si el usuario tiene roles en múltiples tenants, el frontend mantiene el activo en localStorage y lo envía en cada request. El paciente entra al portal por una vía completamente distinta, basada en RUT, fecha de nacimiento y email; el sistema envía un OTP, valida y emite un JWT con patient_id como claim. El patientAuthMiddleware verifica ese token solo para rutas bajo /api/portal/*.

El motor de pricing reconoce tres modos de pago distintos y los procesa con engines separados. La decisión de cuál aplica depende del tipo de Coverage del paciente y del tipo de cliente que está detrás (particular, aseguradora o convenio empresa):

Particular

Aseguradora

Fonasa-tramo-A

Fonasa-tramo-B

Fonasa-tramo-C

Fonasa-tramo-D

Isapre

Convenio

POST /api/pricing/compute

Tipo de pago

base_price_clp

de service_charge_items

Coverage.class

copago 0%

copago 10%

copago 20%

copago 20% + diff

bono IMED · imed-client

copago-engine.ts

pricing_agreements

type=convenio

agreement_prices override

Invoice.status = draft

postpaid

Invoice.status = issued

al cobro

El caso particular es el más simple: el sistema toma el base_price_clp de service_charge_items. Para Fonasa el copago-engine aplica el porcentaje correspondiente al tramo (0%, 10% o 20% más diferencial); para Isapre se invoca al imed-client que valida el bono físico o digital y aplica el monto correspondiente. El caso convenio empresa no cobra al paciente: deja un Invoice en estado draft que se factura al pagador mensualmente.

Cuatro recursos tienen estados visibles para el usuario y vale la pena documentar sus transiciones explícitamente. La cartografía siguiente muestra los caminos válidos para cada uno.

El Encounter tiene tres estados activos más cancelación:

admisión

cobro registrado

médico cierra · diagnóstico · summary

paciente no se presenta

error operacional

arrived

in_progress

finished

cancelled

El Appointment modela una reserva, con la particularidad de que puede entrar al sistema como proposed (slot virtual generado por el motor de scheduling) antes de pasar a booked:

$find devuelve slots

paciente / staff agenda

check-in

encounter creado

cancelación

paciente no se presenta

proposed

booked

arrived

fulfilled

cancelled

noshow

El Invoice tiene cuatro estados que reflejan el ciclo de vida del documento legal:

crear (cobro pendiente o convenio)

emisión SII

pago confirmado

anulación + nota crédito

draft

issued

balanced

cancelled

La Condition puede nacer como provisional (auto-detectada por el motor clínico, por ejemplo cuando una Observation de presión arterial supera 140) y solo pasa a confirmed cuando el médico la valida explícitamente:

auto-detected (PA>140 → HTA)

médico confirma

médico descarta

corrección

provisional

confirmed

refuted

entered_in_error

La Task modela tanto declaraciones GES como cuestionarios SDC pendientes y notificaciones ENO. La transición a failed es automática vía cron cuando vence restriction.period.end:

alguien toma la task

ready

in_progress

completed

cancelled

restriction.period.end define el SLA

Tasks vencidas se marcan failed por cron