Saltar a contenido

Modelo de Datos: Interacciones Medicamentosas

Identificador: MDL-INT-001 Version: 2.0.0 Fecha: 2025-12-08 Estado: En Revision - ACTUALIZADO POR DIRECTIVA DEL DIRECTOR Autor: DatabaseDrone / SpecQueen Technical Division Modulo Funcional: MTS-INT-001 Iteracion: IT-10



1. Resumen y Trazabilidad

Campo Valor
Ref Funcional MTS-INT-001
Dominio INT (Interacciones)
Iteracion IT-10
Dependencias MDL-MED-001, MDL-ALT-001, MDL-USR-001
Prioridad ALTA (Seguridad del paciente)

1.1. Arquitectura Dual

┌─────────────────────────────────────────────────────────────┐
│                  DISPOSITIVO (LOCAL - 95%)                   │
│                                                              │
│  [Cache CRITICO <100 interacciones CONTRAINDICADAS]         │
│  • Embebido en app (NO descargado)                          │
│  • Datos en CLARO                                            │
│  • Severidad 4 (CONTRAINDICADO) mas comunes                 │
│  • Deteccion offline O(n^2) para casos criticos              │
│                                                              │
│  [Detecciones del usuario]                                   │
│  • PHI - Cifradas E2E antes de sync                          │
│  • Historial 6 anos (HIPAA)                                  │
│  • Incluye resultados de busquedas online                    │
│                                                              │
│  [Confirmaciones y Consentimiento]                           │
│  • PHI - Decisiones medicas cifradas E2E                     │
│  • Consentimiento de disclaimer de privacidad                │
│                                                              │
└─────────────────────────────────────────────────────────────┘
                Cifrado E2E (AES-256-GCM)
┌─────────────────────────────────────────────────────────────┐
│                   SERVIDOR (5% - Coordinacion)               │
│                                                              │
│  [Catalogo maestro de interacciones]                        │
│  • Datos publicos (no PHI)                                   │
│  • ~1,000,000+ pares (DrugBank + RxNorm + propios)          │
│  • Busquedas online (Pro/Perfect con consentimiento)         │
│                                                              │
│  [Cache de consultas DrugBank]                              │
│  • Metadata de consultas exitosas                            │
│  • TTL 7 dias                                                │
│                                                              │
│  [Log de analisis IA]                                        │
│  • Metricas sin PHI                                          │
│  • Perfect tier only                                         │
│                                                              │
└─────────────────────────────────────────────────────────────┘

1.2. Principios de Diseno

Principio Implementacion
Offline Critico Cache local <100 CONTRAINDICADAS mas criticas (embebido en app)
Online con Consentimiento Busquedas online (Pro/Perfect) requieren aceptar disclaimer de privacidad
Zero-Knowledge Servidor NUNCA ve medicamentos del usuario en claro
Retencion HIPAA Historial de alertas se mantiene 6 anos
Sanitizacion IA Datos enviados a Claude NUNCA incluyen PII
Regla 100 Registros Ningun catalogo >100 registros se descarga (Directiva Director)

2. Clasificacion de Datos

2.1. Tabla de Clasificacion por Entidad

Entidad Clasificacion Cifrado Sync Razon
SERVIDOR
srv_interaction_catalog SERVER_SOURCE No Busqueda online Catalogo publico de interacciones (~1M+)
srv_drugbank_cache SERVER_ONLY No No Metadata de consultas API
srv_ia_analysis_log SERVER_ONLY No No Metricas sin PHI
CLIENTE
cli_critical_interactions_cache LOCAL_ONLY No Embebido en app <100 CONTRAINDICADAS criticas (NO descargado)
cli_detected_interactions SYNCED_E2E Si Bidireccional PHI - medicamentos detectados + busquedas online
cli_interaction_acknowledgments SYNCED_E2E Si Bidireccional PHI - decisiones medicas
cli_interaction_history SYNCED_E2E Si Bidireccional PHI - historial 6 anos
cli_interaction_consent SYNCED_E2E Si Bidireccional Consentimiento de disclaimer de privacidad
cli_pending_verifications LOCAL_ONLY No No Cola temporal (max 10 verificaciones pendientes)

2.2. Justificacion de Clasificaciones

Dato Clasificacion Justificacion
Catalogo critico <100 LOCAL_ONLY <100 interacciones CONTRAINDICADAS mas criticas. Embebido en app (NO descargado). Cumple Regla 100
Catalogo completo servidor SERVER_SOURCE ~1M+ interacciones. Solo busquedas online (Pro/Perfect con consentimiento). NUNCA se descarga
Interacciones detectadas SYNCED_E2E Revela medicamentos del usuario (PHI). Incluye resultados de busquedas online. Servidor solo blob cifrado
Confirmaciones de usuario SYNCED_E2E Decisiones medicas sensibles (PHI). Requiere confirmacion explicita en CONTRAINDICADO
Consentimiento de busqueda SYNCED_E2E Aceptacion del disclaimer de privacidad para busquedas online. PHI (vinculado a usuario)
Historial de alertas SYNCED_E2E HIPAA requiere retencion 6 anos. PHI que debe cifrarse
Cola de verificaciones LOCAL_ONLY Max 10 verificaciones pendientes (no descarga masiva). Cola temporal, no PHI
Cache DrugBank SERVER_ONLY Metadata operativa sin PHI. No sale del servidor
Log IA SERVER_ONLY Solo metricas (latencia, tokens). Datos enviados a IA sanitizados (sin PII)

3. Esquema SERVIDOR (Catalogos y Logs)

3.1. srv_interaction_catalog

Catalogo maestro de interacciones medicamento-medicamento.

Clasificacion: SERVER_SOURCE (publico)

CREATE TABLE srv_interaction_catalog (
    -- Primary Key
    interaction_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Medicamentos Involucrados
    drug_a_id           UUID NOT NULL,  -- FK a srv_drug_catalog
    drug_b_id           UUID NOT NULL,  -- FK a srv_drug_catalog
    principle_a         VARCHAR(200) NOT NULL,  -- Principio activo A
    principle_b         VARCHAR(200) NOT NULL,  -- Principio activo B

    -- Clasificacion
    severity            VARCHAR(20) NOT NULL CHECK (severity IN ('CONTRAINDICADO', 'SERIO', 'MODERADO', 'MENOR')),
    severity_code       SMALLINT NOT NULL CHECK (severity_code BETWEEN 1 AND 4),  -- 1=MENOR, 4=CONTRAINDICADO
    interaction_type    VARCHAR(20) NOT NULL CHECK (interaction_type IN ('FARMACOCINETICA', 'FARMACODINAMICA', 'MIXTA')),
    category            VARCHAR(100),  -- CYP450, QT prolongation, etc.

    -- Descripcion
    description         TEXT NOT NULL,
    mechanism           TEXT,
    expected_effect     TEXT,
    possible_symptoms   TEXT[],

    -- Recomendacion
    clinical_recommendation TEXT,
    suggested_action    VARCHAR(20) CHECK (suggested_action IN ('EVITAR', 'MONITOREAR', 'AJUSTAR_DOSIS', 'ESPACIAR', 'INFORMAR')),
    monitoring_required TEXT,

    -- Fuente y Evidencia
    primary_source      VARCHAR(100) NOT NULL,  -- DrugBank, RxNorm, manual, etc.
    additional_sources  TEXT[],
    evidence_level      VARCHAR(10) CHECK (evidence_level IN ('ALTA', 'MEDIA', 'BAJA')),
    pubmed_refs         TEXT[],
    last_updated_date   DATE NOT NULL,

    -- Metadata Regional
    applicable_countries TEXT[] DEFAULT ARRAY['MX', 'US', 'BR'],  -- ISO 3166-1 alpha-2
    regional_notes      JSONB,  -- {"MX": "...", "US": "..."}
    is_active           BOOLEAN NOT NULL DEFAULT TRUE,

    -- Timestamps
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Constraint: evitar duplicados (A-B == B-A)
    CONSTRAINT unique_pair CHECK (drug_a_id < drug_b_id)
);

3.1.1. Columnas

Columna Tipo Nullable Descripcion
interaction_id UUID No Identificador unico
drug_a_id UUID No FK a srv_drug_catalog (menor UUID)
drug_b_id UUID No FK a srv_drug_catalog (mayor UUID)
principle_a VARCHAR(200) No Principio activo del medicamento A
principle_b VARCHAR(200) No Principio activo del medicamento B
severity VARCHAR(20) No CONTRAINDICADO, SERIO, MODERADO, MENOR
severity_code SMALLINT No 1=MENOR, 2=MODERADO, 3=SERIO, 4=CONTRAINDICADO
interaction_type VARCHAR(20) No FARMACOCINETICA, FARMACODINAMICA, MIXTA
category VARCHAR(100) Si Categoria de interaccion (CYP450, QT prolongation, etc.)
description TEXT No Descripcion de la interaccion
mechanism TEXT Si Mecanismo de la interaccion
expected_effect TEXT Si Efecto esperado de la interaccion
possible_symptoms TEXT[] Si Sintomas posibles
clinical_recommendation TEXT Si Recomendacion clinica
suggested_action VARCHAR(20) Si EVITAR, MONITOREAR, AJUSTAR_DOSIS, ESPACIAR, INFORMAR
monitoring_required TEXT Si Monitoreo requerido
primary_source VARCHAR(100) No Fuente primaria (DrugBank, RxNorm, manual)
additional_sources TEXT[] Si Fuentes adicionales
evidence_level VARCHAR(10) Si ALTA, MEDIA, BAJA
pubmed_refs TEXT[] Si Referencias PubMed
last_updated_date DATE No Fecha ultima actualizacion de la fuente
applicable_countries TEXT[] Si Paises donde aplica (ISO 3166-1 alpha-2)
regional_notes JSONB Si Notas especificas por pais
is_active BOOLEAN No Si esta activa en el catalogo
created_at TIMESTAMPTZ No Fecha creacion en servidor
updated_at TIMESTAMPTZ No Ultima actualizacion

3.1.2. Indices

-- Busqueda bidireccional por par de medicamentos
CREATE INDEX idx_interaction_drugs ON srv_interaction_catalog(drug_a_id, drug_b_id);
CREATE INDEX idx_interaction_drugs_reverse ON srv_interaction_catalog(drug_b_id, drug_a_id);

-- Busqueda por principio activo
CREATE INDEX idx_interaction_principles ON srv_interaction_catalog(principle_a, principle_b);

-- Busqueda por severidad
CREATE INDEX idx_interaction_severity ON srv_interaction_catalog(severity_code DESC);

-- Busqueda por categoria
CREATE INDEX idx_interaction_category ON srv_interaction_catalog(category) WHERE category IS NOT NULL;

-- Filtro de activos
CREATE INDEX idx_interaction_active ON srv_interaction_catalog(is_active) WHERE is_active = TRUE;

-- Actualizacion reciente (para OTA)
CREATE INDEX idx_interaction_updated ON srv_interaction_catalog(updated_at DESC);

-- Full-text search en descripcion
CREATE INDEX idx_interaction_search ON srv_interaction_catalog USING GIN(to_tsvector('spanish', description));

3.1.3. Constraints

-- Trigger para actualizar updated_at
CREATE OR REPLACE FUNCTION update_interaction_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_interaction_updated_at
    BEFORE UPDATE ON srv_interaction_catalog
    FOR EACH ROW
    EXECUTE FUNCTION update_interaction_updated_at();

-- Constraint adicional: drug_a_id != drug_b_id
ALTER TABLE srv_interaction_catalog
    ADD CONSTRAINT different_drugs CHECK (drug_a_id != drug_b_id);

3.2. srv_drugbank_cache

Cache de consultas exitosas a DrugBank API (Pro/Perfect).

Clasificacion: SERVER_ONLY

CREATE TABLE srv_drugbank_cache (
    -- Primary Key
    cache_id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Medicamentos Consultados (hash para no revelar combinaciones especificas)
    drug_pair_hash      VARCHAR(64) NOT NULL UNIQUE,  -- SHA-256(drug_a_id + drug_b_id)

    -- Metadata de la Consulta
    tier                VARCHAR(10) NOT NULL CHECK (tier IN ('Pro', 'Perfect')),
    http_status         SMALLINT NOT NULL,
    response_time_ms    INT,
    tokens_used         INT,  -- Si aplica (API con billing por tokens)

    -- Resultado (sin PHI)
    interactions_found  INT NOT NULL DEFAULT 0,
    max_severity_code   SMALLINT CHECK (max_severity_code BETWEEN 0 AND 4),  -- 0=sin interaccion

    -- Timestamp y Expiracion
    queried_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at          TIMESTAMPTZ NOT NULL,  -- TTL 7 dias

    -- Metadata
    source_version      VARCHAR(20),  -- Version de DrugBank consultada
    is_error            BOOLEAN NOT NULL DEFAULT FALSE,
    error_message       TEXT,

    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

3.2.1. Columnas

Columna Tipo Nullable Descripcion
cache_id UUID No Identificador unico
drug_pair_hash VARCHAR(64) No SHA-256 del par de medicamentos (blind index)
tier VARCHAR(10) No Pro o Perfect
http_status SMALLINT No Codigo HTTP de respuesta
response_time_ms INT Si Tiempo de respuesta en ms
tokens_used INT Si Tokens consumidos (si aplica)
interactions_found INT No Numero de interacciones encontradas
max_severity_code SMALLINT Si Maxima severidad encontrada (0-4)
queried_at TIMESTAMPTZ No Timestamp de la consulta
expires_at TIMESTAMPTZ No Expiracion del cache (TTL 7 dias)
source_version VARCHAR(20) Si Version de DrugBank
is_error BOOLEAN No Si la consulta resulto en error
error_message TEXT Si Mensaje de error (si aplica)
created_at TIMESTAMPTZ No Fecha creacion

3.2.2. Indices

-- Busqueda por hash (lookup de cache)
CREATE UNIQUE INDEX idx_drugbank_cache_hash ON srv_drugbank_cache(drug_pair_hash);

-- Limpieza de expirados
CREATE INDEX idx_drugbank_cache_expires ON srv_drugbank_cache(expires_at);

-- Metricas por tier
CREATE INDEX idx_drugbank_cache_tier ON srv_drugbank_cache(tier, queried_at);

-- Errores
CREATE INDEX idx_drugbank_cache_errors ON srv_drugbank_cache(is_error) WHERE is_error = TRUE;

3.2.3. Politica de Expiracion

-- Funcion para limpiar cache expirado (ejecutar con cron diario)
CREATE OR REPLACE FUNCTION cleanup_expired_drugbank_cache()
RETURNS void AS $$
BEGIN
    DELETE FROM srv_drugbank_cache
    WHERE expires_at < NOW();
END;
$$ LANGUAGE plpgsql;

-- TTL default: 7 dias
CREATE OR REPLACE FUNCTION set_drugbank_cache_expiry()
RETURNS TRIGGER AS $$
BEGIN
    NEW.expires_at = NEW.queried_at + INTERVAL '7 days';
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_drugbank_cache_expiry
    BEFORE INSERT ON srv_drugbank_cache
    FOR EACH ROW
    WHEN (NEW.expires_at IS NULL)
    EXECUTE FUNCTION set_drugbank_cache_expiry();

3.3. srv_ia_analysis_log

Log de consultas al motor IA (Claude - Perfect tier).

Clasificacion: SERVER_ONLY (sin PHI)

CREATE TABLE srv_ia_analysis_log (
    -- Primary Key
    log_id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Metadata de Consulta (SIN PHI)
    session_token       VARCHAR(64) NOT NULL,  -- Token efimero, no vinculado a user_id
    num_medications     SMALLINT NOT NULL,     -- Numero de medicamentos en la consulta
    severity_detected   VARCHAR(20),           -- Maxima severidad detectada

    -- Metricas de Rendimiento
    request_time_ms     INT,
    tokens_input        INT,
    tokens_output       INT,
    tokens_total        INT,

    -- Resultado
    http_status         SMALLINT NOT NULL,
    is_error            BOOLEAN NOT NULL DEFAULT FALSE,
    error_type          VARCHAR(50),  -- timeout, rate_limit, api_error, etc.

    -- Timestamp
    analyzed_at         TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Metadata
    anthropic_version   VARCHAR(20),  -- Version del modelo Claude

    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

3.3.1. Columnas

Columna Tipo Nullable Descripcion
log_id UUID No Identificador unico
session_token VARCHAR(64) No Token efimero ALEATORIO (no vinculado a user)
num_medications SMALLINT No Numero de medicamentos analizados
severity_detected VARCHAR(20) Si Maxima severidad detectada por IA
request_time_ms INT Si Tiempo de respuesta en ms
tokens_input INT Si Tokens de entrada
tokens_output INT Si Tokens de salida
tokens_total INT Si Tokens totales
http_status SMALLINT No Codigo HTTP de respuesta
is_error BOOLEAN No Si hubo error
error_type VARCHAR(50) Si Tipo de error
analyzed_at TIMESTAMPTZ No Timestamp del analisis
anthropic_version VARCHAR(20) Si Version de Claude
created_at TIMESTAMPTZ No Fecha creacion

3.3.2. Indices

-- Metricas por fecha
CREATE INDEX idx_ia_log_analyzed ON srv_ia_analysis_log(analyzed_at DESC);

-- Errores
CREATE INDEX idx_ia_log_errors ON srv_ia_analysis_log(is_error) WHERE is_error = TRUE;

-- Uso de tokens (para facturacion)
CREATE INDEX idx_ia_log_tokens ON srv_ia_analysis_log(tokens_total) WHERE tokens_total IS NOT NULL;

3.3.3. Retencion

-- Retencion: 90 dias (metricas operativas, no PHI)
CREATE OR REPLACE FUNCTION cleanup_old_ia_logs()
RETURNS void AS $$
BEGIN
    DELETE FROM srv_ia_analysis_log
    WHERE analyzed_at < NOW() - INTERVAL '90 days';
END;
$$ LANGUAGE plpgsql;

4. Esquema CLIENTE (Detecciones y Historial)

4.1. cli_critical_interactions_cache

Cache local CRITICO de interacciones CONTRAINDICADAS mas comunes (<100 registros).

Clasificacion: LOCAL_ONLY (embebido en app, NO descargado dinamicamente)

DIRECTIVA DEL DIRECTOR - REGLA 100 REGISTROS: - Maximo <100 interacciones CONTRAINDICADAS - Solo severidad 4 (CONTRAINDICADO) - Interacciones mas criticas y comunes - Embebido en el codigo de la app (NO descarga dinamica) - Cumple con: "Ningun catalogo >100 registros se descarga al dispositivo" - Referencia: 02-arquitectura-cliente-servidor.md seccion 3.2.3.1

4.1.1. Estructura (Conceptual)

Campo Tipo Descripcion
cache_id UUID Identificador unico
drug_a_id UUID Medicamento A (del catalogo local)
drug_b_id UUID Medicamento B (del catalogo local)
principle_a String Principio activo A
principle_b String Principio activo B
severity Enum SIEMPRE 'CONTRAINDICADO' (solo nivel 4)
severity_code Int SIEMPRE 4
interaction_type Enum FARMACOCINETICA, FARMACODINAMICA, MIXTA
category String Categoria de interaccion
description Text Descripcion
mechanism Text Mecanismo
expected_effect Text Efecto esperado
clinical_recommendation Text Recomendacion clinica
suggested_action Enum SIEMPRE 'EVITAR'
primary_source String DrugBank, RxNorm, MedTime
evidence_level Enum ALTA, MEDIA, BAJA
embedded_version String Version del codigo embebido

4.1.2. SQLite (iOS/Android)

-- Cliente SQLite
-- NOTA: Maximo <100 registros. Solo CONTRAINDICADO (severity_code = 4)
CREATE TABLE cli_critical_interactions_cache (
    cache_id            TEXT PRIMARY KEY,
    drug_a_id           TEXT NOT NULL,
    drug_b_id           TEXT NOT NULL,
    principle_a         TEXT NOT NULL,
    principle_b         TEXT NOT NULL,
    severity            TEXT NOT NULL DEFAULT 'CONTRAINDICADO' CHECK (severity = 'CONTRAINDICADO'),
    severity_code       INTEGER NOT NULL DEFAULT 4 CHECK (severity_code = 4),
    interaction_type    TEXT NOT NULL CHECK (interaction_type IN ('FARMACOCINETICA', 'FARMACODINAMICA', 'MIXTA')),
    category            TEXT,
    description         TEXT NOT NULL,
    mechanism           TEXT,
    expected_effect     TEXT,
    clinical_recommendation TEXT,
    suggested_action    TEXT NOT NULL DEFAULT 'EVITAR' CHECK (suggested_action = 'EVITAR'),
    primary_source      TEXT NOT NULL,
    evidence_level      TEXT CHECK (evidence_level IN ('ALTA', 'MEDIA', 'BAJA')),
    embedded_version    TEXT NOT NULL,  -- Version del codigo embebido
    UNIQUE(drug_a_id, drug_b_id),
    -- Constraint para garantizar max 100 registros (no soportado en SQLite, debe validarse en codigo)
    CHECK ((SELECT COUNT(*) FROM cli_critical_interactions_cache) <= 100)
);

-- Indices
CREATE INDEX idx_critical_cache_drugs ON cli_critical_interactions_cache(drug_a_id, drug_b_id);
CREATE INDEX idx_critical_cache_principles ON cli_critical_interactions_cache(principle_a, principle_b);

4.1.3. Realm (iOS)

// Realm Model (iOS)
// NOTA: Maximo <100 registros. Solo CONTRAINDICADO (severityCode = 4)
class CriticalInteractionCache: Object {
    @Persisted(primaryKey: true) var cacheId: String
    @Persisted var drugAId: String
    @Persisted var drugBId: String
    @Persisted var principleA: String
    @Persisted var principleB: String
    @Persisted var severity: String = "CONTRAINDICADO" // SIEMPRE CONTRAINDICADO
    @Persisted var severityCode: Int = 4 // SIEMPRE 4
    @Persisted var interactionType: String // FARMACOCINETICA, FARMACODINAMICA, MIXTA
    @Persisted var category: String?
    @Persisted var descriptionText: String
    @Persisted var mechanism: String?
    @Persisted var expectedEffect: String?
    @Persisted var clinicalRecommendation: String?
    @Persisted var suggestedAction: String = "EVITAR" // SIEMPRE EVITAR
    @Persisted var primarySource: String
    @Persisted var evidenceLevel: String? // ALTA, MEDIA, BAJA
    @Persisted var embeddedVersion: String // Version del codigo embebido
}

4.1.4. Room (Android)

// Room Entity (Android)
// NOTA: Maximo <100 registros. Solo CONTRAINDICADO (severityCode = 4)
@Entity(
    tableName = "cli_critical_interactions_cache",
    indices = [
        Index(value = ["drug_a_id", "drug_b_id"], unique = true),
        Index(value = ["principle_a", "principle_b"])
    ]
)
data class CriticalInteractionCache(
    @PrimaryKey val cacheId: String,
    @ColumnInfo(name = "drug_a_id") val drugAId: String,
    @ColumnInfo(name = "drug_b_id") val drugBId: String,
    @ColumnInfo(name = "principle_a") val principleA: String,
    @ColumnInfo(name = "principle_b") val principleB: String,
    val severity: String = "CONTRAINDICADO", // SIEMPRE CONTRAINDICADO
    @ColumnInfo(name = "severity_code") val severityCode: Int = 4, // SIEMPRE 4
    @ColumnInfo(name = "interaction_type") val interactionType: String,
    val category: String?,
    val description: String,
    val mechanism: String?,
    @ColumnInfo(name = "expected_effect") val expectedEffect: String?,
    @ColumnInfo(name = "clinical_recommendation") val clinicalRecommendation: String?,
    @ColumnInfo(name = "suggested_action") val suggestedAction: String = "EVITAR", // SIEMPRE EVITAR
    @ColumnInfo(name = "primary_source") val primarySource: String,
    @ColumnInfo(name = "evidence_level") val evidenceLevel: String?,
    @ColumnInfo(name = "embedded_version") val embeddedVersion: String // Version del codigo embebido
)

4.2. cli_detected_interactions

Interacciones detectadas para los medicamentos del usuario.

Clasificacion: SYNCED_E2E (PHI)

4.2.1. Estructura (Conceptual)

Campo Tipo Descripcion
detection_id UUID Identificador unico
user_id UUID FK a usuarios (LOCAL)
medication_a_id UUID FK a medicamentos del usuario
medication_b_id UUID FK a medicamentos del usuario
interaction_cache_id UUID FK a cache de interacciones
severity Enum CONTRAINDICADO, SERIO, MODERADO, MENOR
severity_code Int 1-4
status Enum ACTIVE, ACKNOWLEDGED, RESOLVED, DISMISSED
detected_at DateTime Timestamp de deteccion
resolved_at DateTime Timestamp de resolucion
sync_status Enum PENDING, SYNCED, CONFLICT
last_sync_version Int Version de sincronizacion
created_at DateTime Timestamp creacion
updated_at DateTime Ultima actualizacion

4.2.2. SQLite (iOS/Android)

-- Cliente SQLite
CREATE TABLE cli_detected_interactions (
    detection_id        TEXT PRIMARY KEY,
    user_id             TEXT NOT NULL,
    medication_a_id     TEXT NOT NULL,
    medication_b_id     TEXT NOT NULL,
    interaction_cache_id TEXT,  -- FK a cli_critical_interactions_cache (opcional, solo si fue detectada offline)
    severity            TEXT NOT NULL,
    severity_code       INTEGER NOT NULL,
    status              TEXT NOT NULL CHECK (status IN ('ACTIVE', 'ACKNOWLEDGED', 'RESOLVED', 'DISMISSED')),
    detected_at         TEXT NOT NULL,  -- ISO 8601
    resolved_at         TEXT,
    sync_status         TEXT NOT NULL DEFAULT 'PENDING',
    last_sync_version   INTEGER DEFAULT 0,
    created_at          TEXT NOT NULL,
    updated_at          TEXT NOT NULL,
    FOREIGN KEY (medication_a_id) REFERENCES cli_medications(medication_id),
    FOREIGN KEY (medication_b_id) REFERENCES cli_medications(medication_id),
    FOREIGN KEY (interaction_cache_id) REFERENCES cli_critical_interactions_cache(cache_id)
);

-- Indices
CREATE INDEX idx_detected_user ON cli_detected_interactions(user_id);
CREATE INDEX idx_detected_status ON cli_detected_interactions(status) WHERE status = 'ACTIVE';
CREATE INDEX idx_detected_sync ON cli_detected_interactions(sync_status) WHERE sync_status = 'PENDING';
CREATE INDEX idx_detected_medications ON cli_detected_interactions(medication_a_id, medication_b_id);

4.2.3. Realm (iOS)

// Realm Model (iOS)
class DetectedInteraction: Object {
    @Persisted(primaryKey: true) var detectionId: String
    @Persisted var userId: String
    @Persisted var medicationAId: String
    @Persisted var medicationBId: String
    @Persisted var interactionCacheId: String?
    @Persisted var severity: String
    @Persisted var severityCode: Int
    @Persisted var status: String // ACTIVE, ACKNOWLEDGED, RESOLVED, DISMISSED
    @Persisted var detectedAt: Date
    @Persisted var resolvedAt: Date?
    @Persisted var syncStatus: String
    @Persisted var lastSyncVersion: Int
    @Persisted var createdAt: Date
    @Persisted var updatedAt: Date
}

4.2.4. Room (Android)

// Room Entity (Android)
@Entity(
    tableName = "cli_detected_interactions",
    foreignKeys = [
        ForeignKey(
            entity = Medication::class,
            parentColumns = ["medication_id"],
            childColumns = ["medication_a_id"]
        ),
        ForeignKey(
            entity = Medication::class,
            parentColumns = ["medication_id"],
            childColumns = ["medication_b_id"]
        ),
        ForeignKey(
            entity = InteractionCache::class,
            parentColumns = ["cache_id"],
            childColumns = ["interaction_cache_id"]
        )
    ],
    indices = [
        Index(value = ["user_id"]),
        Index(value = ["status"]),
        Index(value = ["sync_status"]),
        Index(value = ["medication_a_id", "medication_b_id"])
    ]
)
data class DetectedInteraction(
    @PrimaryKey val detectionId: String,
    @ColumnInfo(name = "user_id") val userId: String,
    @ColumnInfo(name = "medication_a_id") val medicationAId: String,
    @ColumnInfo(name = "medication_b_id") val medicationBId: String,
    @ColumnInfo(name = "interaction_cache_id") val interactionCacheId: String?,
    val severity: String,
    @ColumnInfo(name = "severity_code") val severityCode: Int,
    val status: String,
    @ColumnInfo(name = "detected_at") val detectedAt: String,
    @ColumnInfo(name = "resolved_at") val resolvedAt: String?,
    @ColumnInfo(name = "sync_status") val syncStatus: String,
    @ColumnInfo(name = "last_sync_version") val lastSyncVersion: Int,
    @ColumnInfo(name = "created_at") val createdAt: String,
    @ColumnInfo(name = "updated_at") val updatedAt: String
)

4.3. cli_interaction_acknowledgments

Confirmaciones del usuario para interacciones detectadas.

Clasificacion: SYNCED_E2E (PHI - decisiones medicas)

4.3.1. Estructura (Conceptual)

Campo Tipo Descripcion
ack_id UUID Identificador unico
detection_id UUID FK a cli_detected_interactions
user_id UUID FK a usuarios
acknowledged_at DateTime Timestamp de confirmacion
checkbox_confirmed Boolean Si marco el checkbox de entendimiento
consulted_doctor Boolean Si consulto con medico (CONTRAINDICADO)
notes Text Notas del usuario
sync_status Enum PENDING, SYNCED, CONFLICT
last_sync_version Int Version de sincronizacion
created_at DateTime Timestamp creacion

4.3.2. SQLite (iOS/Android)

-- Cliente SQLite
CREATE TABLE cli_interaction_acknowledgments (
    ack_id              TEXT PRIMARY KEY,
    detection_id        TEXT NOT NULL,
    user_id             TEXT NOT NULL,
    acknowledged_at     TEXT NOT NULL,  -- ISO 8601
    checkbox_confirmed  INTEGER NOT NULL DEFAULT 0,  -- Boolean
    consulted_doctor    INTEGER NOT NULL DEFAULT 0,  -- Boolean
    notes               TEXT,
    sync_status         TEXT NOT NULL DEFAULT 'PENDING',
    last_sync_version   INTEGER DEFAULT 0,
    created_at          TEXT NOT NULL,
    FOREIGN KEY (detection_id) REFERENCES cli_detected_interactions(detection_id)
);

-- Indices
CREATE UNIQUE INDEX idx_ack_detection ON cli_interaction_acknowledgments(detection_id);
CREATE INDEX idx_ack_user ON cli_interaction_acknowledgments(user_id);
CREATE INDEX idx_ack_sync ON cli_interaction_acknowledgments(sync_status) WHERE sync_status = 'PENDING';

4.3.3. Realm (iOS)

// Realm Model (iOS)
class InteractionAcknowledgment: Object {
    @Persisted(primaryKey: true) var ackId: String
    @Persisted var detectionId: String
    @Persisted var userId: String
    @Persisted var acknowledgedAt: Date
    @Persisted var checkboxConfirmed: Bool
    @Persisted var consultedDoctor: Bool
    @Persisted var notes: String?
    @Persisted var syncStatus: String
    @Persisted var lastSyncVersion: Int
    @Persisted var createdAt: Date
}

4.3.4. Room (Android)

// Room Entity (Android)
@Entity(
    tableName = "cli_interaction_acknowledgments",
    foreignKeys = [
        ForeignKey(
            entity = DetectedInteraction::class,
            parentColumns = ["detection_id"],
            childColumns = ["detection_id"]
        )
    ],
    indices = [
        Index(value = ["detection_id"], unique = true),
        Index(value = ["user_id"]),
        Index(value = ["sync_status"])
    ]
)
data class InteractionAcknowledgment(
    @PrimaryKey val ackId: String,
    @ColumnInfo(name = "detection_id") val detectionId: String,
    @ColumnInfo(name = "user_id") val userId: String,
    @ColumnInfo(name = "acknowledged_at") val acknowledgedAt: String,
    @ColumnInfo(name = "checkbox_confirmed") val checkboxConfirmed: Boolean,
    @ColumnInfo(name = "consulted_doctor") val consultedDoctor: Boolean,
    val notes: String?,
    @ColumnInfo(name = "sync_status") val syncStatus: String,
    @ColumnInfo(name = "last_sync_version") val lastSyncVersion: Int,
    @ColumnInfo(name = "created_at") val createdAt: String
)

4.4. cli_interaction_history

Historial de alertas de interacciones mostradas al usuario (retencion 6 anos HIPAA).

Clasificacion: SYNCED_E2E (PHI)

4.4.1. Estructura (Conceptual)

Campo Tipo Descripcion
history_id UUID Identificador unico
user_id UUID FK a usuarios
detection_id UUID FK a cli_detected_interactions
alert_type Enum BLOCKING, PROMINENT, WARNING, INFO
severity Enum CONTRAINDICADO, SERIO, MODERADO, MENOR
shown_at DateTime Timestamp de cuando se mostro
dismissed_at DateTime Timestamp de cuando se descarto
user_action Enum ACKNOWLEDGED, DISMISSED, CONSULTED_DOCTOR, IGNORED
sync_status Enum PENDING, SYNCED, CONFLICT
last_sync_version Int Version de sincronizacion
created_at DateTime Timestamp creacion

4.4.2. SQLite (iOS/Android)

-- Cliente SQLite
CREATE TABLE cli_interaction_history (
    history_id          TEXT PRIMARY KEY,
    user_id             TEXT NOT NULL,
    detection_id        TEXT NOT NULL,
    alert_type          TEXT NOT NULL CHECK (alert_type IN ('BLOCKING', 'PROMINENT', 'WARNING', 'INFO')),
    severity            TEXT NOT NULL,
    shown_at            TEXT NOT NULL,  -- ISO 8601
    dismissed_at        TEXT,
    user_action         TEXT CHECK (user_action IN ('ACKNOWLEDGED', 'DISMISSED', 'CONSULTED_DOCTOR', 'IGNORED')),
    sync_status         TEXT NOT NULL DEFAULT 'PENDING',
    last_sync_version   INTEGER DEFAULT 0,
    created_at          TEXT NOT NULL,
    FOREIGN KEY (detection_id) REFERENCES cli_detected_interactions(detection_id)
);

-- Indices
CREATE INDEX idx_history_user ON cli_interaction_history(user_id);
CREATE INDEX idx_history_detection ON cli_interaction_history(detection_id);
CREATE INDEX idx_history_shown ON cli_interaction_history(shown_at DESC);
CREATE INDEX idx_history_sync ON cli_interaction_history(sync_status) WHERE sync_status = 'PENDING';

-- Politica de retencion: 6 anos (HIPAA)
-- Se implementa mediante job que elimina registros > 6 anos

4.4.3. Realm (iOS)

// Realm Model (iOS)
class InteractionHistory: Object {
    @Persisted(primaryKey: true) var historyId: String
    @Persisted var userId: String
    @Persisted var detectionId: String
    @Persisted var alertType: String // BLOCKING, PROMINENT, WARNING, INFO
    @Persisted var severity: String
    @Persisted var shownAt: Date
    @Persisted var dismissedAt: Date?
    @Persisted var userAction: String?
    @Persisted var syncStatus: String
    @Persisted var lastSyncVersion: Int
    @Persisted var createdAt: Date
}

4.4.4. Room (Android)

// Room Entity (Android)
@Entity(
    tableName = "cli_interaction_history",
    foreignKeys = [
        ForeignKey(
            entity = DetectedInteraction::class,
            parentColumns = ["detection_id"],
            childColumns = ["detection_id"]
        )
    ],
    indices = [
        Index(value = ["user_id"]),
        Index(value = ["detection_id"]),
        Index(value = ["shown_at"]),
        Index(value = ["sync_status"])
    ]
)
data class InteractionHistory(
    @PrimaryKey val historyId: String,
    @ColumnInfo(name = "user_id") val userId: String,
    @ColumnInfo(name = "detection_id") val detectionId: String,
    @ColumnInfo(name = "alert_type") val alertType: String,
    val severity: String,
    @ColumnInfo(name = "shown_at") val shownAt: String,
    @ColumnInfo(name = "dismissed_at") val dismissedAt: String?,
    @ColumnInfo(name = "user_action") val userAction: String?,
    @ColumnInfo(name = "sync_status") val syncStatus: String,
    @ColumnInfo(name = "last_sync_version") val lastSyncVersion: Int,
    @ColumnInfo(name = "created_at") val createdAt: String
)

Registro del consentimiento del usuario para disclaimer de privacidad en busquedas online.

Clasificacion: SYNCED_E2E (PHI - vinculado a usuario)

4.5.1. Estructura (Conceptual)

Campo Tipo Descripcion
consent_id UUID Identificador unico
user_id UUID FK a usuarios (nullable para Free sin cuenta)
consent_type Enum AUTOMATIC_SEARCH, MANUAL_SEARCH
consented_at DateTime Timestamp de cuando acepto
ip_hash String Blind index del IP (HMAC-SHA256)
app_version String Version de la app
sync_status Enum PENDING, SYNCED, CONFLICT
last_sync_version Int Version de sincronizacion
created_at DateTime Timestamp creacion

4.5.2. SQLite (iOS/Android)

-- Cliente SQLite
CREATE TABLE cli_interaction_consent (
    consent_id          TEXT PRIMARY KEY,
    user_id             TEXT,  -- Nullable para Free sin cuenta
    consent_type        TEXT NOT NULL CHECK (consent_type IN ('AUTOMATIC_SEARCH', 'MANUAL_SEARCH')),
    consented_at        TEXT NOT NULL,  -- ISO 8601
    ip_hash             TEXT,  -- Blind index
    app_version         TEXT NOT NULL,
    sync_status         TEXT NOT NULL DEFAULT 'PENDING',
    last_sync_version   INTEGER DEFAULT 0,
    created_at          TEXT NOT NULL
);

-- Indices
CREATE INDEX idx_consent_user ON cli_interaction_consent(user_id);
CREATE INDEX idx_consent_type ON cli_interaction_consent(consent_type);
CREATE INDEX idx_consent_sync ON cli_interaction_consent(sync_status) WHERE sync_status = 'PENDING';

4.5.3. Realm (iOS)

// Realm Model (iOS)
class InteractionConsent: Object {
    @Persisted(primaryKey: true) var consentId: String
    @Persisted var userId: String?  // Nullable para Free sin cuenta
    @Persisted var consentType: String // AUTOMATIC_SEARCH, MANUAL_SEARCH
    @Persisted var consentedAt: Date
    @Persisted var ipHash: String?
    @Persisted var appVersion: String
    @Persisted var syncStatus: String
    @Persisted var lastSyncVersion: Int
    @Persisted var createdAt: Date
}

4.5.4. Room (Android)

// Room Entity (Android)
@Entity(
    tableName = "cli_interaction_consent",
    indices = [
        Index(value = ["user_id"]),
        Index(value = ["consent_type"]),
        Index(value = ["sync_status"])
    ]
)
data class InteractionConsent(
    @PrimaryKey val consentId: String,
    @ColumnInfo(name = "user_id") val userId: String?,  // Nullable para Free sin cuenta
    @ColumnInfo(name = "consent_type") val consentType: String,
    @ColumnInfo(name = "consented_at") val consentedAt: String,
    @ColumnInfo(name = "ip_hash") val ipHash: String?,
    @ColumnInfo(name = "app_version") val appVersion: String,
    @ColumnInfo(name = "sync_status") val syncStatus: String,
    @ColumnInfo(name = "last_sync_version") val lastSyncVersion: Int,
    @ColumnInfo(name = "created_at") val createdAt: String
)

4.6. cli_pending_verifications

Cola temporal de verificaciones pendientes cuando DrugBank API falla (fallback).

CUMPLE REGLA 100: Esta tabla es una cola temporal limitada a max 10 verificaciones pendientes. No es un catalogo que se descarga masivamente.

Clasificacion: LOCAL_ONLY (no se sincroniza)

4.6.1. Estructura (Conceptual)

Campo Tipo Descripcion
verification_id UUID Identificador unico
medication_a_id UUID FK a medicamentos del usuario
medication_b_id UUID FK a medicamentos del usuario
added_at DateTime Timestamp de cuando se agrego a la cola
last_attempt_at DateTime Ultimo intento de verificacion
attempts Int Numero de reintentos (max 5)
status Enum PENDING, VERIFIED, FAILED
error_type Enum TIMEOUT, API_ERROR, RATE_LIMIT, NO_CONNECTION
next_retry_at DateTime Timestamp de proximo reintento
created_at DateTime Timestamp creacion

4.6.2. SQLite (iOS/Android)

-- Cliente SQLite
CREATE TABLE cli_pending_verifications (
    verification_id     TEXT PRIMARY KEY,
    medication_a_id     TEXT NOT NULL,
    medication_b_id     TEXT NOT NULL,
    added_at            TEXT NOT NULL,  -- ISO 8601
    last_attempt_at     TEXT,
    attempts            INTEGER NOT NULL DEFAULT 0,
    status              TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING', 'VERIFIED', 'FAILED')),
    error_type          TEXT CHECK (error_type IN ('TIMEOUT', 'API_ERROR', 'RATE_LIMIT', 'NO_CONNECTION')),
    next_retry_at       TEXT,
    created_at          TEXT NOT NULL,
    FOREIGN KEY (medication_a_id) REFERENCES cli_medications(medication_id),
    FOREIGN KEY (medication_b_id) REFERENCES cli_medications(medication_id),
    UNIQUE(medication_a_id, medication_b_id)
);

-- Indices
CREATE INDEX idx_verification_status ON cli_pending_verifications(status) WHERE status = 'PENDING';
CREATE INDEX idx_verification_retry ON cli_pending_verifications(next_retry_at) WHERE status = 'PENDING';

4.6.3. Realm (iOS)

// Realm Model (iOS)
class PendingVerification: Object {
    @Persisted(primaryKey: true) var verificationId: String
    @Persisted var medicationAId: String
    @Persisted var medicationBId: String
    @Persisted var addedAt: Date
    @Persisted var lastAttemptAt: Date?
    @Persisted var attempts: Int
    @Persisted var status: String // PENDING, VERIFIED, FAILED
    @Persisted var errorType: String?
    @Persisted var nextRetryAt: Date?
    @Persisted var createdAt: Date
}

4.6.4. Room (Android)

// Room Entity (Android)
@Entity(
    tableName = "cli_pending_verifications",
    foreignKeys = [
        ForeignKey(
            entity = Medication::class,
            parentColumns = ["medication_id"],
            childColumns = ["medication_a_id"]
        ),
        ForeignKey(
            entity = Medication::class,
            parentColumns = ["medication_id"],
            childColumns = ["medication_b_id"]
        )
    ],
    indices = [
        Index(value = ["medication_a_id", "medication_b_id"], unique = true),
        Index(value = ["status"]),
        Index(value = ["next_retry_at"])
    ]
)
data class PendingVerification(
    @PrimaryKey val verificationId: String,
    @ColumnInfo(name = "medication_a_id") val medicationAId: String,
    @ColumnInfo(name = "medication_b_id") val medicationBId: String,
    @ColumnInfo(name = "added_at") val addedAt: String,
    @ColumnInfo(name = "last_attempt_at") val lastAttemptAt: String?,
    val attempts: Int,
    val status: String,
    @ColumnInfo(name = "error_type") val errorType: String?,
    @ColumnInfo(name = "next_retry_at") val nextRetryAt: String?,
    @ColumnInfo(name = "created_at") val createdAt: String
)

5. Mapeo de Sincronizacion

5.1. Cache Critico de Interacciones (Embebido en App)

Campo LOCAL → SERVIDOR Formato
cli_critical_interactions_cache N/A EMBEBIDO en codigo (NO sincroniza)

Flujo:

  1. Cache de <100 interacciones CONTRAINDICADAS esta embebido en el codigo de la app
  2. Se actualiza solo con nuevas versiones de la app (NO OTA)
  3. NO hay sincronizacion dinamica (cumple Regla 100)

Busquedas Online (Pro/Perfect):

  1. Usuario con >2 medicamentos activos puede optar por busqueda online
  2. Requiere aceptar disclaimer de privacidad (cli_interaction_consent)
  3. Busqueda se hace contra srv_interaction_catalog via API
  4. Resultados se guardan en cli_detected_interactions (cifrado E2E)

5.2. Detecciones de Usuario (Bidireccional E2E)

Campo LOCAL → SERVIDOR Formato
detection_id, medication_a_id, medication_b_id, severity, status, detected_at encrypted_data Blob E2E (AES-256-GCM)
sync_status N/A LOCAL_ONLY
last_sync_version sync_version Plaintext (metadata)

Flujo de Sync:

PUSH (Cliente → Servidor):
1. Filtrar detecciones con sync_status = PENDING
2. Cifrar blob: {detection_id, medication_a_id, medication_b_id, severity, status, detected_at, ...}
3. POST /v1/sync/interactions/push con encrypted_blob
4. Actualizar sync_status = SYNCED, last_sync_version

PULL (Servidor → Cliente):
1. GET /v1/sync/interactions/pull?since=<last_sync_version>
2. Recibir blobs cifrados
3. Descifrar con master_key local
4. Merge con detecciones locales (resolver conflictos)

5.3. Confirmaciones (Bidireccional E2E)

Campo LOCAL → SERVIDOR Formato
ack_id, detection_id, acknowledged_at, checkbox_confirmed, consulted_doctor, notes encrypted_data Blob E2E
sync_status N/A LOCAL_ONLY
last_sync_version sync_version Plaintext

Igual que detecciones: Push/Pull bidireccional con cifrado E2E.


5.4. Historial (Bidireccional E2E)

Campo LOCAL → SERVIDOR Formato
history_id, detection_id, alert_type, severity, shown_at, dismissed_at, user_action encrypted_data Blob E2E
sync_status N/A LOCAL_ONLY
last_sync_version sync_version Plaintext

Retencion: 6 anos (HIPAA). El servidor mantiene blobs cifrados por 6 anos.


5.5. Consentimiento de Busqueda (Bidireccional E2E)

Campo LOCAL → SERVIDOR Formato
consent_id, user_id, consent_type, consented_at, app_version encrypted_data Blob E2E
ip_hash blind_index HMAC-SHA256
sync_status N/A LOCAL_ONLY
last_sync_version sync_version Plaintext

Flujo:

  1. Usuario inicia busqueda online de interacciones
  2. Si no ha aceptado disclaimer, se muestra dialogo de consentimiento
  3. Al aceptar, se crea registro en cli_interaction_consent
  4. Se sincroniza cifrado E2E al servidor (obligacion legal)
  5. ip_hash se usa como blind index (no vincula a usuario, solo metadata)

6. Diagrama ER del Modulo

6.1. Servidor

+--------------------------------+
|  srv_interaction_catalog       |
+--------------------------------+
| PK: interaction_id             |
| FK: drug_a_id → srv_drug_catalog |
| FK: drug_b_id → srv_drug_catalog |
|     principle_a                |
|     principle_b                |
|     severity                   |
|     severity_code              |
|     ...                        |
+--------------------------------+
           ↓ (referenciado por hash)
+--------------------------------+
|  srv_drugbank_cache            |
+--------------------------------+
| PK: cache_id                   |
|     drug_pair_hash (blind idx) |
|     tier                       |
|     interactions_found         |
|     max_severity_code          |
|     expires_at                 |
|     ...                        |
+--------------------------------+

+--------------------------------+
|  srv_ia_analysis_log           |
+--------------------------------+
| PK: log_id                     |
|     session_token (efimero)    |
|     num_medications            |
|     tokens_total               |
|     ...                        |
+--------------------------------+

6.2. Cliente

+----------------------------------------+
| cli_critical_interactions_cache        |
| (<100 CONTRAINDICADAS - EMBEBIDO)      |
+----------------------------------------+
| PK: cache_id                           |
|     drug_a_id                          |
|     drug_b_id                          |
|     severity = 'CONTRAINDICADO'        |
|     severity_code = 4                  |
|     embedded_version                   |
|     ...                                |
+----------------------------------------+
        ↓ (referenciado por)
+----------------------------------------+
| cli_detected_interactions              |
| (PHI - SYNCED_E2E)                     |
+----------------------------------------+
| PK: detection_id                       |
| FK: medication_a_id → cli_medications  |
| FK: medication_b_id → cli_medications  |
| FK: interaction_cache_id (optional)    |
|     severity                           |
|     status                             |
|     sync_status                        |
|     ...                                |
+----------------------------------------+
        ↓ (1:1)
+----------------------------------------+
| cli_interaction_acknowledgments        |
| (PHI - SYNCED_E2E)                     |
+----------------------------------------+
| PK: ack_id                             |
| FK: detection_id (unique)              |
|     acknowledged_at                    |
|     checkbox_confirmed                 |
|     consulted_doctor                   |
|     ...                                |
+----------------------------------------+

+----------------------------------------+
| cli_interaction_history                |
| (PHI - SYNCED_E2E - 6 anos HIPAA)      |
+----------------------------------------+
| PK: history_id                         |
| FK: detection_id                       |
|     alert_type                         |
|     shown_at                           |
|     ...                                |
+----------------------------------------+

+----------------------------------------+
| cli_interaction_consent (NUEVO)        |
| (PHI - SYNCED_E2E)                     |
+----------------------------------------+
| PK: consent_id                         |
|     user_id (nullable)                 |
|     consent_type                       |
|     consented_at                       |
|     ip_hash (blind index)              |
|     ...                                |
+----------------------------------------+

+----------------------------------------+
| cli_pending_verifications              |
| (LOCAL_ONLY - max 10 registros)        |
+----------------------------------------+
| PK: verification_id                    |
| FK: medication_a_id                    |
| FK: medication_b_id                    |
|     status                             |
|     attempts                           |
|     ...                                |
+----------------------------------------+

7. Consideraciones de Cifrado

7.1. Campos Cifrados E2E

Las siguientes entidades se cifran COMPLETAS antes de sincronizacion:

Entidad Campos Cifrados
cli_detected_interactions TODO el registro (detection_id, medication_a_id, medication_b_id, severity, status, etc.)
cli_interaction_acknowledgments TODO el registro (ack_id, detection_id, acknowledged_at, checkbox_confirmed, consulted_doctor, notes)
cli_interaction_history TODO el registro (history_id, detection_id, alert_type, severity, shown_at, dismissed_at, user_action)

Algoritmo: AES-256-GCM

Proceso:

  1. Serializar registro completo a JSON
  2. Generar nonce aleatorio (96 bits)
  3. Cifrar con master_key del usuario
  4. Empaquetar: nonce || ciphertext || auth_tag
  5. Calcular SHA-256 del blob cifrado (integridad)

Servidor recibe:

{
  "entity_type": "detected_interaction",
  "entity_id": "uuid",
  "encrypted_blob": "base64...",
  "blob_hash": "sha256:abc123...",
  "sync_version": 123456,
  "updated_at": "2025-12-08T10:30:00Z"
}

7.2. Blind Indexes (No Aplica)

NO se usan blind indexes para interacciones detectadas porque:

  1. No hay necesidad de busqueda por parte del servidor
  2. El servidor NUNCA necesita saber que medicamentos especificos tienen interacciones
  3. Toda la deteccion es LOCAL en el dispositivo

Excepcion: srv_drugbank_cache.drug_pair_hash es un blind index, pero NO contiene datos del usuario (solo metadata de consultas API).

7.3. Proceso de Sincronizacion

flowchart TD
    A[Usuario agrega medicamento B] --> B[Motor local detecta interaccion con medicamento A]
    B --> C[Crear registro cli_detected_interactions]
    C --> D[sync_status = PENDING]

    D --> E{Hay conexion?}
    E -->|No| F[Mantener PENDING, reintentar luego]
    E -->|Si| G[Serializar registro a JSON]

    G --> H[Obtener master_key de Keychain]
    H --> I[Cifrar JSON con AES-256-GCM]
    I --> J[Calcular SHA-256 del blob]

    J --> K[POST /v1/sync/interactions/push]
    K --> L{Respuesta 200?}

    L -->|Si| M[sync_status = SYNCED]
    L -->|Conflicto 409| N[Resolver conflicto manual]
    L -->|Error 5xx| O[Reintentar con backoff]

    M --> P[last_sync_version++]

8. Reglas de Validacion

8.1. Nivel Servidor

Regla Descripcion
VR-INT-001 interaction_id debe ser UUID valido
VR-INT-002 drug_a_id < drug_b_id (evitar duplicados A-B == B-A)
VR-INT-003 drug_a_id != drug_b_id
VR-INT-004 severity_code debe estar entre 1-4
VR-INT-005 expires_at en srv_drugbank_cache debe ser futura
VR-INT-006 session_token en srv_ia_analysis_log debe ser unico por consulta

8.2. Nivel Cliente

Regla Descripcion
VR-INT-010 Antes de agregar a cli_detected_interactions, verificar que medicamentos existan en cli_medications
VR-INT-011 status = ACKNOWLEDGED requiere registro en cli_interaction_acknowledgments
VR-INT-012 severity = CONTRAINDICADO requiere checkbox_confirmed = TRUE en acknowledgment
VR-INT-013 cli_pending_verifications.attempts no debe exceder 5
VR-INT-014 cli_interaction_history debe registrarse SIEMPRE que se muestre alerta

9. Politicas RLS (Row Level Security)

9.1. srv_interaction_catalog (Sin RLS)

Razon: Catalogo publico, accesible sin autenticacion.

-- No RLS policies needed
-- Public catalog, read-only access for all authenticated users

9.2. srv_drugbank_cache

Razon: Metadata operativa, solo backend puede leer/escribir.

-- Enable RLS
ALTER TABLE srv_drugbank_cache ENABLE ROW LEVEL SECURITY;

-- Policy: Solo backend puede leer/escribir
CREATE POLICY backend_only_drugbank_cache ON srv_drugbank_cache
    FOR ALL
    USING (false)  -- Usuarios no pueden acceder
    WITH CHECK (false);

-- Grant para service role (backend)
GRANT ALL ON srv_drugbank_cache TO service_role;

9.3. srv_ia_analysis_log

Razon: Log interno, solo backend.

-- Enable RLS
ALTER TABLE srv_ia_analysis_log ENABLE ROW LEVEL SECURITY;

-- Policy: Solo backend
CREATE POLICY backend_only_ia_log ON srv_ia_analysis_log
    FOR ALL
    USING (false)
    WITH CHECK (false);

-- Grant para service role
GRANT ALL ON srv_ia_analysis_log TO service_role;

Nota sobre datos sincronizados E2E:

Los datos de usuario (cli_detected_interactions, cli_interaction_acknowledgments, cli_interaction_history) NO se almacenan en tablas individuales en el servidor. Se almacenan en la tabla generica encrypted_user_data (ver MDL-SYNC-001) como blobs cifrados.

Por lo tanto, NO requieren RLS especificas aqui. Las RLS de encrypted_user_data aplican:

-- Politica en encrypted_user_data (de MDL-SYNC-001)
CREATE POLICY user_encrypted_data_policy ON encrypted_user_data
    FOR ALL
    USING (user_id = auth.uid())
    WITH CHECK (user_id = auth.uid());

10. Queries de Ejemplo

10.1. Buscar Interaccion CRITICA en Cache Local (<100)

-- SQLite (Cliente)
-- Buscar interaccion CONTRAINDICADA en cache embebido
-- NOTA: Solo <100 interacciones criticas estan embebidas
SELECT
    cache_id,
    severity,
    severity_code,
    description,
    mechanism,
    clinical_recommendation,
    suggested_action,
    primary_source
FROM cli_critical_interactions_cache
WHERE (drug_a_id = ? AND drug_b_id = ?)
   OR (drug_a_id = ? AND drug_b_id = ?)
LIMIT 1;
-- SQLite (Cliente)
-- Buscar interaccion CONTRAINDICADA por principios activos
SELECT
    cache_id,
    severity,
    severity_code,
    description,
    clinical_recommendation
FROM cli_critical_interactions_cache
WHERE (principle_a = ? AND principle_b = ?)
   OR (principle_a = ? AND principle_b = ?)
LIMIT 1;
-- SQLite (Cliente)
-- Verificar si usuario ha dado consentimiento para busqueda online
SELECT consent_id, consented_at
FROM cli_interaction_consent
WHERE user_id = ?
  AND consent_type = 'AUTOMATIC_SEARCH'
ORDER BY consented_at DESC
LIMIT 1;

10.2. Detectar Interacciones para Medicamentos del Usuario

-- SQLite (Cliente)
-- Algoritmo O(n^2): comparar todos los pares de medicamentos activos
WITH active_meds AS (
    SELECT medication_id, drug_id, principle_active
    FROM cli_medications
    WHERE user_id = ?
      AND status = 'ACTIVE'
      AND deleted_at IS NULL
)
SELECT
    a.medication_id AS med_a_id,
    b.medication_id AS med_b_id,
    ic.cache_id,
    ic.severity,
    ic.severity_code,
    ic.description,
    ic.clinical_recommendation
FROM active_meds a
CROSS JOIN active_meds b
INNER JOIN cli_critical_interactions_cache ic
    ON (
        (ic.principle_a = a.principle_active AND ic.principle_b = b.principle_active)
        OR
        (ic.principle_a = b.principle_active AND ic.principle_b = a.principle_active)
    )
WHERE a.medication_id < b.medication_id  -- Evitar duplicados y self-join
ORDER BY ic.severity_code DESC;

10.3. Obtener Historial de Alertas (6 anos HIPAA)

-- SQLite (Cliente)
-- Historial de alertas de interacciones en los ultimos 6 anos
SELECT
    h.history_id,
    h.detection_id,
    h.alert_type,
    h.severity,
    h.shown_at,
    h.dismissed_at,
    h.user_action,
    d.medication_a_id,
    d.medication_b_id
FROM cli_interaction_history h
INNER JOIN cli_detected_interactions d
    ON h.detection_id = d.detection_id
WHERE h.user_id = ?
  AND h.shown_at >= datetime('now', '-6 years')
ORDER BY h.shown_at DESC;
-- SQLite (Cliente)
-- Cleanup de historial > 6 anos (ejecutar periodicamente)
DELETE FROM cli_interaction_history
WHERE shown_at < datetime('now', '-6 years');

10.4. Verificaciones Pendientes (Fallback)

-- SQLite (Cliente)
-- Obtener verificaciones pendientes para reintentar
SELECT
    verification_id,
    medication_a_id,
    medication_b_id,
    attempts,
    next_retry_at
FROM cli_pending_verifications
WHERE status = 'PENDING'
  AND attempts < 5
  AND next_retry_at <= datetime('now')
ORDER BY added_at ASC
LIMIT 10;
-- SQLite (Cliente)
-- Actualizar intento fallido
UPDATE cli_pending_verifications
SET
    last_attempt_at = datetime('now'),
    attempts = attempts + 1,
    error_type = ?,
    next_retry_at = datetime('now', '+1 hour')  -- Backoff exponencial
WHERE verification_id = ?;
-- SQLite (Cliente)
-- Marcar como verificado exitosamente
UPDATE cli_pending_verifications
SET
    status = 'VERIFIED',
    last_attempt_at = datetime('now')
WHERE verification_id = ?;

10.5. Consultar Cache DrugBank (Servidor)

-- PostgreSQL (Servidor)
-- Buscar en cache de DrugBank por hash de par
SELECT
    cache_id,
    interactions_found,
    max_severity_code,
    queried_at,
    expires_at
FROM srv_drugbank_cache
WHERE drug_pair_hash = ?
  AND expires_at > NOW()
LIMIT 1;
-- PostgreSQL (Servidor)
-- Insertar resultado de consulta DrugBank
INSERT INTO srv_drugbank_cache (
    drug_pair_hash,
    tier,
    http_status,
    response_time_ms,
    interactions_found,
    max_severity_code,
    queried_at,
    source_version
) VALUES (?, ?, ?, ?, ?, ?, NOW(), ?)
ON CONFLICT (drug_pair_hash)
DO UPDATE SET
    http_status = EXCLUDED.http_status,
    response_time_ms = EXCLUDED.response_time_ms,
    interactions_found = EXCLUDED.interactions_found,
    max_severity_code = EXCLUDED.max_severity_code,
    queried_at = NOW(),
    expires_at = NOW() + INTERVAL '7 days';

11. Migraciones

11.1. Migration 001: Create Interactions Schema (Servidor)

-- Migration: 001_create_interactions_schema.sql
-- Version: 1.0.0
-- Date: 2025-12-08
-- Author: DatabaseDrone

BEGIN;

-- ====================
-- srv_interaction_catalog
-- ====================
CREATE TABLE srv_interaction_catalog (
    interaction_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    drug_a_id           UUID NOT NULL,
    drug_b_id           UUID NOT NULL,
    principle_a         VARCHAR(200) NOT NULL,
    principle_b         VARCHAR(200) NOT NULL,
    severity            VARCHAR(20) NOT NULL CHECK (severity IN ('CONTRAINDICADO', 'SERIO', 'MODERADO', 'MENOR')),
    severity_code       SMALLINT NOT NULL CHECK (severity_code BETWEEN 1 AND 4),
    interaction_type    VARCHAR(20) NOT NULL CHECK (interaction_type IN ('FARMACOCINETICA', 'FARMACODINAMICA', 'MIXTA')),
    category            VARCHAR(100),
    description         TEXT NOT NULL,
    mechanism           TEXT,
    expected_effect     TEXT,
    possible_symptoms   TEXT[],
    clinical_recommendation TEXT,
    suggested_action    VARCHAR(20) CHECK (suggested_action IN ('EVITAR', 'MONITOREAR', 'AJUSTAR_DOSIS', 'ESPACIAR', 'INFORMAR')),
    monitoring_required TEXT,
    primary_source      VARCHAR(100) NOT NULL,
    additional_sources  TEXT[],
    evidence_level      VARCHAR(10) CHECK (evidence_level IN ('ALTA', 'MEDIA', 'BAJA')),
    pubmed_refs         TEXT[],
    last_updated_date   DATE NOT NULL,
    applicable_countries TEXT[] DEFAULT ARRAY['MX', 'US', 'BR'],
    regional_notes      JSONB,
    is_active           BOOLEAN NOT NULL DEFAULT TRUE,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT unique_pair CHECK (drug_a_id < drug_b_id),
    CONSTRAINT different_drugs CHECK (drug_a_id != drug_b_id)
);

CREATE INDEX idx_interaction_drugs ON srv_interaction_catalog(drug_a_id, drug_b_id);
CREATE INDEX idx_interaction_drugs_reverse ON srv_interaction_catalog(drug_b_id, drug_a_id);
CREATE INDEX idx_interaction_principles ON srv_interaction_catalog(principle_a, principle_b);
CREATE INDEX idx_interaction_severity ON srv_interaction_catalog(severity_code DESC);
CREATE INDEX idx_interaction_category ON srv_interaction_catalog(category) WHERE category IS NOT NULL;
CREATE INDEX idx_interaction_active ON srv_interaction_catalog(is_active) WHERE is_active = TRUE;
CREATE INDEX idx_interaction_updated ON srv_interaction_catalog(updated_at DESC);
CREATE INDEX idx_interaction_search ON srv_interaction_catalog USING GIN(to_tsvector('spanish', description));

-- Trigger para updated_at
CREATE OR REPLACE FUNCTION update_interaction_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_interaction_updated_at
    BEFORE UPDATE ON srv_interaction_catalog
    FOR EACH ROW
    EXECUTE FUNCTION update_interaction_updated_at();

-- ====================
-- srv_drugbank_cache
-- ====================
CREATE TABLE srv_drugbank_cache (
    cache_id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    drug_pair_hash      VARCHAR(64) NOT NULL UNIQUE,
    tier                VARCHAR(10) NOT NULL CHECK (tier IN ('Pro', 'Perfect')),
    http_status         SMALLINT NOT NULL,
    response_time_ms    INT,
    tokens_used         INT,
    interactions_found  INT NOT NULL DEFAULT 0,
    max_severity_code   SMALLINT CHECK (max_severity_code BETWEEN 0 AND 4),
    queried_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at          TIMESTAMPTZ NOT NULL,
    source_version      VARCHAR(20),
    is_error            BOOLEAN NOT NULL DEFAULT FALSE,
    error_message       TEXT,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE UNIQUE INDEX idx_drugbank_cache_hash ON srv_drugbank_cache(drug_pair_hash);
CREATE INDEX idx_drugbank_cache_expires ON srv_drugbank_cache(expires_at);
CREATE INDEX idx_drugbank_cache_tier ON srv_drugbank_cache(tier, queried_at);
CREATE INDEX idx_drugbank_cache_errors ON srv_drugbank_cache(is_error) WHERE is_error = TRUE;

-- Trigger para expires_at
CREATE OR REPLACE FUNCTION set_drugbank_cache_expiry()
RETURNS TRIGGER AS $$
BEGIN
    NEW.expires_at = NEW.queried_at + INTERVAL '7 days';
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_drugbank_cache_expiry
    BEFORE INSERT ON srv_drugbank_cache
    FOR EACH ROW
    WHEN (NEW.expires_at IS NULL)
    EXECUTE FUNCTION set_drugbank_cache_expiry();

-- Funcion de limpieza
CREATE OR REPLACE FUNCTION cleanup_expired_drugbank_cache()
RETURNS void AS $$
BEGIN
    DELETE FROM srv_drugbank_cache WHERE expires_at < NOW();
END;
$$ LANGUAGE plpgsql;

-- ====================
-- srv_ia_analysis_log
-- ====================
CREATE TABLE srv_ia_analysis_log (
    log_id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    session_token       VARCHAR(64) NOT NULL,
    num_medications     SMALLINT NOT NULL,
    severity_detected   VARCHAR(20),
    request_time_ms     INT,
    tokens_input        INT,
    tokens_output       INT,
    tokens_total        INT,
    http_status         SMALLINT NOT NULL,
    is_error            BOOLEAN NOT NULL DEFAULT FALSE,
    error_type          VARCHAR(50),
    analyzed_at         TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    anthropic_version   VARCHAR(20),
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_ia_log_analyzed ON srv_ia_analysis_log(analyzed_at DESC);
CREATE INDEX idx_ia_log_errors ON srv_ia_analysis_log(is_error) WHERE is_error = TRUE;
CREATE INDEX idx_ia_log_tokens ON srv_ia_analysis_log(tokens_total) WHERE tokens_total IS NOT NULL;

-- Funcion de limpieza (90 dias)
CREATE OR REPLACE FUNCTION cleanup_old_ia_logs()
RETURNS void AS $$
BEGIN
    DELETE FROM srv_ia_analysis_log WHERE analyzed_at < NOW() - INTERVAL '90 days';
END;
$$ LANGUAGE plpgsql;

-- ====================
-- RLS Policies
-- ====================

-- srv_interaction_catalog: publico, sin RLS
-- (usuarios autenticados pueden leer)

-- srv_drugbank_cache: solo backend
ALTER TABLE srv_drugbank_cache ENABLE ROW LEVEL SECURITY;
CREATE POLICY backend_only_drugbank_cache ON srv_drugbank_cache FOR ALL USING (false) WITH CHECK (false);
GRANT ALL ON srv_drugbank_cache TO service_role;

-- srv_ia_analysis_log: solo backend
ALTER TABLE srv_ia_analysis_log ENABLE ROW LEVEL SECURITY;
CREATE POLICY backend_only_ia_log ON srv_ia_analysis_log FOR ALL USING (false) WITH CHECK (false);
GRANT ALL ON srv_ia_analysis_log TO service_role;

COMMIT;

11.2. Migration 001: Create Interactions Schema (Cliente SQLite)

-- Migration: 001_create_interactions_schema_client.sql
-- Version: 2.0.0
-- Date: 2025-12-08
-- Platform: SQLite (iOS/Android)
-- Author: DatabaseDrone
-- DIRECTIVA DEL DIRECTOR: Regla 100 Registros

BEGIN TRANSACTION;

-- ====================
-- cli_critical_interactions_cache
-- DIRECTIVA: <100 CONTRAINDICADAS (embebido en app)
-- ====================
CREATE TABLE cli_critical_interactions_cache (
    cache_id            TEXT PRIMARY KEY,
    drug_a_id           TEXT NOT NULL,
    drug_b_id           TEXT NOT NULL,
    principle_a         TEXT NOT NULL,
    principle_b         TEXT NOT NULL,
    severity            TEXT NOT NULL DEFAULT 'CONTRAINDICADO' CHECK (severity = 'CONTRAINDICADO'),
    severity_code       INTEGER NOT NULL DEFAULT 4 CHECK (severity_code = 4),
    interaction_type    TEXT NOT NULL CHECK (interaction_type IN ('FARMACOCINETICA', 'FARMACODINAMICA', 'MIXTA')),
    category            TEXT,
    description         TEXT NOT NULL,
    mechanism           TEXT,
    expected_effect     TEXT,
    clinical_recommendation TEXT,
    suggested_action    TEXT NOT NULL DEFAULT 'EVITAR' CHECK (suggested_action = 'EVITAR'),
    primary_source      TEXT NOT NULL,
    evidence_level      TEXT CHECK (evidence_level IN ('ALTA', 'MEDIA', 'BAJA')),
    embedded_version    TEXT NOT NULL,
    UNIQUE(drug_a_id, drug_b_id),
    CHECK ((SELECT COUNT(*) FROM cli_critical_interactions_cache) <= 100)
);

CREATE INDEX idx_critical_cache_drugs ON cli_critical_interactions_cache(drug_a_id, drug_b_id);
CREATE INDEX idx_critical_cache_principles ON cli_critical_interactions_cache(principle_a, principle_b);

-- ====================
-- cli_detected_interactions
-- ====================
CREATE TABLE cli_detected_interactions (
    detection_id        TEXT PRIMARY KEY,
    user_id             TEXT NOT NULL,
    medication_a_id     TEXT NOT NULL,
    medication_b_id     TEXT NOT NULL,
    interaction_cache_id TEXT,
    severity            TEXT NOT NULL,
    severity_code       INTEGER NOT NULL,
    status              TEXT NOT NULL CHECK (status IN ('ACTIVE', 'ACKNOWLEDGED', 'RESOLVED', 'DISMISSED')),
    detected_at         TEXT NOT NULL,
    resolved_at         TEXT,
    sync_status         TEXT NOT NULL DEFAULT 'PENDING',
    last_sync_version   INTEGER DEFAULT 0,
    created_at          TEXT NOT NULL,
    updated_at          TEXT NOT NULL,
    FOREIGN KEY (medication_a_id) REFERENCES cli_medications(medication_id),
    FOREIGN KEY (medication_b_id) REFERENCES cli_medications(medication_id),
    FOREIGN KEY (interaction_cache_id) REFERENCES cli_critical_interactions_cache(cache_id)
);

CREATE INDEX idx_detected_user ON cli_detected_interactions(user_id);
CREATE INDEX idx_detected_status ON cli_detected_interactions(status) WHERE status = 'ACTIVE';
CREATE INDEX idx_detected_sync ON cli_detected_interactions(sync_status) WHERE sync_status = 'PENDING';
CREATE INDEX idx_detected_medications ON cli_detected_interactions(medication_a_id, medication_b_id);

-- ====================
-- cli_interaction_acknowledgments
-- ====================
CREATE TABLE cli_interaction_acknowledgments (
    ack_id              TEXT PRIMARY KEY,
    detection_id        TEXT NOT NULL,
    user_id             TEXT NOT NULL,
    acknowledged_at     TEXT NOT NULL,
    checkbox_confirmed  INTEGER NOT NULL DEFAULT 0,
    consulted_doctor    INTEGER NOT NULL DEFAULT 0,
    notes               TEXT,
    sync_status         TEXT NOT NULL DEFAULT 'PENDING',
    last_sync_version   INTEGER DEFAULT 0,
    created_at          TEXT NOT NULL,
    FOREIGN KEY (detection_id) REFERENCES cli_detected_interactions(detection_id)
);

CREATE UNIQUE INDEX idx_ack_detection ON cli_interaction_acknowledgments(detection_id);
CREATE INDEX idx_ack_user ON cli_interaction_acknowledgments(user_id);
CREATE INDEX idx_ack_sync ON cli_interaction_acknowledgments(sync_status) WHERE sync_status = 'PENDING';

-- ====================
-- cli_interaction_history
-- ====================
CREATE TABLE cli_interaction_history (
    history_id          TEXT PRIMARY KEY,
    user_id             TEXT NOT NULL,
    detection_id        TEXT NOT NULL,
    alert_type          TEXT NOT NULL CHECK (alert_type IN ('BLOCKING', 'PROMINENT', 'WARNING', 'INFO')),
    severity            TEXT NOT NULL,
    shown_at            TEXT NOT NULL,
    dismissed_at        TEXT,
    user_action         TEXT CHECK (user_action IN ('ACKNOWLEDGED', 'DISMISSED', 'CONSULTED_DOCTOR', 'IGNORED')),
    sync_status         TEXT NOT NULL DEFAULT 'PENDING',
    last_sync_version   INTEGER DEFAULT 0,
    created_at          TEXT NOT NULL,
    FOREIGN KEY (detection_id) REFERENCES cli_detected_interactions(detection_id)
);

CREATE INDEX idx_history_user ON cli_interaction_history(user_id);
CREATE INDEX idx_history_detection ON cli_interaction_history(detection_id);
CREATE INDEX idx_history_shown ON cli_interaction_history(shown_at DESC);
CREATE INDEX idx_history_sync ON cli_interaction_history(sync_status) WHERE sync_status = 'PENDING';

-- ====================
-- cli_interaction_consent (NUEVO)
-- ====================
CREATE TABLE cli_interaction_consent (
    consent_id          TEXT PRIMARY KEY,
    user_id             TEXT,  -- Nullable para Free sin cuenta
    consent_type        TEXT NOT NULL CHECK (consent_type IN ('AUTOMATIC_SEARCH', 'MANUAL_SEARCH')),
    consented_at        TEXT NOT NULL,
    ip_hash             TEXT,
    app_version         TEXT NOT NULL,
    sync_status         TEXT NOT NULL DEFAULT 'PENDING',
    last_sync_version   INTEGER DEFAULT 0,
    created_at          TEXT NOT NULL
);

CREATE INDEX idx_consent_user ON cli_interaction_consent(user_id);
CREATE INDEX idx_consent_type ON cli_interaction_consent(consent_type);
CREATE INDEX idx_consent_sync ON cli_interaction_consent(sync_status) WHERE sync_status = 'PENDING';

-- ====================
-- cli_pending_verifications
-- DIRECTIVA: Max 10 registros (cumple Regla 100)
-- ====================
CREATE TABLE cli_pending_verifications (
    verification_id     TEXT PRIMARY KEY,
    medication_a_id     TEXT NOT NULL,
    medication_b_id     TEXT NOT NULL,
    added_at            TEXT NOT NULL,
    last_attempt_at     TEXT,
    attempts            INTEGER NOT NULL DEFAULT 0,
    status              TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING', 'VERIFIED', 'FAILED')),
    error_type          TEXT CHECK (error_type IN ('TIMEOUT', 'API_ERROR', 'RATE_LIMIT', 'NO_CONNECTION')),
    next_retry_at       TEXT,
    created_at          TEXT NOT NULL,
    FOREIGN KEY (medication_a_id) REFERENCES cli_medications(medication_id),
    FOREIGN KEY (medication_b_id) REFERENCES cli_medications(medication_id),
    UNIQUE(medication_a_id, medication_b_id)
);

CREATE INDEX idx_verification_status ON cli_pending_verifications(status) WHERE status = 'PENDING';
CREATE INDEX idx_verification_retry ON cli_pending_verifications(next_retry_at) WHERE status = 'PENDING';

COMMIT;

12. Integracion con Otros Modulos

12.1. MDL-MED-001 (Medicamentos)

Integracion Descripcion
FK cli_medications cli_detected_interactions referencia medicamentos del usuario
Trigger de deteccion Al agregar/modificar medicamento, ejecutar motor de interacciones
Principios activos Usar principle_active de cli_medications para lookup en cache

Flujo:

1. Usuario agrega medicamento → Trigger en cli_medications
2. Motor obtiene lista de medicamentos ACTIVOS del usuario
3. Compara nuevo medicamento vs existentes usando cli_critical_interactions_cache (<100 CONTRAINDICADAS)
4. Si hay match CRITICO, crea registro en cli_detected_interactions
5. Si NO hay match y usuario es Pro/Perfect con consentimiento:
   - Agregar a cli_pending_verifications (max 10)
   - Buscar online en srv_interaction_catalog
   - Guardar resultado en cli_detected_interactions (cifrado E2E)
6. Genera alerta segun severidad

12.2. MDL-ALT-001 (Alertas)

Integracion Descripcion
Generacion de alertas cli_detected_interactions genera alertas segun severidad
Tipo de alerta CONTRAINDICADO → Bloqueante, SERIO → Prominente, etc.
Historial cli_interaction_history registra alertas mostradas

Mapeo de Severidad a Alerta:

Severidad Tipo Alerta Comportamiento
CONTRAINDICADO (4) BLOCKING Requiere confirmacion explicita
SERIO (3) PROMINENT Alerta prominente, ack requerido
MODERADO (2) WARNING Advertencia informativa
MENOR (1) INFO Nota discreta

12.3. MDL-USR-001 (Usuarios)

Integracion Descripcion
user_id Todas las entidades cliente tienen FK a usuarios
Tier verification Verificar tier para funcionalidades Pro/Perfect (DrugBank, IA)
Sincronizacion Datos E2E se sincronizan por user_id

13. Metricas y Monitoreo

13.1. Metricas de Cache Critico

Metrica Query
Tamano del cache critico SELECT COUNT(*) FROM cli_critical_interactions_cache (debe ser <100)
Version embebida SELECT DISTINCT embedded_version FROM cli_critical_interactions_cache
Todas CONTRAINDICADAS SELECT COUNT(*) FROM cli_critical_interactions_cache WHERE severity_code = 4 (debe ser 100%)
Verificaciones pendientes SELECT COUNT(*) FROM cli_pending_verifications WHERE status = 'PENDING' (max 10)

13.2. Metricas de Deteccion

Metrica Query
Interacciones activas SELECT COUNT(*) FROM cli_detected_interactions WHERE status = 'ACTIVE'
Interacciones CONTRAINDICADAS activas SELECT COUNT(*) FROM cli_detected_interactions WHERE status = 'ACTIVE' AND severity = 'CONTRAINDICADO'
Tasa de confirmacion SELECT COUNT(ack_id) / COUNT(detection_id) FROM cli_detected_interactions LEFT JOIN cli_interaction_acknowledgments USING(detection_id)

13.3. Metricas de Verificacion Online

Metrica Query (Servidor)
Consultas DrugBank por tier SELECT tier, COUNT(*) FROM srv_drugbank_cache GROUP BY tier
Tasa de error DrugBank SELECT COUNT(*) FILTER (WHERE is_error) / COUNT(*) FROM srv_drugbank_cache
Tiempo promedio de respuesta SELECT AVG(response_time_ms) FROM srv_drugbank_cache WHERE is_error = FALSE
Consultas IA (Perfect) SELECT COUNT(*) FROM srv_ia_analysis_log
Tokens consumidos IA SELECT SUM(tokens_total) FROM srv_ia_analysis_log

14. Referencias

14.1. Documentos Tecnicos

Documento Proposito
02-arquitectura-cliente-servidor.md CRITICO: Regla 100 Registros (seccion 3.2.3.1)
03-estrategia-datos.md Clasificaciones, sincronizacion
04-seguridad-cliente.md Cifrado E2E, AES-256-GCM
MDL-MED-001 Modelo de medicamentos
MDL-ALT-001 Modelo de alertas
API-INT-001 API de interacciones (proxima entrega)

14.2. Documentos Funcionales

Documento Proposito
MTS-INT-001 Spec funcional de interacciones med-med
MTS-CAT-001 Catalogo de medicamentos
INV-010 Anonimizacion de datos

14.3. Estandares Externos

Estandar URL
DrugBank https://go.drugbank.com/documentation
RxNorm https://rxnav.nlm.nih.gov/RxNormAPIs.html
HIPAA https://www.hhs.gov/hipaa/for-professionals/security/index.html
ATC Classification https://www.whocc.no/atc_ddd_index/

Validacion de Modelo: ACTUALIZADO v2.0.0 - DIRECTIVA DEL DIRECTOR

Cambios Criticos:

  1. ✅ cli_interaction_cache ELIMINADO (50,000 registros VIOLABAN Regla 100)
  2. ✅ cli_critical_interactions_cache NUEVO (<100 CONTRAINDICADAS, embebido en app)
  3. ✅ cli_interaction_consent NUEVO (consentimiento de disclaimer de privacidad)
  4. ✅ cli_pending_verifications REDUCIDO (max 10 registros, cola temporal)
  5. ✅ Busquedas online Pro/Perfect con consentimiento (srv_interaction_catalog ~1M+)

Clasificacion de Datos: COMPLETA

  • Todos los campos clasificados
  • Ningun PHI como SYNCED_PLAIN
  • Datos sensibles como LOCAL_ONLY o SYNCED_E2E
  • CUMPLE REGLA 100: Ningun catalogo >100 registros se descarga

Esquemas: COMPLETOS

  • Esquema SERVIDOR conceptual completo (PostgreSQL)
  • Esquema CLIENTE conceptual completo (SQLite/Realm/Room)
  • Mapeo de sincronizacion definido
  • Cache critico <100 embebido en app (NO descarga dinamica)
  • Nueva tabla de consentimiento para busquedas online

Zero-Knowledge: VALIDADO

  • Servidor NO puede ver medicamentos del usuario (solo blobs cifrados)
  • Cache critico es LOCAL_ONLY (embebido, <100 registros)
  • Busquedas online requieren consentimiento explicito
  • Blind Index solo en srv_drugbank_cache (metadata, no PHI)

Cumplimiento Directiva del Director:

  • Regla 100 Registros (02-arquitectura-cliente-servidor.md 3.2.3.1)
  • Cache critico <100 CONTRAINDICADAS mas comunes
  • NO descarga masiva de catalogos
  • Busquedas online solo con consentimiento (Pro/Perfect)

DatabaseDrone - Doce de Quince "Los datos son el nucleo. LOCAL primero, SERVIDOR solo blobs." Iteracion IT-10 - Modelo de Interacciones Medicamentosas Version 2.0.0 - Cumple Regla 100 del Director