Modelo de Datos: Interacciones Medicamento-Estudio¶
Identificador: MDL-INT-002 Version: 1.1.0 Fecha: 2025-12-08 Autor: DatabaseDrone (Doce de Quince) Modulo Funcional: MTS-INT-002 v1.1.0 Refs Tecnicas: TECH-CS-001 (3.2.3.1), TECH-DS-001 Nota Critica: Directiva del Director - Regla de 100 Registros (Catalogo Mini-Cache)
<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- 1. Resumen y Trazabilidad
- 2. Arquitectura Dual
- 2.1. Division de Responsabilidades
- 2.2. Principio de Operacion
- 3. Clasificacion de Datos
- 3.1. Tabla de Clasificacion
- 3.2. Justificacion de Clasificaciones
- 4. Esquema Cliente (Conceptual)
- 4.1. cli_med_study_rules (Base de Reglas Embebida)
- 4.2. cli_detected_med_study (Interacciones Detectadas)
- 4.3. cli_suspension_reminders (Recordatorios de Suspension)
- 4.4. cli_post_study_confirmations (Confirmaciones Post-Estudio)
- 5. Esquema Servidor (Conceptual)
- 5.1. srv_med_study_catalog (Catalogo Maestro)
- 5.2. srv_rules_updates (Actualizaciones OTA)
- 5.3. srv_encrypted_detections (Sincronizacion E2E)
- 6. Implementacion iOS (Swift/Realm)
- 6.1. MedStudyRule
- 6.2. DetectedMedStudyInteraction
- 6.3. SuspensionReminder
- 6.4. PostStudyConfirmation
- 7. Implementacion Android (Kotlin/Room)
- 7.1. MedStudyRule
- 7.2. DetectedMedStudyInteraction
- 7.3. SuspensionReminder
- 7.4. PostStudyConfirmation
- 8. Esquema Servidor (PostgreSQL con RLS)
- 8.1. srv_med_study_catalog
- 8.2. srv_rules_updates
- 8.3. srv_encrypted_detections
- 9. Indices y Constraints
- 9.1. Indices Cliente (iOS)
- 9.2. Indices Cliente (Android)
- 9.3. Indices Servidor
- 10. Row Level Security (RLS)
- 11. Mapeo de Sincronizacion
- 11.1. Sincronizacion de Detecciones
- 11.2. Actualizacion de Reglas OTA
- 12. Algoritmo de Deteccion
- 12.1. Pseudocodigo
- 12.2. Calculo de Tiempos de Suspension
- 13. Queries de Ejemplo
- 13.1. Buscar Interacciones al Agendar Cita
- 13.2. Verificar Estudios Futuros al Agregar Medicamento
- 13.3. Obtener Recordatorios Pendientes
- 13.4. Historial de Interacciones (6 anos HIPAA)
- 14. Consideraciones de Cifrado
- 14.1. Datos PHI que se Cifran E2E
- 14.2. Datos Publicos (No Cifrados)
- 15. Retencion y Limpieza
- 16. Validacion del Modelo
- 16.1. Clasificacion de Datos
- 16.2. Esquemas
- 16.3. Zero-Knowledge
- 17. Referencias
- 17.1. Documentos Funcionales
- 17.2. Documentos Tecnicos
- 17.3. Estandares e Investigaciones
1. Resumen y Trazabilidad¶
Dominio: INT (Interacciones) Ref Funcional: MTS-INT-002 Arquitectura: TECH-CS-001 (Cliente-Servidor Dual)
Este modelo define las estructuras de datos para el motor de deteccion de interacciones entre medicamentos activos del paciente y estudios medicos o tratamientos programados.
Caracteristicas clave:
- 100% operativo offline con mini-cache critico (<100 reglas)
- Cache embebido en app (NO descargado por OTA)
- Reglas del usuario obtenidas online bajo demanda
- Recordatorios automaticos de suspension
- Seguimiento post-estudio para reinicio seguro
- Sincronizacion E2E de detecciones (PHI)
- Disclaimer/consentimiento para busquedas online
2. Arquitectura Dual¶
2.1. Division de Responsabilidades¶
┌──────────────────────────────────────────────────────────────────┐
│ DISPOSITIVO (95%) │
├──────────────────────────────────────────────────────────────────┤
│ │
│ MINI-CACHE CRITICO (100% offline, embebido en app) │
│ ├── cli_critical_med_study_cache → <100 reglas CRITICAS │
│ │ (suspensiones obligatorias) │
│ └── Busqueda basica offline → solo lo mas critico │
│ │
│ REGLAS DEL USUARIO (SYNCED_E2E) │
│ ├── cli_user_med_study_rules → Reglas aplicables al usuario │
│ │ (resultado de busquedas) │
│ └── Se populan bajo demanda con consent │
│ │
│ DETECCIONES POR EVENTO (PHI - SYNCED_E2E) │
│ ├── cli_detected_med_study → Por cada cita/evento │
│ ├── cli_suspension_reminders → Programacion automatica │
│ └── cli_post_study_confirmations → Seguimiento reinicio │
│ │
│ CONSENTIMIENTO DISCLAIMER (SYNCED_E2E) │
│ └── cli_med_study_consent → Consent para busquedas │
│ │
│ MOTOR DE VERIFICACION │
│ ├── 1. Busca en mini-cache critico (offline) │
│ ├── 2. Si nada critico → ofrece busqueda online (con consent) │
│ ├── 3. Guarda resultados en cli_user_med_study_rules │
│ └── 4. Calculo automatico de tiempos │
│ │
└──────────────────────────────────────────────────────────────────┘
↕
Cifrado E2E (AES-256-GCM)
↕
┌──────────────────────────────────────────────────────────────────┐
│ SERVIDOR (5%) │
├──────────────────────────────────────────────────────────────────┤
│ │
│ CATALOGO MAESTRO (SERVER_SOURCE) │
│ └── srv_med_study_catalog → Reglas completas (~5,000+) │
│ │
│ ACTUALIZACIONES MINI-CACHE (App Update) │
│ └── srv_rules_updates → Actualizaciones del mini-cache│
│ (<100 reglas criticas) │
│ │
│ SINCRONIZACION E2E (SYNCED_E2E) │
│ ├── srv_encrypted_detections → Blobs opacos de detecciones │
│ └── srv_encrypted_user_rules → Blobs opacos de reglas user │
│ │
│ API BUSQUEDA ONLINE (con rate-limit) │
│ └── POST /v1/interactions/med-study/search │
│ → Busqueda para los medicamentos del usuario │
│ │
│ GARANTIA ZERO-KNOWLEDGE │
│ └── Servidor NO ve medicamentos ni estudios del paciente │
│ │
└──────────────────────────────────────────────────────────────────┘
2.2. Principio de Operacion¶
| Operacion | Ubicacion | Requiere Conexion | Clasificacion |
|---|---|---|---|
| Verificar interaccion (critica) | Cliente | No (mini-cache) | LOCAL_ONLY |
| Busqueda completa (bajo demanda) | Servidor | Si (con consent) | SYNCED_E2E |
| Guardar reglas del usuario | Cliente | No | SYNCED_E2E |
| Agendar cita con deteccion | Cliente | No | SYNCED_E2E |
| Programar recordatorios | Cliente | No | SYNCED_E2E |
| Confirmar suspension | Cliente | No | SYNCED_E2E |
| Seguimiento post-estudio | Cliente | No | SYNCED_E2E |
| Actualizar mini-cache | App update | Si (App Store) | LOCAL_ONLY |
| Sincronizar detecciones | Servidor | Si (background) | SYNCED_E2E |
| Sincronizar reglas usuario | Servidor | Si (background) | SYNCED_E2E |
3. Clasificacion de Datos¶
3.1. Tabla de Clasificacion¶
| Entidad/Campo | Clasificacion | PHI | Servidor Ve | Justificacion |
|---|---|---|---|---|
| cli_critical_med_study_cache | LOCAL_ONLY | No | N/A | Mini-cache embebido (<100 reglas criticas) |
| - medicamento_codigo | LOCAL_ONLY | No | N/A | Referencia a catalogo publico |
| - estudio_codigo | LOCAL_ONLY | No | N/A | Referencia a catalogo publico |
| - descripcion | LOCAL_ONLY | No | N/A | Texto educativo |
| cli_user_med_study_rules | SYNCED_E2E | Si | Blob cifrado | Reglas aplicables al usuario |
| - medicamento_usuario_codigo | SYNCED_E2E | Si | Blob cifrado | Referencia a medicamento del usuario |
| - estudio_categoria | SYNCED_E2E | Si | Blob cifrado | Contexto del usuario |
| cli_med_study_consent | SYNCED_E2E | Si | Blob cifrado | Consentimiento para busquedas |
| - consent_type | SYNCED_E2E | Si | Blob cifrado | AUTOMATIC o MANUAL |
| - consented_at | SYNCED_E2E | Si | Blob cifrado | Timestamp del consent |
| cli_detected_med_study | SYNCED_E2E | Si | Blob cifrado | Deteccion especifica del paciente |
| - evento_id | SYNCED_E2E | Si | Blob cifrado | Vincula a cita del paciente |
| - medicamento_usuario_id | SYNCED_E2E | Si | Blob cifrado | FK a medicamento del paciente |
| - medicamento_nombre | SYNCED_E2E | Si | Blob cifrado | Snapshot del medicamento |
| - estudio_nombre | SYNCED_E2E | Si | Blob cifrado | Snapshot del estudio |
| - usuario_confirmo | SYNCED_E2E | Si | Blob cifrado | Accion del paciente |
| cli_suspension_reminders | SYNCED_E2E | Si | Blob cifrado | Recordatorio personalizado |
| - titulo | SYNCED_E2E | Si | Blob cifrado | Contiene nombre medicamento |
| - mensaje | SYNCED_E2E | Si | Blob cifrado | Instrucciones personalizadas |
| cli_post_study_confirmations | SYNCED_E2E | Si | Blob cifrado | Seguimiento del paciente |
| - tuvo_reaccion | SYNCED_E2E | Si | Blob cifrado | Informacion medica |
| srv_med_study_catalog | SERVER_SOURCE | No | Todo | Datos publicos |
| srv_rules_updates | SERVER_SOURCE | No | Todo | Metadatos de actualizacion |
| srv_encrypted_detections | SYNCED_E2E | Si | Solo metadata | Blobs E2E del paciente |
3.2. Justificacion de Clasificaciones¶
LOCAL_ONLY - Mini-Cache Critico:
- Son <100 reglas mas criticas (suspensiones obligatorias)
- Datos publicos derivados de literatura medica
- No contienen informacion del paciente
- Embebidos en app, actualizan solo con app update
- NO se descargan por OTA (Regla del Director: max 100 registros)
SYNCED_E2E - Reglas del Usuario:
- Reglas aplicables a los medicamentos/estudios especificos del usuario
- Resultado de busquedas online (bajo demanda con consent)
- Contienen referencias a medicamentos del usuario (PHI)
- Servidor almacena cifrado, NO puede leer
- Se sincronizan entre dispositivos del usuario
SYNCED_E2E - Detecciones y Recordatorios:
- Contienen medicamentos especificos del paciente (PHI)
- Incluyen fechas de citas (PHI)
- Decisiones del paciente (confirmo, consulto medico)
- Seguimiento post-procedimiento (PHI)
- Servidor debe almacenar para sincronizacion entre dispositivos, pero NO puede leer
SERVER_SOURCE - Catalogo Maestro:
- Origen: Literatura medica curada
- Publico: Cualquier profesional puede acceder
- Usado para construir base local del cliente
- Sin informacion del paciente
4. Esquema Cliente (Conceptual)¶
4.1. cli_critical_med_study_cache (Mini-Cache Critico Embebido)¶
Proposito: Mini-cache de <100 reglas mas criticas (suspensiones obligatorias). 100% offline. Embebido en app.
REGLA DEL DIRECTOR: Maximo <100 registros. NO se descarga por OTA.
| Campo | Tipo | Descripcion |
|---|---|---|
| id | UUID | PK |
| medicamento_codigo | String | Codigo en catalogo medicamentos |
| principio_activo | String | Nombre generico (busqueda) |
| categoria_terapeutica | String | ATC nivel 2 (busqueda fallback) |
| estudio_codigo | String | Codigo en catalogo estudios |
| estudio_categoria | Enum | LAB, IMG, ESP, TRT |
| estudio_subcategoria | String | QUI, TC, CAR, etc |
| tipo_interaccion | Enum | AFE, SUS, INC, POS |
| severidad | Enum | CRITICO(3), IMPORTANTE(2), INFORMATIVO(1) |
| nivel_evidencia | Enum | ALTA, MEDIA, BAJA |
| descripcion | Text | Descripcion para paciente |
| mecanismo | Text | Mecanismo de interaccion |
| efecto | Text | Efecto esperado |
| accion | Enum | SUSPENDER, AJUSTAR, INFORMAR, MONITOREAR |
| tiempo_antes_horas | Integer | Horas antes del estudio |
| tiempo_despues_horas | Integer | Horas despues para reiniciar |
| instrucciones_especiales | Text | Instrucciones adicionales |
| aplica_si_dosis_mayor | Decimal | Aplica solo si dosis > X (nullable) |
| aplica_si_funcion_renal_menor | Integer | Aplica solo si eGFR < X (nullable) |
| excepciones | Text | Excepciones documentadas |
| fuente_primaria | String | Fuente principal (FDA, CHEST, etc) |
| referencias | JSON Array | Array de referencias |
| fecha_evidencia | Date | Fecha de evidencia |
| activa | Boolean | Regla activa |
| version | Integer | Version de la regla |
| created_at | Timestamp | Fecha creacion |
| updated_at | Timestamp | Ultima actualizacion |
Indices conceptuales:
(principio_activo, estudio_categoria)- Busqueda primaria(categoria_terapeutica, estudio_categoria)- Busqueda fallback(medicamento_codigo, estudio_codigo)- Busqueda exacta(severidad, tipo_interaccion)- Filtrado
Criterios de inclusion en mini-cache (<100 reglas):
- Severidad = CRITICO (3)
- Accion = SUSPENDER o INCOMPATIBILIDAD
- Interacciones mas frecuentes en poblacion general
- Ejemplos: Anticoagulantes + cirugia, Metformina + contraste, Biotin + labs
4.2. cli_user_med_study_rules (Reglas del Usuario)¶
Proposito: Reglas aplicables a los medicamentos/estudios especificos del usuario. Resultado de busquedas online.
Clasificacion: SYNCED_E2E (PHI)
| Campo | Tipo | Descripcion |
|---|---|---|
| id | UUID | PK |
| rule_source_id | UUID | FK a srv_med_study_catalog |
| medicamento_usuario_id | UUID | FK a medicamento del usuario |
| medicamento_codigo | String | Codigo del medicamento (cifrado) |
| principio_activo | String | Nombre generico (cifrado) |
| estudio_codigo | String | Codigo del estudio (cifrado) |
| estudio_categoria | Enum | LAB, IMG, ESP, TRT (cifrado) |
| tipo_interaccion | Enum | AFE, SUS, INC, POS (cifrado) |
| severidad | Enum | CRITICO, IMPORTANTE, INFORMATIVO |
| descripcion | Text | Descripcion (cifrado) |
| accion | Enum | SUSPENDER, AJUSTAR, INFORMAR, MONITOREAR |
| tiempo_antes_horas | Integer | Horas antes del estudio |
| tiempo_despues_horas | Integer | Horas despues para reiniciar |
| instrucciones_especiales | Text | Instrucciones (cifrado) |
| fecha_obtenida | Timestamp | Cuando se obtuvo del servidor |
| sync_status | Enum | LOCAL, PENDING, SYNCED |
| created_at | Timestamp | Fecha creacion |
| updated_at | Timestamp | Ultima actualizacion |
Indices conceptuales:
(medicamento_usuario_id)- Por medicamento del usuario(estudio_categoria)- Por tipo de estudio(severidad)- Por severidad
4.3. cli_med_study_consent (Consentimiento Disclaimer)¶
Proposito: Registro de consentimiento del usuario para realizar busquedas online de interacciones.
Clasificacion: SYNCED_E2E (PHI - parte del historial medico)
| Campo | Tipo | Descripcion |
|---|---|---|
| id | UUID | PK |
| user_id | UUID | FK a usuario (nullable para local-first) |
| consent_type | Enum | AUTOMATIC, MANUAL |
| consent_version | String | Version del disclaimer (ej: "1.0") |
| consented_at | Timestamp | Cuando dio consent |
| disclaimer_shown | Boolean | Se mostro disclaimer completo |
| disclaimer_accepted | Boolean | Usuario acepto explicitamente |
| ip_address | String | IP cuando acepto (opcional, cifrado) |
| user_agent | String | User agent (opcional, cifrado) |
| sync_status | Enum | LOCAL, PENDING, SYNCED |
| created_at | Timestamp | Fecha creacion |
Indices conceptuales:
(user_id, consent_version)- Buscar consent por version(consented_at)- Por fecha
4.4. cli_detected_med_study (Interacciones Detectadas)¶
Proposito: Registro de interacciones detectadas para un evento/cita especifica del paciente.
Clasificacion: SYNCED_E2E (PHI)
| Campo | Tipo | Descripcion |
|---|---|---|
| id | UUID | PK |
| evento_id | UUID | FK a EventoMedico (cita) |
| interaccion_id | UUID | FK a cli_med_study_rules |
| medicamento_usuario_id | UUID | FK a medicamento del paciente |
| medicamento_nombre | String | Snapshot cifrado del medicamento |
| medicamento_dosis | String | Snapshot cifrado de dosis |
| estudio_nombre | String | Snapshot cifrado del estudio |
| estudio_fecha | DateTime | Fecha de la cita (cifrado) |
| severidad | Enum | CRITICO, IMPORTANTE, INFORMATIVO |
| tipo_interaccion | Enum | AFE, SUS, INC, POS |
| accion_requerida | Text | Accion especifica (cifrado) |
| tiempo_antes_horas | Integer | Horas calculadas antes |
| tiempo_despues_horas | Integer | Horas calculadas despues |
| fecha_ultima_dosis | DateTime | Calculado automaticamente |
| fecha_reinicio | DateTime | Calculado automaticamente |
| usuario_confirmo | Boolean | Usuario vio y confirmo |
| fecha_confirmacion | Timestamp | Cuando confirmo |
| usuario_consulto_medico | Boolean | Indico que consulto |
| medicamento_suspendido | Boolean | Usuario marco suspension |
| fecha_suspension | Timestamp | Cuando suspendio |
| medicamento_reiniciado | Boolean | Usuario marco reinicio |
| fecha_reinicio_real | Timestamp | Cuando reinicio |
| notas_seguimiento | Text | Notas del paciente (cifrado) |
| sync_status | Enum | LOCAL, PENDING, SYNCED |
| sync_version | Integer | Version de sincronizacion |
| created_at | Timestamp | Fecha deteccion |
| updated_at | Timestamp | Ultima modificacion |
Indices conceptuales:
(evento_id)- Buscar por evento(medicamento_usuario_id)- Buscar por medicamento(estudio_fecha)- Buscar por fecha(severidad, tipo_interaccion)- Filtrar criticas
4.5. cli_suspension_reminders (Recordatorios de Suspension)¶
Proposito: Recordatorios programados para suspensiones de medicamentos.
Clasificacion: SYNCED_E2E (PHI)
| Campo | Tipo | Descripcion |
|---|---|---|
| id | UUID | PK |
| interaccion_evento_id | UUID | FK a cli_detected_med_study |
| tipo | Enum | PREVIO, DIA_SUSPENSION, PRE_ESTUDIO, POST_ESTUDIO, REINICIO |
| fecha_hora_programada | Timestamp | Cuando enviar |
| enviado | Boolean | Ya se envio |
| fecha_envio | Timestamp | Cuando se envio |
| titulo | String | Titulo del recordatorio (cifrado) |
| mensaje | Text | Mensaje completo (cifrado) |
| usuario_confirmo | Boolean | Usuario marco como leido |
| fecha_confirmacion | Timestamp | Cuando confirmo |
| notification_id | String | ID de notificacion local (OS) |
| sync_status | Enum | LOCAL, PENDING, SYNCED |
| created_at | Timestamp | Fecha creacion |
Indices conceptuales:
(interaccion_evento_id)- Por interaccion(fecha_hora_programada, enviado)- Pendientes(tipo)- Por tipo
4.6. cli_post_study_confirmations (Confirmaciones Post-Estudio)¶
Proposito: Seguimiento post-estudio para verificar reinicio seguro.
Clasificacion: SYNCED_E2E (PHI)
| Campo | Tipo | Descripcion |
|---|---|---|
| id | UUID | PK |
| interaccion_evento_id | UUID | FK a cli_detected_med_study |
| fecha_estudio_realizado | DateTime | Fecha del procedimiento |
| tuvo_reaccion | Boolean | Reporto reaccion adversa |
| descripcion_reaccion | Text | Descripcion (cifrado) |
| creatinina_verificada | Boolean | Para metformina/contraste |
| valor_creatinina | Decimal | Valor reportado (cifrado) |
| medico_autorizo_reinicio | Boolean | Medico dio OK |
| fecha_autorizacion | Timestamp | Cuando autorizo |
| medicamento_reiniciado | Boolean | Ya reinicio |
| fecha_reinicio | Timestamp | Cuando reinicio |
| notas | Text | Notas adicionales (cifrado) |
| sync_status | Enum | LOCAL, PENDING, SYNCED |
| created_at | Timestamp | Fecha creacion |
| updated_at | Timestamp | Ultima modificacion |
Indices conceptuales:
(interaccion_evento_id)- Por interaccion(fecha_estudio_realizado)- Por fecha(medicamento_reiniciado)- Pendientes de reinicio
5. Esquema Servidor (Conceptual)¶
5.1. srv_med_study_catalog (Catalogo Maestro)¶
Proposito: Catalogo completo de reglas de interaccion mantenido por MedTime.
Clasificacion: SERVER_SOURCE (publico)
| Campo | Tipo | Descripcion |
|---|---|---|
| id | UUID | PK |
| medicamento_codigo | String | Codigo catalogo |
| principio_activo | String | Nombre generico |
| categoria_terapeutica | String | ATC nivel 2 |
| estudio_codigo | String | Codigo catalogo estudios |
| estudio_categoria | String | LAB, IMG, ESP, TRT |
| estudio_subcategoria | String | Subcategoria |
| tipo_interaccion | String | AFE, SUS, INC, POS |
| severidad | Integer | 3=CRITICO, 2=IMPORTANTE, 1=INFORMATIVO |
| nivel_evidencia | String | ALTA, MEDIA, BAJA |
| descripcion | Text | Texto para paciente |
| mecanismo | Text | Mecanismo |
| efecto | Text | Efecto esperado |
| accion | String | SUSPENDER, AJUSTAR, etc |
| tiempo_antes_horas | Integer | Horas antes |
| tiempo_despues_horas | Integer | Horas despues |
| instrucciones_especiales | Text | Instrucciones |
| condiciones | JSONB | Condiciones de aplicacion |
| fuentes | JSONB | Referencias y fuentes |
| fecha_evidencia | Date | Fecha evidencia |
| tier_minimo | String | FREE, PRO, PERFECT |
| activa | Boolean | Activa |
| version | Integer | Version |
| created_at | Timestamp | Creacion |
| updated_at | Timestamp | Actualizacion |
Indices:
(principio_activo, estudio_categoria)BTREE(categoria_terapeutica, estudio_categoria)BTREE(severidad, tipo_interaccion)BTREE(tier_minimo)BTREE(activa)BTREE
5.2. srv_rules_updates (Actualizaciones Mini-Cache)¶
Proposito: Registro de actualizaciones del mini-cache critico (<100 reglas). Se distribuye via app update.
Clasificacion: SERVER_SOURCE (metadata publica)
NOTA: Ya NO es para OTA masivo de 500-1000+ reglas. Solo para el mini-cache de <100 reglas criticas.
| Campo | Tipo | Descripcion |
|---|---|---|
| id | UUID | PK |
| version | String | Version (ej: "2025.12.1") |
| tipo | String | MINI_CACHE_UPDATE |
| reglas_agregadas | Integer | Cantidad nuevas (debe mantener total <100) |
| reglas_modificadas | Integer | Cantidad modificadas |
| reglas_eliminadas | Integer | Cantidad eliminadas |
| total_reglas | Integer | Total de reglas en mini-cache (MUST be <100) |
| delta_json | JSONB | Delta de cambios |
| hash_sha256 | String | Hash del delta |
| tamano_bytes | Integer | Tamaño del delta |
| fecha_publicacion | Timestamp | Cuando se publico |
| app_version_minima | String | Version minima de app (ej: "1.2.0") |
| activa | Boolean | Disponible |
| created_at | Timestamp | Creacion |
Indices:
(version)BTREE UNIQUE(fecha_publicacion)BTREE(activa)BTREE
Constraint:
5.3. srv_encrypted_user_rules (Sincronizacion E2E - Reglas Usuario)¶
Proposito: Blobs cifrados de reglas del usuario para sincronizacion entre dispositivos.
Clasificacion: SYNCED_E2E (servidor NO puede leer)
| Campo | Tipo | Descripcion |
|---|---|---|
| id | UUID | PK, mismo que cliente |
| user_id | UUID | FK a users (RLS) |
| entity_type | String | 'user_med_study_rule' |
| encrypted_blob | BYTEA | Blob cifrado E2E |
| blob_hash | String | SHA-256 para integridad |
| blob_size_bytes | Integer | Tamaño |
| encryption_version | String | Version cifrado (ej: "1.0") |
| sync_version | BigInt | Version sincronizacion |
| created_at | Timestamp | Creacion |
| updated_at | Timestamp | Actualizacion |
Indices:
(user_id, entity_type)BTREE(user_id, sync_version)BTREE(blob_hash)BTREE
5.4. srv_encrypted_detections (Sincronizacion E2E - Detecciones)¶
Proposito: Blobs cifrados de detecciones para sincronizacion entre dispositivos.
Clasificacion: SYNCED_E2E (servidor NO puede leer)
| Campo | Tipo | Descripcion |
|---|---|---|
| id | UUID | PK, mismo que cliente |
| user_id | UUID | FK a users (RLS) |
| entity_type | String | 'med_study_detection' |
| encrypted_blob | BYTEA | Blob cifrado E2E |
| blob_hash | String | SHA-256 para integridad |
| blob_size_bytes | Integer | Tamaño |
| encryption_version | String | Version cifrado (ej: "1.0") |
| sync_version | BigInt | Version sincronizacion |
| created_at | Timestamp | Creacion |
| updated_at | Timestamp | Actualizacion |
Indices:
(user_id, entity_type)BTREE(user_id, sync_version)BTREE(blob_hash)BTREE
6. Implementacion iOS (Swift/Realm)¶
6.1. MedStudyRule¶
// ============================================================
// MODELO: MedStudyRule
// Descripcion: Base de reglas de interacciones medicamento-estudio
// Almacenamiento: Realm cifrado
// Sync: NO - LOCAL_ONLY (actualiza por OTA)
// Clasificacion: LOCAL_ONLY (datos publicos)
// ============================================================
import RealmSwift
class MedStudyRule: Object, Identifiable {
@Persisted(primaryKey: true) var id: String = UUID().uuidString
// Medicamento (busqueda)
@Persisted var medicamentoCodigo: String = ""
@Persisted(indexed: true) var principioActivo: String = ""
@Persisted(indexed: true) var categoriaTerapeutica: String = ""
// Estudio (busqueda)
@Persisted var estudioCodigo: String = ""
@Persisted(indexed: true) var estudioCategoria: String = "" // LAB, IMG, ESP, TRT
@Persisted var estudioSubcategoria: String = ""
// Tipo de interaccion
@Persisted var tipoInteraccion: String = "" // AFE, SUS, INC, POS
// Clasificacion
@Persisted(indexed: true) var severidad: Int = 1 // 3=CRITICO, 2=IMPORTANTE, 1=INFORMATIVO
@Persisted var nivelEvidencia: String = "" // ALTA, MEDIA, BAJA
// Contenido
@Persisted var titulo: String = ""
@Persisted var descripcion: String = ""
@Persisted var mecanismo: String = ""
@Persisted var efecto: String = ""
// Accion
@Persisted var accion: String = "" // SUSPENDER, AJUSTAR, INFORMAR, MONITOREAR
@Persisted var tiempoAntesHoras: Int = 0
@Persisted var tiempoDespuesHoras: Int = 0
@Persisted var instruccionesEspeciales: String?
// Condiciones (nullable)
@Persisted var aplicaSiDosisMayor: Double?
@Persisted var aplicaSiFuncionRenalMenor: Int?
@Persisted var excepciones: String?
// Fuentes
@Persisted var fuentePrimaria: String = ""
@Persisted var referencias: List<String>
@Persisted var fechaEvidencia: Date = Date()
// Metadata
@Persisted var activa: Bool = true
@Persisted var version: Int = 1
@Persisted var createdAt: Date = Date()
@Persisted var updatedAt: Date = Date()
// Computed
var esCritica: Bool {
return severidad == 3
}
var requiereSuspension: Bool {
return tipoInteraccion == "SUS" || tipoInteraccion == "INC"
}
}
// Extension: Busqueda de interacciones
extension MedStudyRule {
static func buscarInteracciones(
in realm: Realm,
principioActivo: String,
estudioCategoria: String,
estudioSubcategoria: String? = nil
) -> Results<MedStudyRule> {
var query = realm.objects(MedStudyRule.self)
.filter("principioActivo == %@ AND estudioCategoria == %@ AND activa == true",
principioActivo, estudioCategoria)
if let subcategoria = estudioSubcategoria {
query = query.filter("estudioSubcategoria == %@", subcategoria)
}
return query.sorted(byKeyPath: "severidad", ascending: false)
}
static func buscarPorCategoriaTerapeutica(
in realm: Realm,
categoriaTerapeutica: String,
estudioCategoria: String
) -> Results<MedStudyRule> {
return realm.objects(MedStudyRule.self)
.filter("categoriaTerapeutica == %@ AND estudioCategoria == %@ AND activa == true",
categoriaTerapeutica, estudioCategoria)
.sorted(byKeyPath: "severidad", ascending: false)
}
}
6.2. DetectedMedStudyInteraction¶
// ============================================================
// MODELO: DetectedMedStudyInteraction
// Descripcion: Interaccion detectada para un evento especifico
// Almacenamiento: Realm cifrado
// Sync: SI - SYNCED_E2E (PHI)
// Clasificacion: SYNCED_E2E
// ============================================================
class DetectedMedStudyInteraction: Object, Identifiable {
@Persisted(primaryKey: true) var id: String = UUID().uuidString
// Referencias
@Persisted var eventoId: String = "" // FK a EventoMedico (cita)
@Persisted(indexed: true) var interaccionId: String = "" // FK a MedStudyRule
@Persisted(indexed: true) var medicamentoUsuarioId: String = "" // FK a medicamento
// Snapshot al momento de deteccion (PHI - cifrados E2E)
@Persisted var medicamentoNombre: String = ""
@Persisted var medicamentoDosis: String = ""
@Persisted var estudioNombre: String = ""
@Persisted(indexed: true) var estudioFecha: Date = Date()
// Clasificacion
@Persisted var severidad: Int = 1 // 3, 2, 1
@Persisted var tipoInteraccion: String = "" // AFE, SUS, INC, POS
@Persisted var accionRequerida: String = ""
// Tiempos calculados
@Persisted var tiempoAntesHoras: Int = 0
@Persisted var tiempoDespuesHoras: Int = 0
@Persisted var fechaUltimaDosis: Date?
@Persisted var fechaReinicio: Date?
// Confirmacion usuario
@Persisted var usuarioConfirmo: Bool = false
@Persisted var fechaConfirmacion: Date?
@Persisted var usuarioConsultoMedico: Bool = false
// Seguimiento de suspension
@Persisted var medicamentoSuspendido: Bool = false
@Persisted var fechaSuspension: Date?
@Persisted var medicamentoReiniciado: Bool = false
@Persisted var fechaReinicioReal: Date?
@Persisted var notasSeguimiento: String?
// Sincronizacion
@Persisted var syncStatus: String = "local" // local, pending, synced
@Persisted var syncVersion: Int = 1
@Persisted var createdAt: Date = Date()
@Persisted var updatedAt: Date = Date()
// Relaciones
var reminders: LinkingObjects<SuspensionReminder> {
LinkingObjects(fromType: SuspensionReminder.self, property: "interaccionEventoId")
}
// Computed
var esCritica: Bool {
return severidad == 3
}
var requiereAccionInmediata: Bool {
guard let fechaUltimaDosis = fechaUltimaDosis else { return false }
let diasRestantes = Calendar.current.dateComponents([.day], from: Date(), to: fechaUltimaDosis).day ?? 0
return diasRestantes <= 2 && !medicamentoSuspendido
}
}
// Extension: Queries
extension DetectedMedStudyInteraction {
static func porEvento(in realm: Realm, eventoId: String) -> Results<DetectedMedStudyInteraction> {
return realm.objects(DetectedMedStudyInteraction.self)
.filter("eventoId == %@", eventoId)
.sorted(byKeyPath: "severidad", ascending: false)
}
static func criticasSinConfirmar(in realm: Realm) -> Results<DetectedMedStudyInteraction> {
return realm.objects(DetectedMedStudyInteraction.self)
.filter("severidad == 3 AND usuarioConfirmo == false")
.sorted(byKeyPath: "estudioFecha", ascending: true)
}
static func pendientesSuspension(in realm: Realm) -> Results<DetectedMedStudyInteraction> {
let hoy = Date()
return realm.objects(DetectedMedStudyInteraction.self)
.filter("medicamentoSuspendido == false AND fechaUltimaDosis != nil AND fechaUltimaDosis >= %@", hoy)
.sorted(byKeyPath: "fechaUltimaDosis", ascending: true)
}
static func historialUltimos6Anos(in realm: Realm) -> Results<DetectedMedStudyInteraction> {
let cutoff = Calendar.current.date(byAdding: .year, value: -6, to: Date())!
return realm.objects(DetectedMedStudyInteraction.self)
.filter("createdAt >= %@", cutoff)
.sorted(byKeyPath: "estudioFecha", ascending: false)
}
}
6.3. SuspensionReminder¶
// ============================================================
// MODELO: SuspensionReminder
// Descripcion: Recordatorios programados de suspension
// Almacenamiento: Realm cifrado
// Sync: SI - SYNCED_E2E (PHI)
// Clasificacion: SYNCED_E2E
// ============================================================
class SuspensionReminder: Object, Identifiable {
@Persisted(primaryKey: true) var id: String = UUID().uuidString
// Referencia
@Persisted(indexed: true) var interaccionEventoId: String = ""
// Tipo de recordatorio
@Persisted(indexed: true) var tipo: String = ""
// PREVIO: 24h antes de iniciar suspension
// DIA_SUSPENSION: Dia de ultima dosis
// PRE_ESTUDIO: Dia antes del estudio
// POST_ESTUDIO: Dia del estudio (no tomar)
// REINICIO: Cuando puede reiniciar
// Programacion
@Persisted(indexed: true) var fechaHoraProgramada: Date = Date()
@Persisted var enviado: Bool = false
@Persisted var fechaEnvio: Date?
// Contenido (PHI - cifrado)
@Persisted var titulo: String = ""
@Persisted var mensaje: String = ""
// Respuesta usuario
@Persisted var usuarioConfirmo: Bool = false
@Persisted var fechaConfirmacion: Date?
// ID de notificacion local (iOS)
@Persisted var notificationId: String?
// Sincronizacion
@Persisted var syncStatus: String = "local"
@Persisted var createdAt: Date = Date()
}
// Extension: Gestion de recordatorios
extension SuspensionReminder {
static func pendientes(in realm: Realm) -> Results<SuspensionReminder> {
return realm.objects(SuspensionReminder.self)
.filter("enviado == false AND fechaHoraProgramada <= %@", Date())
.sorted(byKeyPath: "fechaHoraProgramada", ascending: true)
}
static func porInteraccion(in realm: Realm, interaccionId: String) -> Results<SuspensionReminder> {
return realm.objects(SuspensionReminder.self)
.filter("interaccionEventoId == %@", interaccionId)
.sorted(byKeyPath: "fechaHoraProgramada", ascending: true)
}
}
6.4. PostStudyConfirmation¶
// ============================================================
// MODELO: PostStudyConfirmation
// Descripcion: Confirmacion post-estudio para reinicio
// Almacenamiento: Realm cifrado
// Sync: SI - SYNCED_E2E (PHI)
// Clasificacion: SYNCED_E2E
// ============================================================
class PostStudyConfirmation: Object, Identifiable {
@Persisted(primaryKey: true) var id: String = UUID().uuidString
// Referencia
@Persisted(indexed: true) var interaccionEventoId: String = ""
// Fecha del estudio
@Persisted(indexed: true) var fechaEstudioRealizado: Date = Date()
// Verificaciones
@Persisted var tuvoReaccion: Bool = false
@Persisted var descripcionReaccion: String?
// Caso especial: Metformina + Contraste
@Persisted var creatininaVerificada: Bool = false
@Persisted var valorCreatinina: Double?
// Autorizacion medica
@Persisted var medicoAutorizoReinicio: Bool = false
@Persisted var fechaAutorizacion: Date?
// Reinicio
@Persisted var medicamentoReiniciado: Bool = false
@Persisted var fechaReinicio: Date?
@Persisted var notas: String?
// Sincronizacion
@Persisted var syncStatus: String = "local"
@Persisted var createdAt: Date = Date()
@Persisted var updatedAt: Date = Date()
// Computed
var puedeReiniciar: Bool {
// No tuvo reaccion Y (creatinina OK si aplica) Y medico autorizo
return !tuvoReaccion &&
(creatininaVerificada || valorCreatinina == nil) &&
medicoAutorizoReinicio
}
}
7. Implementacion Android (Kotlin/Room)¶
7.1. MedStudyRule¶
// ============================================================
// MODELO: MedStudyRule
// Descripcion: Base de reglas de interacciones
// Almacenamiento: Room cifrado (SQLCipher)
// Sync: NO - LOCAL_ONLY
// Clasificacion: LOCAL_ONLY (datos publicos)
// ============================================================
package com.medtime.data.entities
import androidx.room.*
import java.time.Instant
import java.util.UUID
@Entity(
tableName = "cli_med_study_rules",
indices = [
Index(value = ["principio_activo", "estudio_categoria"]),
Index(value = ["categoria_terapeutica", "estudio_categoria"]),
Index(value = ["severidad", "tipo_interaccion"]),
Index(value = ["activa"])
]
)
data class MedStudyRule(
@PrimaryKey
@ColumnInfo(name = "id")
val id: String = UUID.randomUUID().toString(),
// Medicamento
@ColumnInfo(name = "medicamento_codigo")
val medicamentoCodigo: String = "",
@ColumnInfo(name = "principio_activo")
val principioActivo: String = "",
@ColumnInfo(name = "categoria_terapeutica")
val categoriaTerapeutica: String = "",
// Estudio
@ColumnInfo(name = "estudio_codigo")
val estudioCodigo: String = "",
@ColumnInfo(name = "estudio_categoria")
val estudioCategoria: String = "", // LAB, IMG, ESP, TRT
@ColumnInfo(name = "estudio_subcategoria")
val estudioSubcategoria: String = "",
// Tipo
@ColumnInfo(name = "tipo_interaccion")
val tipoInteraccion: TipoInteraccionEstudio = TipoInteraccionEstudio.INFORMATIVO,
// Clasificacion
@ColumnInfo(name = "severidad")
val severidad: Int = 1, // 3=CRITICO, 2=IMPORTANTE, 1=INFORMATIVO
@ColumnInfo(name = "nivel_evidencia")
val nivelEvidencia: NivelEvidencia = NivelEvidencia.MEDIA,
// Contenido
@ColumnInfo(name = "titulo")
val titulo: String = "",
@ColumnInfo(name = "descripcion")
val descripcion: String = "",
@ColumnInfo(name = "mecanismo")
val mecanismo: String = "",
@ColumnInfo(name = "efecto")
val efecto: String = "",
// Accion
@ColumnInfo(name = "accion")
val accion: AccionRequerida = AccionRequerida.INFORMAR,
@ColumnInfo(name = "tiempo_antes_horas")
val tiempoAntesHoras: Int = 0,
@ColumnInfo(name = "tiempo_despues_horas")
val tiempoDespuesHoras: Int = 0,
@ColumnInfo(name = "instrucciones_especiales")
val instruccionesEspeciales: String? = null,
// Condiciones
@ColumnInfo(name = "aplica_si_dosis_mayor")
val aplicaSiDosisMayor: Double? = null,
@ColumnInfo(name = "aplica_si_funcion_renal_menor")
val aplicaSiFuncionRenalMenor: Int? = null,
@ColumnInfo(name = "excepciones")
val excepciones: String? = null,
// Fuentes
@ColumnInfo(name = "fuente_primaria")
val fuentePrimaria: String = "",
@ColumnInfo(name = "referencias_json")
val referenciasJson: String = "[]", // JSON array
@ColumnInfo(name = "fecha_evidencia")
val fechaEvidencia: Instant = Instant.now(),
// Metadata
@ColumnInfo(name = "activa")
val activa: Boolean = true,
@ColumnInfo(name = "version")
val version: Int = 1,
@ColumnInfo(name = "created_at")
val createdAt: Instant = Instant.now(),
@ColumnInfo(name = "updated_at")
val updatedAt: Instant = Instant.now()
) {
val esCritica: Boolean
get() = severidad == 3
val requiereSuspension: Boolean
get() = tipoInteraccion == TipoInteraccionEstudio.SUSPENSION ||
tipoInteraccion == TipoInteraccionEstudio.INCOMPATIBILIDAD
}
enum class TipoInteraccionEstudio {
AFECTA_RESULTADO, // AFE
SUSPENSION, // SUS
INCOMPATIBILIDAD, // INC
POST_RESTRICCION, // POS
INFORMATIVO
}
enum class NivelEvidencia {
ALTA, MEDIA, BAJA
}
enum class AccionRequerida {
SUSPENDER, AJUSTAR, INFORMAR, MONITOREAR
}
// DAO
@Dao
interface MedStudyRuleDao {
@Query("""
SELECT * FROM cli_med_study_rules
WHERE principio_activo = :principioActivo
AND estudio_categoria = :categoria
AND activa = 1
ORDER BY severidad DESC
""")
suspend fun buscarPorPrincipioActivo(
principioActivo: String,
categoria: String
): List<MedStudyRule>
@Query("""
SELECT * FROM cli_med_study_rules
WHERE categoria_terapeutica = :categoria
AND estudio_categoria = :estudioCategoria
AND activa = 1
ORDER BY severidad DESC
""")
suspend fun buscarPorCategoriaTerapeutica(
categoria: String,
estudioCategoria: String
): List<MedStudyRule>
@Query("""
SELECT * FROM cli_med_study_rules
WHERE severidad = 3 AND activa = 1
""")
suspend fun obtenerCriticas(): List<MedStudyRule>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertar(rule: MedStudyRule)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertarBatch(rules: List<MedStudyRule>)
@Query("DELETE FROM cli_med_study_rules WHERE version < :version")
suspend fun eliminarViejasVersiones(version: Int)
}
7.2. DetectedMedStudyInteraction¶
// ============================================================
// MODELO: DetectedMedStudyInteraction
// Descripcion: Interaccion detectada por evento
// Almacenamiento: Room cifrado
// Sync: SI - SYNCED_E2E (PHI)
// Clasificacion: SYNCED_E2E
// ============================================================
@Entity(
tableName = "cli_detected_med_study",
indices = [
Index(value = ["evento_id"]),
Index(value = ["medicamento_usuario_id"]),
Index(value = ["estudio_fecha"]),
Index(value = ["severidad", "tipo_interaccion"])
],
foreignKeys = [
ForeignKey(
entity = EventoMedico::class,
parentColumns = ["id"],
childColumns = ["evento_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class DetectedMedStudyInteraction(
@PrimaryKey
@ColumnInfo(name = "id")
val id: String = UUID.randomUUID().toString(),
// Referencias
@ColumnInfo(name = "evento_id")
val eventoId: String = "",
@ColumnInfo(name = "interaccion_id")
val interaccionId: String = "",
@ColumnInfo(name = "medicamento_usuario_id")
val medicamentoUsuarioId: String = "",
// Snapshot (PHI - cifrado E2E)
@ColumnInfo(name = "medicamento_nombre")
val medicamentoNombre: String = "",
@ColumnInfo(name = "medicamento_dosis")
val medicamentoDosis: String = "",
@ColumnInfo(name = "estudio_nombre")
val estudioNombre: String = "",
@ColumnInfo(name = "estudio_fecha")
val estudioFecha: Instant = Instant.now(),
// Clasificacion
@ColumnInfo(name = "severidad")
val severidad: Int = 1,
@ColumnInfo(name = "tipo_interaccion")
val tipoInteraccion: String = "",
@ColumnInfo(name = "accion_requerida")
val accionRequerida: String = "",
// Tiempos
@ColumnInfo(name = "tiempo_antes_horas")
val tiempoAntesHoras: Int = 0,
@ColumnInfo(name = "tiempo_despues_horas")
val tiempoDespuesHoras: Int = 0,
@ColumnInfo(name = "fecha_ultima_dosis")
val fechaUltimaDosis: Instant? = null,
@ColumnInfo(name = "fecha_reinicio")
val fechaReinicio: Instant? = null,
// Confirmacion
@ColumnInfo(name = "usuario_confirmo")
val usuarioConfirmo: Boolean = false,
@ColumnInfo(name = "fecha_confirmacion")
val fechaConfirmacion: Instant? = null,
@ColumnInfo(name = "usuario_consulto_medico")
val usuarioConsultoMedico: Boolean = false,
// Seguimiento
@ColumnInfo(name = "medicamento_suspendido")
val medicamentoSuspendido: Boolean = false,
@ColumnInfo(name = "fecha_suspension")
val fechaSuspension: Instant? = null,
@ColumnInfo(name = "medicamento_reiniciado")
val medicamentoReiniciado: Boolean = false,
@ColumnInfo(name = "fecha_reinicio_real")
val fechaReinicioReal: Instant? = null,
@ColumnInfo(name = "notas_seguimiento")
val notasSeguimiento: String? = null,
// Sincronizacion
@ColumnInfo(name = "sync_status")
val syncStatus: String = "local",
@ColumnInfo(name = "sync_version")
val syncVersion: Int = 1,
@ColumnInfo(name = "created_at")
val createdAt: Instant = Instant.now(),
@ColumnInfo(name = "updated_at")
val updatedAt: Instant = Instant.now()
)
// DAO
@Dao
interface DetectedMedStudyDao {
@Query("""
SELECT * FROM cli_detected_med_study
WHERE evento_id = :eventoId
ORDER BY severidad DESC
""")
suspend fun porEvento(eventoId: String): List<DetectedMedStudyInteraction>
@Query("""
SELECT * FROM cli_detected_med_study
WHERE severidad = 3 AND usuario_confirmo = 0
ORDER BY estudio_fecha ASC
""")
suspend fun criticasSinConfirmar(): List<DetectedMedStudyInteraction>
@Query("""
SELECT * FROM cli_detected_med_study
WHERE medicamento_suspendido = 0
AND fecha_ultima_dosis IS NOT NULL
AND fecha_ultima_dosis >= :hoy
ORDER BY fecha_ultima_dosis ASC
""")
suspend fun pendientesSuspension(hoy: Instant): List<DetectedMedStudyInteraction>
@Query("""
SELECT * FROM cli_detected_med_study
WHERE created_at >= :cutoff
ORDER BY estudio_fecha DESC
""")
suspend fun historial6Anos(cutoff: Instant): List<DetectedMedStudyInteraction>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertar(detection: DetectedMedStudyInteraction)
@Update
suspend fun actualizar(detection: DetectedMedStudyInteraction)
}
7.3. SuspensionReminder¶
// ============================================================
// MODELO: SuspensionReminder
// Descripcion: Recordatorios de suspension
// Almacenamiento: Room cifrado
// Sync: SI - SYNCED_E2E (PHI)
// Clasificacion: SYNCED_E2E
// ============================================================
@Entity(
tableName = "cli_suspension_reminders",
indices = [
Index(value = ["interaccion_evento_id"]),
Index(value = ["fecha_hora_programada", "enviado"]),
Index(value = ["tipo"])
]
)
data class SuspensionReminder(
@PrimaryKey
@ColumnInfo(name = "id")
val id: String = UUID.randomUUID().toString(),
@ColumnInfo(name = "interaccion_evento_id")
val interaccionEventoId: String = "",
@ColumnInfo(name = "tipo")
val tipo: TipoRecordatorio = TipoRecordatorio.PREVIO,
@ColumnInfo(name = "fecha_hora_programada")
val fechaHoraProgramada: Instant = Instant.now(),
@ColumnInfo(name = "enviado")
val enviado: Boolean = false,
@ColumnInfo(name = "fecha_envio")
val fechaEnvio: Instant? = null,
// Contenido (PHI - cifrado)
@ColumnInfo(name = "titulo")
val titulo: String = "",
@ColumnInfo(name = "mensaje")
val mensaje: String = "",
@ColumnInfo(name = "usuario_confirmo")
val usuarioConfirmo: Boolean = false,
@ColumnInfo(name = "fecha_confirmacion")
val fechaConfirmacion: Instant? = null,
@ColumnInfo(name = "notification_id")
val notificationId: String? = null,
@ColumnInfo(name = "sync_status")
val syncStatus: String = "local",
@ColumnInfo(name = "created_at")
val createdAt: Instant = Instant.now()
)
enum class TipoRecordatorio {
PREVIO, // 24h antes de iniciar suspension
DIA_SUSPENSION, // Dia de ultima dosis
PRE_ESTUDIO, // Dia antes del estudio
POST_ESTUDIO, // Dia del estudio
REINICIO // Cuando puede reiniciar
}
@Dao
interface SuspensionReminderDao {
@Query("""
SELECT * FROM cli_suspension_reminders
WHERE enviado = 0 AND fecha_hora_programada <= :ahora
ORDER BY fecha_hora_programada ASC
""")
suspend fun pendientes(ahora: Instant): List<SuspensionReminder>
@Query("""
SELECT * FROM cli_suspension_reminders
WHERE interaccion_evento_id = :interaccionId
ORDER BY fecha_hora_programada ASC
""")
suspend fun porInteraccion(interaccionId: String): List<SuspensionReminder>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertar(reminder: SuspensionReminder)
@Update
suspend fun actualizar(reminder: SuspensionReminder)
}
7.4. PostStudyConfirmation¶
// ============================================================
// MODELO: PostStudyConfirmation
// Descripcion: Confirmacion post-estudio
// Almacenamiento: Room cifrado
// Sync: SI - SYNCED_E2E (PHI)
// Clasificacion: SYNCED_E2E
// ============================================================
@Entity(
tableName = "cli_post_study_confirmations",
indices = [
Index(value = ["interaccion_evento_id"]),
Index(value = ["fecha_estudio_realizado"]),
Index(value = ["medicamento_reiniciado"])
]
)
data class PostStudyConfirmation(
@PrimaryKey
@ColumnInfo(name = "id")
val id: String = UUID.randomUUID().toString(),
@ColumnInfo(name = "interaccion_evento_id")
val interaccionEventoId: String = "",
@ColumnInfo(name = "fecha_estudio_realizado")
val fechaEstudioRealizado: Instant = Instant.now(),
// Verificaciones
@ColumnInfo(name = "tuvo_reaccion")
val tuvoReaccion: Boolean = false,
@ColumnInfo(name = "descripcion_reaccion")
val descripcionReaccion: String? = null,
// Caso especial: Metformina + Contraste
@ColumnInfo(name = "creatinina_verificada")
val creatininaVerificada: Boolean = false,
@ColumnInfo(name = "valor_creatinina")
val valorCreatinina: Double? = null,
// Autorizacion
@ColumnInfo(name = "medico_autorizo_reinicio")
val medicoAutorizoReinicio: Boolean = false,
@ColumnInfo(name = "fecha_autorizacion")
val fechaAutorizacion: Instant? = null,
// Reinicio
@ColumnInfo(name = "medicamento_reiniciado")
val medicamentoReiniciado: Boolean = false,
@ColumnInfo(name = "fecha_reinicio")
val fechaReinicio: Instant? = null,
@ColumnInfo(name = "notas")
val notas: String? = null,
@ColumnInfo(name = "sync_status")
val syncStatus: String = "local",
@ColumnInfo(name = "created_at")
val createdAt: Instant = Instant.now(),
@ColumnInfo(name = "updated_at")
val updatedAt: Instant = Instant.now()
) {
val puedeReiniciar: Boolean
get() = !tuvoReaccion &&
(creatininaVerificada || valorCreatinina == null) &&
medicoAutorizoReinicio
}
@Dao
interface PostStudyConfirmationDao {
@Query("""
SELECT * FROM cli_post_study_confirmations
WHERE interaccion_evento_id = :interaccionId
""")
suspend fun porInteraccion(interaccionId: String): PostStudyConfirmation?
@Query("""
SELECT * FROM cli_post_study_confirmations
WHERE medicamento_reiniciado = 0
ORDER BY fecha_estudio_realizado DESC
""")
suspend fun pendientesReinicio(): List<PostStudyConfirmation>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertar(confirmation: PostStudyConfirmation)
@Update
suspend fun actualizar(confirmation: PostStudyConfirmation)
}
8. Esquema Servidor (PostgreSQL con RLS)¶
8.1. srv_med_study_catalog¶
-- ============================================================
-- TABLA: srv_med_study_catalog
-- Descripcion: Catalogo maestro de interacciones medicamento-estudio
-- Clasificacion: SERVER_SOURCE (publico)
-- RLS: No aplica (datos publicos)
-- ============================================================
CREATE TABLE srv_med_study_catalog (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Medicamento (busqueda)
medicamento_codigo VARCHAR(50) NOT NULL,
principio_activo VARCHAR(255) NOT NULL,
categoria_terapeutica VARCHAR(100) NOT NULL,
-- Estudio (busqueda)
estudio_codigo VARCHAR(50) NOT NULL,
estudio_categoria VARCHAR(10) NOT NULL, -- LAB, IMG, ESP, TRT
estudio_subcategoria VARCHAR(50),
-- Tipo de interaccion
tipo_interaccion VARCHAR(10) NOT NULL, -- AFE, SUS, INC, POS
-- Clasificacion
severidad INTEGER NOT NULL CHECK (severidad IN (1, 2, 3)),
nivel_evidencia VARCHAR(10) NOT NULL, -- ALTA, MEDIA, BAJA
-- Contenido
titulo VARCHAR(500) NOT NULL,
descripcion TEXT NOT NULL,
mecanismo TEXT,
efecto TEXT,
-- Accion
accion VARCHAR(20) NOT NULL, -- SUSPENDER, AJUSTAR, INFORMAR, MONITOREAR
tiempo_antes_horas INTEGER DEFAULT 0,
tiempo_despues_horas INTEGER DEFAULT 0,
instrucciones_especiales TEXT,
-- Condiciones (JSONB para flexibilidad)
condiciones JSONB,
/*
Ejemplo:
{
"aplica_si_dosis_mayor": 5000,
"aplica_si_funcion_renal_menor": 60,
"excepciones": "No aplica si paciente en dialisis"
}
*/
-- Fuentes (JSONB)
fuentes JSONB NOT NULL,
/*
Ejemplo:
{
"primaria": "FDA Safety Communication 2019",
"referencias": [
"PMID: 12345678",
"https://..."
],
"fecha_evidencia": "2019-06-15"
}
*/
-- Metadata
tier_minimo VARCHAR(10) NOT NULL DEFAULT 'FREE', -- FREE, PRO, PERFECT
activa BOOLEAN NOT NULL DEFAULT TRUE,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT unique_med_study UNIQUE (medicamento_codigo, estudio_codigo, version)
);
-- Indices
CREATE INDEX idx_catalog_principio_activo ON srv_med_study_catalog(principio_activo, estudio_categoria) WHERE activa = TRUE;
CREATE INDEX idx_catalog_categoria_terapeutica ON srv_med_study_catalog(categoria_terapeutica, estudio_categoria) WHERE activa = TRUE;
CREATE INDEX idx_catalog_severidad ON srv_med_study_catalog(severidad, tipo_interaccion) WHERE activa = TRUE;
CREATE INDEX idx_catalog_tier ON srv_med_study_catalog(tier_minimo) WHERE activa = TRUE;
CREATE INDEX idx_catalog_version ON srv_med_study_catalog(version, updated_at DESC);
-- Comentarios
COMMENT ON TABLE srv_med_study_catalog IS 'Catalogo maestro de interacciones medicamento-estudio (publico)';
COMMENT ON COLUMN srv_med_study_catalog.condiciones IS 'JSON con condiciones de aplicacion';
COMMENT ON COLUMN srv_med_study_catalog.fuentes IS 'JSON con fuentes y referencias';
COMMENT ON COLUMN srv_med_study_catalog.tier_minimo IS 'Tier minimo requerido para acceder a esta regla';
8.2. srv_rules_updates¶
-- ============================================================
-- TABLA: srv_rules_updates
-- Descripcion: Registro de actualizaciones OTA incrementales
-- Clasificacion: SERVER_SOURCE (metadata publica)
-- RLS: No aplica (datos publicos)
-- ============================================================
CREATE TABLE srv_rules_updates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Version
version VARCHAR(20) NOT NULL, -- Ej: "2025.12.1"
tier VARCHAR(10) NOT NULL, -- FREE, PRO, PERFECT
tipo VARCHAR(15) NOT NULL, -- FULL, INCREMENTAL
-- Estadisticas
reglas_agregadas INTEGER NOT NULL DEFAULT 0,
reglas_modificadas INTEGER NOT NULL DEFAULT 0,
reglas_eliminadas INTEGER NOT NULL DEFAULT 0,
-- Delta (JSONB comprimido)
delta_json JSONB NOT NULL,
/*
Ejemplo:
{
"agregadas": [
{ "id": "...", "medicamento_codigo": "...", ... }
],
"modificadas": [
{ "id": "...", "campos_modificados": {...} }
],
"eliminadas": ["id1", "id2"]
}
*/
-- Integridad
hash_sha256 VARCHAR(64) NOT NULL,
tamano_bytes INTEGER NOT NULL,
-- Metadata
fecha_publicacion TIMESTAMPTZ NOT NULL DEFAULT NOW(),
activa BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT unique_version_tier UNIQUE (version, tier)
);
-- Indices
CREATE INDEX idx_updates_version ON srv_rules_updates(version, tier);
CREATE INDEX idx_updates_fecha ON srv_rules_updates(fecha_publicacion DESC) WHERE activa = TRUE;
CREATE INDEX idx_updates_tier ON srv_rules_updates(tier, activa);
-- Comentarios
COMMENT ON TABLE srv_rules_updates IS 'Registro de actualizaciones OTA de reglas (Pro/Perfect)';
COMMENT ON COLUMN srv_rules_updates.delta_json IS 'Delta de cambios en formato JSON';
COMMENT ON COLUMN srv_rules_updates.hash_sha256 IS 'Hash SHA-256 del delta para verificacion de integridad';
8.3. srv_encrypted_detections¶
-- ============================================================
-- TABLA: srv_encrypted_detections
-- Descripcion: Blobs cifrados E2E de detecciones de interacciones
-- Clasificacion: SYNCED_E2E (servidor NO puede leer)
-- RLS: SI - Solo el usuario puede acceder a sus datos
-- ============================================================
CREATE TABLE srv_encrypted_detections (
id UUID PRIMARY KEY, -- Mismo ID que cliente
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Tipo de entidad
entity_type VARCHAR(50) NOT NULL DEFAULT 'med_study_detection',
-- Blob cifrado E2E (OPACO para servidor)
encrypted_blob BYTEA NOT NULL,
blob_hash VARCHAR(64) NOT NULL,
blob_size_bytes INTEGER NOT NULL,
encryption_version VARCHAR(10) NOT NULL DEFAULT '1.0',
-- Sincronizacion
sync_version BIGINT NOT NULL DEFAULT 1,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indices
CREATE INDEX idx_encrypted_detections_user ON srv_encrypted_detections(user_id, entity_type);
CREATE INDEX idx_encrypted_detections_sync ON srv_encrypted_detections(user_id, sync_version);
CREATE INDEX idx_encrypted_detections_hash ON srv_encrypted_detections(blob_hash);
-- RLS
ALTER TABLE srv_encrypted_detections ENABLE ROW LEVEL SECURITY;
CREATE POLICY srv_encrypted_detections_select
ON srv_encrypted_detections FOR SELECT
USING (user_id = auth.uid());
CREATE POLICY srv_encrypted_detections_insert
ON srv_encrypted_detections FOR INSERT
WITH CHECK (user_id = auth.uid());
CREATE POLICY srv_encrypted_detections_update
ON srv_encrypted_detections FOR UPDATE
USING (user_id = auth.uid());
CREATE POLICY srv_encrypted_detections_delete
ON srv_encrypted_detections FOR DELETE
USING (user_id = auth.uid());
-- Comentarios
COMMENT ON TABLE srv_encrypted_detections IS 'Blobs E2E de detecciones de interacciones (servidor NO puede leer)';
COMMENT ON COLUMN srv_encrypted_detections.encrypted_blob IS 'Blob cifrado E2E - servidor no puede descifrar';
COMMENT ON COLUMN srv_encrypted_detections.blob_hash IS 'SHA-256 del blob para verificacion de integridad';
9. Indices y Constraints¶
9.1. Indices Cliente (iOS)¶
Realm maneja indices automaticamente con @Persisted(indexed: true).
Indices criticos:
cli_med_study_rules:(principioActivo, estudioCategoria),(categoriaTerapeutica, estudioCategoria),severidadcli_detected_med_study:eventoId,medicamentoUsuarioId,estudioFecha,severidadcli_suspension_reminders:interaccionEventoId,(fechaHoraProgramada, enviado),tipocli_post_study_confirmations:interaccionEventoId,fechaEstudioRealizado,medicamentoReiniciado
9.2. Indices Cliente (Android)¶
Indices definidos en anotaciones @Entity(indices = [...]).
9.3. Indices Servidor¶
Ver secciones 8.1, 8.2, 8.3 arriba.
10. Row Level Security (RLS)¶
Aplicable a: srv_encrypted_detections
-- RLS para srv_encrypted_detections
-- GARANTIA: Usuario solo puede acceder a SUS propias detecciones
-- Funcion helper para obtener user_id del JWT
CREATE OR REPLACE FUNCTION auth.uid() RETURNS UUID AS $$
SELECT NULLIF(current_setting('request.jwt.claim.sub', TRUE), '')::UUID;
$$ LANGUAGE SQL STABLE;
-- Politicas (ya definidas en seccion 8.3)
-- SELECT: Solo sus datos
-- INSERT: Solo puede insertar con su user_id
-- UPDATE: Solo puede actualizar sus datos
-- DELETE: Solo puede borrar sus datos
Notas:
srv_med_study_catalogysrv_rules_updatesson publicos, NO tienen RLS- Usuarios autenticados pueden leer todo el catalogo
- Solo
srv_encrypted_detectionstiene RLS porque contiene PHI
11. Mapeo de Sincronizacion¶
11.1. Sincronizacion de Detecciones¶
| Campo LOCAL | → SERVIDOR | Formato |
|---|---|---|
| cli_detected_med_study (completo) | encrypted_blob | Blob E2E cifrado |
| cli_suspension_reminders (completo) | encrypted_blob | Blob E2E cifrado |
| cli_post_study_confirmations (completo) | encrypted_blob | Blob E2E cifrado |
| id | id | UUID directo |
| sync_version | sync_version | BigInt |
Proceso de sincronizacion:
1. Cliente detecta interaccion
2. Guarda en cli_detected_med_study (local, claro)
3. Serializa entidad completa a JSON
4. Cifra JSON con AES-256-GCM (master_key del usuario)
5. Calcula SHA-256 del blob cifrado
6. Envia a servidor:
{
"id": "uuid-123",
"user_id": "uuid-user",
"entity_type": "med_study_detection",
"encrypted_blob": "0x7f8a9b...",
"blob_hash": "sha256:abc123...",
"encryption_version": "1.0"
}
7. Servidor almacena blob opaco (NO puede leer contenido)
8. Otros dispositivos del usuario pull y descifran
11.2. Actualizacion del Mini-Cache Critico¶
IMPORTANTE: El mini-cache (<100 reglas criticas) se actualiza SOLO con app updates, NO por OTA.
Proceso de actualizacion:
1. Nuevo release de app incluye archivo embebido:
- critical_med_study_rules_v2025.12.1.json (<100 reglas)
2. Al iniciar app, verifica version de mini-cache:
- Si version local < version embebida → actualiza
3. Carga reglas del archivo embebido a cli_critical_med_study_cache:
- DELETE todas las reglas antiguas
- INSERT nuevas reglas del archivo
4. NINGUN download desde servidor
5. NINGUN OTA de 500-1000+ reglas
**Regla del Director**: Maximo <100 reglas. Distribucion via app bundle.
11.3. Busqueda Online de Reglas del Usuario¶
Proceso bajo demanda (requiere consentimiento):
1. Usuario intenta agendar cita/estudio
2. App busca en cli_critical_med_study_cache (offline)
3. Si NO encuentra interaccion critica:
a. Muestra disclaimer/consent (si no lo dio previamente)
b. Usuario acepta busqueda online
c. POST /v1/interactions/med-study/search
{
"medications": ["codigos medicamentos del usuario"],
"study_category": "IMG",
"study_code": "TC_CONTRASTE"
}
d. Servidor busca en srv_med_study_catalog (~5,000+ reglas)
e. Responde con reglas aplicables
f. App guarda en cli_user_med_study_rules (SYNCED_E2E)
4. Siguiente vez, busca primero en cli_user_med_study_rules
12. Algoritmo de Deteccion¶
12.1. Pseudocodigo¶
// PSEUDOCODIGO - Deteccion de interacciones al agendar cita
func verificarInteraccionesEstudio(
estudioCodigo: String,
estudioCategoria: String,
estudioSubcategoria: String?,
estudioFecha: Date
) -> [DetectedMedStudyInteraction] {
var detecciones: [DetectedMedStudyInteraction] = []
// 1. Obtener medicamentos activos del paciente
let medicamentos = obtenerMedicamentosActivos()
// 2. Para cada medicamento, buscar interacciones
for med in medicamentos {
// 2.1. Buscar por principio activo
var reglas = realm.objects(MedStudyRule.self)
.filter("principioActivo == %@ AND estudioCategoria == %@ AND activa == true",
med.principioActivo, estudioCategoria)
// 2.2. Si subcategoria especificada, filtrar
if let subcategoria = estudioSubcategoria {
reglas = reglas.filter("estudioSubcategoria == %@", subcategoria)
}
// 2.3. Si no hay coincidencias, buscar por categoria terapeutica
if reglas.isEmpty {
reglas = realm.objects(MedStudyRule.self)
.filter("categoriaTerapeutica == %@ AND estudioCategoria == %@ AND activa == true",
med.categoriaATC, estudioCategoria)
}
// 2.4. Filtrar por condiciones de aplicacion
for regla in reglas {
if aplicaRegla(regla, medicamento: med) {
// 2.5. Calcular tiempos de suspension/reinicio
let fechaUltimaDosis = calcularFechaUltimaDosis(
estudioFecha: estudioFecha,
horasAntes: regla.tiempoAntesHoras
)
let fechaReinicio = calcularFechaReinicio(
estudioFecha: estudioFecha,
horasDespues: regla.tiempoDespuesHoras
)
// 2.6. Crear deteccion
let deteccion = DetectedMedStudyInteraction()
deteccion.interaccionId = regla.id
deteccion.medicamentoUsuarioId = med.id
deteccion.medicamentoNombre = med.nombre
deteccion.medicamentoDosis = med.dosis
deteccion.severidad = regla.severidad
deteccion.tipoInteraccion = regla.tipoInteraccion
deteccion.accionRequerida = regla.accion
deteccion.tiempoAntesHoras = regla.tiempoAntesHoras
deteccion.tiempoDespuesHoras = regla.tiempoDespuesHoras
deteccion.fechaUltimaDosis = fechaUltimaDosis
deteccion.fechaReinicio = fechaReinicio
detecciones.append(deteccion)
}
}
}
// 3. Ordenar por severidad (CRITICO primero)
detecciones.sort { $0.severidad > $1.severidad }
return detecciones
}
func aplicaRegla(_ regla: MedStudyRule, medicamento: Medicamento) -> Bool {
// Verificar dosis minima
if let dosisMinima = regla.aplicaSiDosisMayor {
if medicamento.dosisNumerica < dosisMinima {
return false
}
}
// Verificar funcion renal (si perfil del paciente lo tiene)
if let eGFRMaximo = regla.aplicaSiFuncionRenalMenor {
let eGFR = paciente.ultimoEGFR ?? 60 // Default conservador
if eGFR >= eGFRMaximo {
return false
}
}
return true
}
12.2. Calculo de Tiempos de Suspension¶
// Calcular fecha de ultima dosis permitida
func calcularFechaUltimaDosis(estudioFecha: Date, horasAntes: Int) -> Date {
let segundos = TimeInterval(horasAntes * 3600)
return estudioFecha.addingTimeInterval(-segundos)
}
// Calcular fecha de reinicio
func calcularFechaReinicio(estudioFecha: Date, horasDespues: Int) -> Date {
let segundos = TimeInterval(horasDespues * 3600)
return estudioFecha.addingTimeInterval(segundos)
}
// Ejemplo:
// Estudio: 2025-12-20 08:00
// Regla: Metformina, tiempo_antes_horas = 48, tiempo_despues_horas = 48
// Ultima dosis: 2025-12-18 08:00
// Reinicio: 2025-12-22 08:00
13. Queries de Ejemplo¶
13.1. Buscar Interacciones al Agendar Cita¶
iOS (Realm):
// Buscar interacciones para un medicamento y estudio
let interacciones = MedStudyRule.buscarInteracciones(
in: realm,
principioActivo: "Metformina",
estudioCategoria: "IMG",
estudioSubcategoria: "TC"
)
for inter in interacciones {
print("Severidad: \(inter.severidad), Tipo: \(inter.tipoInteraccion)")
print("Accion: \(inter.accion), Tiempo antes: \(inter.tiempoAntesHoras)h")
}
Android (Room):
// Buscar por principio activo
val interacciones = medStudyRuleDao.buscarPorPrincipioActivo(
principioActivo = "Metformina",
categoria = "IMG"
)
Servidor (PostgreSQL):
-- Buscar interacciones para un medicamento
SELECT
id,
principio_activo,
estudio_categoria,
tipo_interaccion,
severidad,
descripcion,
tiempo_antes_horas,
tiempo_despues_horas
FROM srv_med_study_catalog
WHERE principio_activo = 'Metformina'
AND estudio_categoria = 'IMG'
AND activa = TRUE
ORDER BY severidad DESC;
13.2. Verificar Estudios Futuros al Agregar Medicamento¶
iOS (Realm):
// Al agregar medicamento nuevo, verificar contra citas futuras
let hoy = Date()
let citasFuturas = realm.objects(EventoMedico.self)
.filter("tipo == 'ESTUDIO' AND fechaHora >= %@", hoy)
for cita in citasFuturas {
let interacciones = MedStudyRule.buscarInteracciones(
in: realm,
principioActivo: nuevoMedicamento.principioActivo,
estudioCategoria: cita.estudioCategoria
)
if !interacciones.isEmpty {
// Mostrar alerta retroactiva
mostrarAlertaInteraccionesRetroactivas(
medicamento: nuevoMedicamento,
cita: cita,
interacciones: Array(interacciones)
)
}
}
13.3. Obtener Recordatorios Pendientes¶
iOS (Realm):
// Recordatorios que deben enviarse ahora
let pendientes = SuspensionReminder.pendientes(in: realm)
for reminder in pendientes {
// Enviar notificacion local
enviarNotificacionLocal(reminder)
// Marcar como enviado
try? realm.write {
reminder.enviado = true
reminder.fechaEnvio = Date()
}
}
Android (Room):
// Obtener recordatorios pendientes
val ahora = Instant.now()
val pendientes = suspensionReminderDao.pendientes(ahora)
for (reminder in pendientes) {
// Enviar notificacion
notificationManager.enviar(reminder)
// Actualizar estado
val actualizado = reminder.copy(
enviado = true,
fechaEnvio = Instant.now()
)
suspensionReminderDao.actualizar(actualizado)
}
13.4. Historial de Interacciones (6 anos HIPAA)¶
iOS (Realm):
// Historial ultimos 6 anos
let historial = DetectedMedStudyInteraction.historialUltimos6Anos(in: realm)
for deteccion in historial {
print("Estudio: \(deteccion.estudioNombre)")
print("Fecha: \(deteccion.estudioFecha)")
print("Medicamento: \(deteccion.medicamentoNombre)")
print("Severidad: \(deteccion.severidad)")
print("Usuario confirmo: \(deteccion.usuarioConfirmo)")
print("---")
}
Servidor (PostgreSQL):
-- Historial de un usuario (servidor solo ve blobs)
SELECT
id,
entity_type,
blob_size_bytes,
created_at,
updated_at
FROM srv_encrypted_detections
WHERE user_id = $1
AND created_at >= NOW() - INTERVAL '6 years'
ORDER BY created_at DESC;
-- NOTA: Servidor NO puede ver contenido, solo metadata
14. Consideraciones de Cifrado¶
14.1. Datos PHI que se Cifran E2E¶
TODOS estos datos se cifran antes de sincronizar:
| Dato | Razon |
|---|---|
| medicamento_nombre | PHI - identifica condicion medica |
| medicamento_dosis | PHI - parte del tratamiento |
| estudio_nombre | PHI - indica condicion sospechada |
| estudio_fecha | PHI - cita medica |
| notas_seguimiento | PHI - informacion medica del paciente |
| descripcion_reaccion | PHI - reaccion adversa |
| valor_creatinina | PHI - resultado de laboratorio |
| titulo (recordatorio) | PHI - contiene nombre medicamento |
| mensaje (recordatorio) | PHI - instrucciones personalizadas |
Proceso de cifrado:
1. Serializar entidad a JSON:
{
"id": "uuid-123",
"medicamentoNombre": "Metformina 850mg",
"estudioNombre": "TAC Abdominal con Contraste",
"estudioFecha": "2025-12-20T08:00:00Z",
...
}
2. Obtener master_key del Keychain/Keystore
3. Generar nonce aleatorio (96 bits)
4. Cifrar con AES-256-GCM:
ciphertext, tag = AES-256-GCM.encrypt(
key: master_key,
nonce: nonce,
plaintext: json_string,
aad: entity_id
)
5. Empaquetar:
encrypted_blob = nonce || ciphertext || tag
6. Calcular hash:
blob_hash = SHA-256(encrypted_blob)
7. Enviar a servidor
14.2. Datos Publicos (No Cifrados)¶
cli_med_study_rules: Datos publicos, NO se cifran
- Son reglas derivadas de literatura medica
- Cualquier profesional de salud puede acceder
- No contienen informacion del paciente
srv_med_study_catalog: Publico srv_rules_updates: Publico
15. Retencion y Limpieza¶
| Entidad | Retencion | Razon |
|---|---|---|
| cli_med_study_rules | Indefinida | Base de conocimiento, se actualiza OTA |
| cli_detected_med_study | 6 anos | HIPAA compliance (45 CFR 164.316) |
| cli_suspension_reminders | 6 anos | Parte del registro medico |
| cli_post_study_confirmations | 6 anos | Seguimiento medico |
Limpieza automatica:
// iOS - Limpieza de detecciones antiguas (> 6 anos)
extension DetectedMedStudyInteraction {
static func limpiarAntiguasHIPAA(in realm: Realm) {
let cutoff = Calendar.current.date(byAdding: .year, value: -6, to: Date())!
let antiguas = realm.objects(DetectedMedStudyInteraction.self)
.filter("createdAt < %@", cutoff)
try? realm.write {
// Eliminar recordatorios asociados primero
for deteccion in antiguas {
realm.delete(deteccion.reminders)
}
// Eliminar detecciones
realm.delete(antiguas)
}
}
}
Trigger automatico: Ejecutar limpieza mensual en background
16. Validacion del Modelo¶
16.1. Clasificacion de Datos¶
- Todos los campos clasificados
- Ningun PHI como SYNCED_PLAIN
- Datos sensibles como SYNCED_E2E
- Datos publicos como LOCAL_ONLY o SERVER_SOURCE
16.2. Esquemas¶
- Esquema LOCAL conceptual completo
- Esquema SERVIDOR solo blobs + metadata + catalogos publicos
- Mapeo de sincronizacion definido
- Implementaciones iOS (Swift/Realm) completas
- Implementaciones Android (Kotlin/Room) completas
- Esquemas PostgreSQL con RLS
16.3. Zero-Knowledge¶
- Servidor NO puede ver nombres de medicamentos del paciente
- Servidor NO puede ver nombres de estudios del paciente
- Servidor NO puede ver fechas de citas
- Servidor NO puede ver notas de seguimiento
- Solo metadata operativa visible (timestamps, tamanos, sync_version)
- Catalogo de reglas es publico (no contiene datos del paciente)
17. Referencias¶
17.1. Documentos Funcionales¶
- MTS-INT-002: Motor de Interacciones Medicamento-Estudio
- MTS-EST-001: Catalogo de Estudios
- MTS-MED-001: Medicamentos
- MTS-CIT-001: Citas y Eventos
- MTS-NTF-001: Notificaciones
17.2. Documentos Tecnicos¶
- TECH-CS-001: Arquitectura Cliente-Servidor
- TECH-DS-001: Estrategia de Datos
- MDL-MED-001: Modelo de Medicamentos
- MDL-NTF-001: Modelo de Notificaciones
- MDL-CAL-001: Modelo de Calendario
17.3. Estandares e Investigaciones¶
- INV-010: Anonimizacion de Medicamentos
- HIPAA 45 CFR 164.316 (Retencion 6 anos)
- FDA Safety Communications
- AACC Guidance on Biotin Interference
- CHEST Guidelines: Perioperative Anticoagulation
- ACR-NKF Consensus on Contrast Media
Modelo generado por: DatabaseDrone (Doce de Quince) Fecha: 2025-12-08 Estado: APROBADO
"Los datos son el nucleo. LOCAL primero, SERVIDOR solo blobs." "Las reglas de interaccion son publicas. Las detecciones del paciente son privadas."