Saltar a contenido

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

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

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:

CONSTRAINT check_mini_cache_size CHECK (total_reglas < 100)

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), severidad
  • cli_detected_med_study: eventoId, medicamentoUsuarioId, estudioFecha, severidad
  • cli_suspension_reminders: interaccionEventoId, (fechaHoraProgramada, enviado), tipo
  • cli_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_catalog y srv_rules_updates son publicos, NO tienen RLS
  • Usuarios autenticados pueden leer todo el catalogo
  • Solo srv_encrypted_detections tiene 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

17.2. Documentos Tecnicos

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."