Saltar a contenido

Modelo de Datos: Adherencia al Tratamiento

Identificador: MDL-ADH-001 Version: 1.0.0 Fecha: 2025-12-08 Estado: Borrador Autor: DatabaseDrone (Doce de Quince) / SpecQueen Technical Division Modulo Funcional: MTS-ADH-001



1. Introduccion

Este documento define el modelo de datos para el modulo de Adherencia al Tratamiento de MedTime. La adherencia se calcula, analiza y almacena 100% en el dispositivo, permitiendo al paciente entender sus patrones y mejorar su cumplimiento.

1.1. Principios

Principio Aplicacion
100% Local Todos los calculos, metricas, patrones e insights se generan en dispositivo
Offline-First Metricas disponibles sin conexion
Privacy-First Patrones ML y datos sensibles NUNCA salen del dispositivo
Sync Opcional Solo usuarios Pro/Perfect pueden sincronizar adherencia (opcional)

1.2. Clasificacion de Datos

Entidad Clasificacion Cifrado Sync Razon
cli_adherence_daily LOCAL_ONLY / SYNCED_E2E Si Opcional Pro/Perfect Metricas diarias son sensibles
cli_adherence_by_medication LOCAL_ONLY / SYNCED_E2E Si Opcional Pro/Perfect Revela medicamentos activos (PHI)
cli_adherence_streaks LOCAL_ONLY / SYNCED_E2E Si Opcional Pro/Perfect Rachas pueden ser gamificacion sync
cli_adherence_patterns LOCAL_ONLY Si NO Patrones ML on-device, NUNCA salen
cli_adherence_anomalies SYNCED_E2E Si Pro/Perfect Anomalias pueden compartirse con medico
cli_adherence_insights LOCAL_ONLY Si NO Insights generados localmente
cli_adherence_reports LOCAL_ONLY / SYNCED_E2E Si Opcional Pro/Perfect Reportes para compartir con medico

NOTA CRITICA: Por defecto, adherencia esLOCAL_ONLY. El usuario debeoptar activamentepor sincronizar datos de adherencia (Pro/Perfect). Esto respeta el principio de que datos de adherencia sonaltamente sensibles y revelan comportamiento medico.


2. Entidades del Cliente (LOCAL/SYNCED_E2E)

2.1. cli_adherence_daily

Metricas de adherencia calculadas diariamente.

2.1.1. Clasificacion

Campo Clasificacion Razon
dosesScheduled SYNCED_E2E (opcional) Cantidad de tomas programadas revela intensidad de tratamiento
dosesTaken SYNCED_E2E (opcional) Cantidad tomadas revela comportamiento
adherencePercentage SYNCED_E2E (opcional) Metrica sensible de cumplimiento
currentStreak SYNCED_E2E (opcional) Racha puede ser gamificacion

2.1.2. iOS - Swift/Realm

// ============================================================
// MODELO: AdherenceDaily
// Descripcion: Metricas de adherencia diaria
// Almacenamiento: Realm cifrado
// Sync: LOCAL_ONLY por defecto, SYNCED_E2E si usuario activa
// ============================================================

import RealmSwift

class AdherenceDaily: Object, Identifiable {
    // Identificadores
    @Persisted(primaryKey: true) var id: String = UUID().uuidString
    @Persisted var userId: String = ""

    // Fecha
    @Persisted var date: Date = Date()

    // Metricas basicas
    @Persisted var dosesScheduled: Int = 0
    @Persisted var dosesTaken: Int = 0
    @Persisted var dosesSkipped: Int = 0
    @Persisted var dosesMissed: Int = 0
    @Persisted var dosesPostponed: Int = 0

    // Adherencia calculada
    @Persisted var adherencePercentage: Double = 0.0  // 0-100
    @Persisted var adherenceScore: String = ""
    // excellent (>=95), good (80-94), fair (60-79), poor (<60)

    // Metricas de puntualidad
    @Persisted var dosesOnTime: Int = 0
    @Persisted var dosesLate: Int = 0
    @Persisted var averageDelayMinutes: Int = 0

    // Rachas
    @Persisted var currentStreak: Int = 0  // Dias consecutivos con 100%
    @Persisted var maxStreak: Int = 0      // Mejor racha historica

    // Flags
    @Persisted var isPerfectDay: Bool = false  // 100% adherencia
    @Persisted var hasSkipped: Bool = false
    @Persisted var hasMissed: Bool = false

    // Sync (solo si usuario activa)
    @Persisted var syncEnabled: Bool = false  // Usuario opta por sync
    @Persisted var syncStatus: String = "local_only"
    // local_only, pending, synced, conflict

    @Persisted var lastSyncAt: Date?
    @Persisted var version: Int = 1

    // Timestamps
    @Persisted var calculatedAt: Date = Date()
    @Persisted var updatedAt: Date = Date()

    // Computed
    var isExcellent: Bool {
        return adherencePercentage >= 95.0
    }

    var isGood: Bool {
        return adherencePercentage >= 80.0 && adherencePercentage < 95.0
    }
}

2.1.3. Android - Kotlin/Room

// ============================================================
// MODELO: AdherenceDaily
// Descripcion: Metricas de adherencia diaria
// Almacenamiento: Room cifrado (SQLCipher)
// Sync: LOCAL_ONLY por defecto, SYNCED_E2E si usuario activa
// ============================================================

package com.medtime.data.entities

import androidx.room.*
import java.time.Instant
import java.time.LocalDate
import java.util.UUID

@Entity(
    tableName = "cli_adherence_daily",
    indices = [
        Index(value = ["user_id", "date"], unique = true),
        Index(value = ["date"]),
        Index(value = ["adherence_percentage"]),
        Index(value = ["sync_enabled"])
    ]
)
data class AdherenceDaily(
    @PrimaryKey
    @ColumnInfo(name = "id")
    val id: String = UUID.randomUUID().toString(),

    @ColumnInfo(name = "user_id")
    val userId: String = "",

    // Fecha
    @ColumnInfo(name = "date")
    val date: LocalDate = LocalDate.now(),

    // Metricas basicas
    @ColumnInfo(name = "doses_scheduled")
    val dosesScheduled: Int = 0,

    @ColumnInfo(name = "doses_taken")
    val dosesTaken: Int = 0,

    @ColumnInfo(name = "doses_skipped")
    val dosesSkipped: Int = 0,

    @ColumnInfo(name = "doses_missed")
    val dosesMissed: Int = 0,

    @ColumnInfo(name = "doses_postponed")
    val dosesPostponed: Int = 0,

    // Adherencia
    @ColumnInfo(name = "adherence_percentage")
    val adherencePercentage: Double = 0.0,

    @ColumnInfo(name = "adherence_score")
    val adherenceScore: AdherenceScore = AdherenceScore.POOR,

    // Puntualidad
    @ColumnInfo(name = "doses_on_time")
    val dosesOnTime: Int = 0,

    @ColumnInfo(name = "doses_late")
    val dosesLate: Int = 0,

    @ColumnInfo(name = "average_delay_minutes")
    val averageDelayMinutes: Int = 0,

    // Rachas
    @ColumnInfo(name = "current_streak")
    val currentStreak: Int = 0,

    @ColumnInfo(name = "max_streak")
    val maxStreak: Int = 0,

    // Flags
    @ColumnInfo(name = "is_perfect_day")
    val isPerfectDay: Boolean = false,

    @ColumnInfo(name = "has_skipped")
    val hasSkipped: Boolean = false,

    @ColumnInfo(name = "has_missed")
    val hasMissed: Boolean = false,

    // Sync
    @ColumnInfo(name = "sync_enabled")
    val syncEnabled: Boolean = false,

    @ColumnInfo(name = "sync_status")
    val syncStatus: SyncStatus = SyncStatus.LOCAL_ONLY,

    @ColumnInfo(name = "last_sync_at")
    val lastSyncAt: Instant? = null,

    @ColumnInfo(name = "version")
    val version: Int = 1,

    // Timestamps
    @ColumnInfo(name = "calculated_at")
    val calculatedAt: Instant = Instant.now(),

    @ColumnInfo(name = "updated_at")
    val updatedAt: Instant = Instant.now()
)

enum class AdherenceScore {
    EXCELLENT,  // >= 95%
    GOOD,       // 80-94%
    FAIR,       // 60-79%
    POOR        // < 60%
}

enum class SyncStatus {
    LOCAL_ONLY,
    PENDING,
    SYNCED,
    CONFLICT
}

2.2. cli_adherence_by_medication

Adherencia calculada por medicamento individual.

2.2.1. Clasificacion

Campo Clasificacion Razon
medicationId SYNCED_E2E (opcional) Referencia a medicamento (PHI)
adherencePercentage SYNCED_E2E (opcional) Metrica de cumplimiento por medicamento
problematicTime LOCAL_ONLY Patron detectado, ML on-device
problematicDay LOCAL_ONLY Patron detectado, ML on-device

2.2.2. iOS - Swift/Realm

// ============================================================
// MODELO: AdherenceByMedication
// Descripcion: Adherencia por medicamento
// Almacenamiento: Realm cifrado
// Sync: LOCAL_ONLY por defecto, SYNCED_E2E si usuario activa
// ============================================================

import RealmSwift

class AdherenceByMedication: Object, Identifiable {
    // Identificadores
    @Persisted(primaryKey: true) var id: String = UUID().uuidString
    @Persisted var userId: String = ""

    // Referencia
    @Persisted var medicationId: String = ""  // FK a cli_medications

    // Periodo de calculo
    @Persisted var startDate: Date = Date()
    @Persisted var endDate: Date = Date()
    @Persisted var periodDays: Int = 0

    // Metricas
    @Persisted var dosesScheduled: Int = 0
    @Persisted var dosesTaken: Int = 0
    @Persisted var dosesSkipped: Int = 0
    @Persisted var dosesMissed: Int = 0

    // Adherencia
    @Persisted var adherencePercentage: Double = 0.0
    @Persisted var adherenceScore: String = ""

    // Tendencia
    @Persisted var trend: String = "stable"
    // improving, stable, declining

    @Persisted var trendPercentageChange: Double = 0.0  // +/- %

    // Patrones detectados (LOCAL_ONLY - ML on-device)
    @Persisted var problematicTime: String?  // "14:00" - hora con mas omisiones
    @Persisted var problematicDay: String?   // "monday" - dia con mas omisiones
    @Persisted var problematicPattern: String?  // "weekends", "mornings", etc.

    // Tiempo en tratamiento
    @Persisted var daysOnMedication: Int = 0

    // Sync (solo si usuario activa)
    @Persisted var syncEnabled: Bool = false
    @Persisted var syncStatus: String = "local_only"
    @Persisted var lastSyncAt: Date?
    @Persisted var version: Int = 1

    // Timestamps
    @Persisted var calculatedAt: Date = Date()
    @Persisted var updatedAt: Date = Date()
}

2.2.3. Android - Kotlin/Room

// ============================================================
// MODELO: AdherenceByMedication
// Descripcion: Adherencia por medicamento
// Almacenamiento: Room cifrado (SQLCipher)
// Sync: LOCAL_ONLY por defecto, SYNCED_E2E si usuario activa
// ============================================================

package com.medtime.data.entities

import androidx.room.*
import java.time.Instant
import java.time.LocalDate
import java.util.UUID

@Entity(
    tableName = "cli_adherence_by_medication",
    foreignKeys = [
        ForeignKey(
            entity = Medication::class,
            parentColumns = ["medication_id"],
            childColumns = ["medication_id"],
            onDelete = ForeignKey.CASCADE
        )
    ],
    indices = [
        Index(value = ["medication_id"]),
        Index(value = ["user_id", "medication_id"]),
        Index(value = ["adherence_percentage"])
    ]
)
data class AdherenceByMedication(
    @PrimaryKey
    @ColumnInfo(name = "id")
    val id: String = UUID.randomUUID().toString(),

    @ColumnInfo(name = "user_id")
    val userId: String = "",

    // Referencia
    @ColumnInfo(name = "medication_id", index = true)
    val medicationId: String = "",

    // Periodo
    @ColumnInfo(name = "start_date")
    val startDate: LocalDate = LocalDate.now(),

    @ColumnInfo(name = "end_date")
    val endDate: LocalDate = LocalDate.now(),

    @ColumnInfo(name = "period_days")
    val periodDays: Int = 0,

    // Metricas
    @ColumnInfo(name = "doses_scheduled")
    val dosesScheduled: Int = 0,

    @ColumnInfo(name = "doses_taken")
    val dosesTaken: Int = 0,

    @ColumnInfo(name = "doses_skipped")
    val dosesSkipped: Int = 0,

    @ColumnInfo(name = "doses_missed")
    val dosesMissed: Int = 0,

    // Adherencia
    @ColumnInfo(name = "adherence_percentage")
    val adherencePercentage: Double = 0.0,

    @ColumnInfo(name = "adherence_score")
    val adherenceScore: AdherenceScore = AdherenceScore.POOR,

    // Tendencia
    @ColumnInfo(name = "trend")
    val trend: Trend = Trend.STABLE,

    @ColumnInfo(name = "trend_percentage_change")
    val trendPercentageChange: Double = 0.0,

    // Patrones (LOCAL_ONLY)
    @ColumnInfo(name = "problematic_time")
    val problematicTime: String? = null,

    @ColumnInfo(name = "problematic_day")
    val problematicDay: String? = null,

    @ColumnInfo(name = "problematic_pattern")
    val problematicPattern: String? = null,

    // Tiempo en tratamiento
    @ColumnInfo(name = "days_on_medication")
    val daysOnMedication: Int = 0,

    // Sync
    @ColumnInfo(name = "sync_enabled")
    val syncEnabled: Boolean = false,

    @ColumnInfo(name = "sync_status")
    val syncStatus: SyncStatus = SyncStatus.LOCAL_ONLY,

    @ColumnInfo(name = "last_sync_at")
    val lastSyncAt: Instant? = null,

    @ColumnInfo(name = "version")
    val version: Int = 1,

    // Timestamps
    @ColumnInfo(name = "calculated_at")
    val calculatedAt: Instant = Instant.now(),

    @ColumnInfo(name = "updated_at")
    val updatedAt: Instant = Instant.now()
)

enum class Trend {
    IMPROVING,
    STABLE,
    DECLINING
}

2.3. cli_adherence_streaks

Rachas de adherencia (dias consecutivos con 100% adherencia).

2.3.1. Clasificacion

Campo Clasificacion Razon
streakDays SYNCED_E2E (opcional) Puede ser parte de gamificacion sync
startDate SYNCED_E2E (opcional) Fecha de inicio de racha
isCurrent LOCAL_ONLY Flag de estado, no requiere sync

2.3.2. iOS - Swift/Realm

// ============================================================
// MODELO: AdherenceStreak
// Descripcion: Rachas de adherencia perfecta
// Almacenamiento: Realm cifrado
// Sync: LOCAL_ONLY por defecto, SYNCED_E2E si usuario activa
// ============================================================

import RealmSwift

class AdherenceStreak: Object, Identifiable {
    // Identificadores
    @Persisted(primaryKey: true) var id: String = UUID().uuidString
    @Persisted var userId: String = ""

    // Tipo de racha
    @Persisted var streakType: String = "perfect"
    // perfect (100%), good (>=80%), any_taken (>0%)

    // Duracion
    @Persisted var streakDays: Int = 0
    @Persisted var startDate: Date = Date()
    @Persisted var endDate: Date?  // nil si es racha actual

    // Estado
    @Persisted var isCurrent: Bool = true
    @Persisted var isBroken: Bool = false

    // Razon de ruptura
    @Persisted var brokenReason: String?
    // missed_dose, skipped_dose, medication_paused

    @Persisted var brokenDate: Date?

    // Milestones alcanzados
    @Persisted var milestones: List<Int>  // [7, 14, 30, 60, 90, 180, 365]
    @Persisted var lastMilestone: Int = 0

    // Metricas de la racha
    @Persisted var totalDosesTaken: Int = 0
    @Persisted var totalDosesScheduled: Int = 0
    @Persisted var averageAdherence: Double = 0.0

    // Sync (solo si usuario activa)
    @Persisted var syncEnabled: Bool = false
    @Persisted var syncStatus: String = "local_only"
    @Persisted var lastSyncAt: Date?
    @Persisted var version: Int = 1

    // Timestamps
    @Persisted var createdAt: Date = Date()
    @Persisted var updatedAt: Date = Date()

    // Computed
    var isRecordStreak: Bool {
        // Verificar si es la racha mas larga del usuario
        return false  // Implementar en logica de negocio
    }
}

2.3.3. Android - Kotlin/Room

// ============================================================
// MODELO: AdherenceStreak
// Descripcion: Rachas de adherencia perfecta
// Almacenamiento: Room cifrado (SQLCipher)
// Sync: LOCAL_ONLY por defecto, SYNCED_E2E si usuario activa
// ============================================================

package com.medtime.data.entities

import androidx.room.*
import java.time.Instant
import java.time.LocalDate
import java.util.UUID

@Entity(
    tableName = "cli_adherence_streaks",
    indices = [
        Index(value = ["user_id", "is_current"]),
        Index(value = ["streak_days"]),
        Index(value = ["start_date"])
    ]
)
data class AdherenceStreak(
    @PrimaryKey
    @ColumnInfo(name = "id")
    val id: String = UUID.randomUUID().toString(),

    @ColumnInfo(name = "user_id")
    val userId: String = "",

    // Tipo
    @ColumnInfo(name = "streak_type")
    val streakType: StreakType = StreakType.PERFECT,

    // Duracion
    @ColumnInfo(name = "streak_days")
    val streakDays: Int = 0,

    @ColumnInfo(name = "start_date")
    val startDate: LocalDate = LocalDate.now(),

    @ColumnInfo(name = "end_date")
    val endDate: LocalDate? = null,

    // Estado
    @ColumnInfo(name = "is_current")
    val isCurrent: Boolean = true,

    @ColumnInfo(name = "is_broken")
    val isBroken: Boolean = false,

    // Ruptura
    @ColumnInfo(name = "broken_reason")
    val brokenReason: BreakReason? = null,

    @ColumnInfo(name = "broken_date")
    val brokenDate: LocalDate? = null,

    // Milestones
    @ColumnInfo(name = "milestones")
    val milestones: List<Int> = emptyList(),  // TypeConverter

    @ColumnInfo(name = "last_milestone")
    val lastMilestone: Int = 0,

    // Metricas
    @ColumnInfo(name = "total_doses_taken")
    val totalDosesTaken: Int = 0,

    @ColumnInfo(name = "total_doses_scheduled")
    val totalDosesScheduled: Int = 0,

    @ColumnInfo(name = "average_adherence")
    val averageAdherence: Double = 0.0,

    // Sync
    @ColumnInfo(name = "sync_enabled")
    val syncEnabled: Boolean = false,

    @ColumnInfo(name = "sync_status")
    val syncStatus: SyncStatus = SyncStatus.LOCAL_ONLY,

    @ColumnInfo(name = "last_sync_at")
    val lastSyncAt: Instant? = null,

    @ColumnInfo(name = "version")
    val version: Int = 1,

    // Timestamps
    @ColumnInfo(name = "created_at")
    val createdAt: Instant = Instant.now(),

    @ColumnInfo(name = "updated_at")
    val updatedAt: Instant = Instant.now()
)

enum class StreakType {
    PERFECT,    // 100%
    GOOD,       // >= 80%
    ANY_TAKEN   // > 0%
}

enum class BreakReason {
    MISSED_DOSE,
    SKIPPED_DOSE,
    MEDICATION_PAUSED
}

2.4. cli_adherence_patterns

Patrones de comportamiento detectados por ML on-device.

2.4.1. Clasificacion

Campo Clasificacion Razon
ALL LOCAL_ONLY Patrones ML NUNCA salen del dispositivo

CRITICO: Esta entidad es100% LOCAL_ONLY. Los patrones son generados por ML on-device y contienen datos sensibles de comportamiento.NUNCA se sincronizan al servidor.

2.4.2. iOS - Swift/Realm

// ============================================================
// MODELO: AdherencePattern
// Descripcion: Patrones detectados por ML on-device
// Almacenamiento: Realm cifrado
// Sync: LOCAL_ONLY - NUNCA se sincroniza
// ============================================================

import RealmSwift

class AdherencePattern: Object, Identifiable {
    // Identificadores
    @Persisted(primaryKey: true) var id: String = UUID().uuidString
    @Persisted var userId: String = ""

    // Tipo de patron
    @Persisted var patternType: String = ""
    // day_of_week, time_of_day, weekend_effect, monthly_cycle,
    // medication_specific, weather_correlation, stress_correlation

    // Descripcion del patron
    @Persisted var title: String = ""  // "Adherencia baja los lunes"
    @Persisted var description_text: String = ""  // Descripcion detallada

    // Datos del patron
    @Persisted var affectedDays: List<String>     // ["monday", "friday"]
    @Persisted var affectedTimes: List<String>    // ["14:00", "20:00"]
    @Persisted var affectedMedicationIds: List<String>

    // Metricas del patron
    @Persisted var impactPercentage: Double = 0.0  // % de adherencia afectado
    @Persisted var frequency: String = ""
    // daily, weekly, monthly, occasional

    @Persisted var confidence: Double = 0.0  // 0-1 (confianza del ML)

    // Periodo de deteccion
    @Persisted var detectedFrom: Date = Date()
    @Persisted var detectedUntil: Date = Date()
    @Persisted var sampleSize: Int = 0  // Dias de datos analizados

    // Estado
    @Persisted var isActive: Bool = true
    @Persisted var wasAddressed: Bool = false  // Usuario tomo accion

    // Sugerencia automatica
    @Persisted var suggestedAction: String?
    // "Cambia la toma de las 14:00 al almuerzo"

    // Timestamps
    @Persisted var detectedAt: Date = Date()
    @Persisted var updatedAt: Date = Date()

    // NO HAY SYNC - LOCAL_ONLY
}

2.4.3. Android - Kotlin/Room

// ============================================================
// MODELO: AdherencePattern
// Descripcion: Patrones detectados por ML on-device
// Almacenamiento: Room cifrado (SQLCipher)
// Sync: LOCAL_ONLY - NUNCA se sincroniza
// ============================================================

package com.medtime.data.entities

import androidx.room.*
import java.time.Instant
import java.util.UUID

@Entity(
    tableName = "cli_adherence_patterns",
    indices = [
        Index(value = ["user_id", "pattern_type"]),
        Index(value = ["is_active"]),
        Index(value = ["confidence"])
    ]
)
data class AdherencePattern(
    @PrimaryKey
    @ColumnInfo(name = "id")
    val id: String = UUID.randomUUID().toString(),

    @ColumnInfo(name = "user_id")
    val userId: String = "",

    // Tipo
    @ColumnInfo(name = "pattern_type")
    val patternType: PatternType = PatternType.DAY_OF_WEEK,

    // Descripcion
    @ColumnInfo(name = "title")
    val title: String = "",

    @ColumnInfo(name = "description_text")
    val descriptionText: String = "",

    // Datos
    @ColumnInfo(name = "affected_days")
    val affectedDays: List<String> = emptyList(),  // TypeConverter

    @ColumnInfo(name = "affected_times")
    val affectedTimes: List<String> = emptyList(),

    @ColumnInfo(name = "affected_medication_ids")
    val affectedMedicationIds: List<String> = emptyList(),

    // Metricas
    @ColumnInfo(name = "impact_percentage")
    val impactPercentage: Double = 0.0,

    @ColumnInfo(name = "frequency")
    val frequency: PatternFrequency = PatternFrequency.OCCASIONAL,

    @ColumnInfo(name = "confidence")
    val confidence: Double = 0.0,

    // Periodo
    @ColumnInfo(name = "detected_from")
    val detectedFrom: Instant = Instant.now(),

    @ColumnInfo(name = "detected_until")
    val detectedUntil: Instant = Instant.now(),

    @ColumnInfo(name = "sample_size")
    val sampleSize: Int = 0,

    // Estado
    @ColumnInfo(name = "is_active")
    val isActive: Boolean = true,

    @ColumnInfo(name = "was_addressed")
    val wasAddressed: Boolean = false,

    // Sugerencia
    @ColumnInfo(name = "suggested_action")
    val suggestedAction: String? = null,

    // Timestamps
    @ColumnInfo(name = "detected_at")
    val detectedAt: Instant = Instant.now(),

    @ColumnInfo(name = "updated_at")
    val updatedAt: Instant = Instant.now()

    // NO HAY SYNC - LOCAL_ONLY
)

enum class PatternType {
    DAY_OF_WEEK,
    TIME_OF_DAY,
    WEEKEND_EFFECT,
    MONTHLY_CYCLE,
    MEDICATION_SPECIFIC,
    WEATHER_CORRELATION,
    STRESS_CORRELATION
}

enum class PatternFrequency {
    DAILY,
    WEEKLY,
    MONTHLY,
    OCCASIONAL
}

2.5. cli_adherence_anomalies

Anomalias reportadas por el usuario (efectos secundarios, problemas, etc.).

2.5.1. Clasificacion

Campo Clasificacion Razon
medicationId SYNCED_E2E Referencia a medicamento (PHI)
anomalyType SYNCED_E2E Tipo de problema (PHI)
description SYNCED_E2E Descripcion puede contener info medica (PHI)
imageUrl SYNCED_E2E Imagen puede contener PHI

2.5.2. iOS - Swift/Realm

// ============================================================
// MODELO: AdherenceAnomaly
// Descripcion: Anomalias reportadas por usuario
// Almacenamiento: Realm cifrado
// Sync: SYNCED_E2E (Pro/Perfect) - Para compartir con medico
// ============================================================

import RealmSwift

class AdherenceAnomaly: Object, Identifiable {
    // Identificadores
    @Persisted(primaryKey: true) var id: String = UUID().uuidString
    @Persisted var userId: String = ""

    // Referencia (opcional)
    @Persisted var medicationId: String?  // FK a cli_medications

    // Tipo de anomalia
    @Persisted var anomalyType: String = ""
    // side_effect, systematic_forget, supply_issue, confusion,
    // perceived_interaction, other

    // Detalles
    @Persisted var title: String = ""
    @Persisted var description_text: String = ""

    // Severidad percibida por usuario
    @Persisted var severity: String = "mild"
    // mild, moderate, severe

    // Fecha/hora de ocurrencia
    @Persisted var occurredAt: Date = Date()
    @Persisted var reportedAt: Date = Date()

    // Imagen (opcional)
    @Persisted var imageUrl: String?  // Local file path o blob cifrado

    // Accion tomada
    @Persisted var actionTaken: String?
    // contacted_doctor, stopped_medication, changed_dose, other

    // Estado
    @Persisted var isResolved: Bool = false
    @Persisted var resolvedAt: Date?
    @Persisted var resolution: String?

    // Sync
    @Persisted var syncStatus: String = "pending"
    @Persisted var lastSyncAt: Date?
    @Persisted var version: Int = 1

    // Timestamps
    @Persisted var createdAt: Date = Date()
    @Persisted var updatedAt: Date = Date()
    @Persisted var isDeleted: Bool = false
}

2.5.3. Android - Kotlin/Room

// ============================================================
// MODELO: AdherenceAnomaly
// Descripcion: Anomalias reportadas por usuario
// Almacenamiento: Room cifrado (SQLCipher)
// Sync: SYNCED_E2E (Pro/Perfect) - Para compartir con medico
// ============================================================

package com.medtime.data.entities

import androidx.room.*
import java.time.Instant
import java.util.UUID

@Entity(
    tableName = "cli_adherence_anomalies",
    foreignKeys = [
        ForeignKey(
            entity = Medication::class,
            parentColumns = ["medication_id"],
            childColumns = ["medication_id"],
            onDelete = ForeignKey.SET_NULL
        )
    ],
    indices = [
        Index(value = ["user_id", "occurred_at"]),
        Index(value = ["medication_id"]),
        Index(value = ["anomaly_type"]),
        Index(value = ["severity"])
    ]
)
data class AdherenceAnomaly(
    @PrimaryKey
    @ColumnInfo(name = "id")
    val id: String = UUID.randomUUID().toString(),

    @ColumnInfo(name = "user_id")
    val userId: String = "",

    // Referencia
    @ColumnInfo(name = "medication_id")
    val medicationId: String? = null,

    // Tipo
    @ColumnInfo(name = "anomaly_type")
    val anomalyType: AnomalyType = AnomalyType.OTHER,

    // Detalles
    @ColumnInfo(name = "title")
    val title: String = "",

    @ColumnInfo(name = "description_text")
    val descriptionText: String = "",

    // Severidad
    @ColumnInfo(name = "severity")
    val severity: Severity = Severity.MILD,

    // Tiempos
    @ColumnInfo(name = "occurred_at")
    val occurredAt: Instant = Instant.now(),

    @ColumnInfo(name = "reported_at")
    val reportedAt: Instant = Instant.now(),

    // Imagen
    @ColumnInfo(name = "image_url")
    val imageUrl: String? = null,

    // Accion
    @ColumnInfo(name = "action_taken")
    val actionTaken: ActionTaken? = null,

    // Estado
    @ColumnInfo(name = "is_resolved")
    val isResolved: Boolean = false,

    @ColumnInfo(name = "resolved_at")
    val resolvedAt: Instant? = null,

    @ColumnInfo(name = "resolution")
    val resolution: String? = null,

    // Sync
    @ColumnInfo(name = "sync_status")
    val syncStatus: SyncStatus = SyncStatus.PENDING,

    @ColumnInfo(name = "last_sync_at")
    val lastSyncAt: Instant? = null,

    @ColumnInfo(name = "version")
    val version: Int = 1,

    // Timestamps
    @ColumnInfo(name = "created_at")
    val createdAt: Instant = Instant.now(),

    @ColumnInfo(name = "updated_at")
    val updatedAt: Instant = Instant.now(),

    @ColumnInfo(name = "is_deleted")
    val isDeleted: Boolean = false
)

enum class AnomalyType {
    SIDE_EFFECT,
    SYSTEMATIC_FORGET,
    SUPPLY_ISSUE,
    CONFUSION,
    PERCEIVED_INTERACTION,
    OTHER
}

enum class Severity {
    MILD,
    MODERATE,
    SEVERE
}

enum class ActionTaken {
    CONTACTED_DOCTOR,
    STOPPED_MEDICATION,
    CHANGED_DOSE,
    OTHER
}

2.6. cli_adherence_insights

Insights generados automaticamente por el sistema.

2.6.1. Clasificacion

Campo Clasificacion Razon
ALL LOCAL_ONLY Insights generados localmente, no requieren sync

NOTA: Los insights son generados por el motor de analisis local y se muestran al usuario como recomendaciones. No se sincronizan porque son volatiles y se regeneran periodicamente.

2.6.2. iOS - Swift/Realm

// ============================================================
// MODELO: AdherenceInsight
// Descripcion: Insights generados automaticamente
// Almacenamiento: Realm cifrado
// Sync: LOCAL_ONLY - No se sincroniza (volatil)
// ============================================================

import RealmSwift

class AdherenceInsight: Object, Identifiable {
    // Identificadores
    @Persisted(primaryKey: true) var id: String = UUID().uuidString
    @Persisted var userId: String = ""

    // Tipo de insight
    @Persisted var insightType: String = ""
    // pattern_detected, improvement_suggestion, achievement,
    // alert, motivation

    // Contenido
    @Persisted var title: String = ""
    @Persisted var message: String = ""
    @Persisted var actionText: String?  // "Cambiar horario"

    // Prioridad
    @Persisted var priority: String = "normal"
    // low, normal, high, critical

    // Contexto
    @Persisted var relatedPatternId: String?
    @Persisted var relatedMedicationIds: List<String>
    @Persisted var relatedDates: List<Date>

    // Estado
    @Persisted var isRead: Bool = false
    @Persisted var isDismissed: Bool = false
    @Persisted var wasActioned: Bool = false

    // Validez
    @Persisted var validUntil: Date?  // Insights expiran
    @Persisted var isExpired: Bool = false

    // Timestamps
    @Persisted var generatedAt: Date = Date()
    @Persisted var readAt: Date?
    @Persisted var dismissedAt: Date?

    // NO HAY SYNC - LOCAL_ONLY
}

2.6.3. Android - Kotlin/Room

// ============================================================
// MODELO: AdherenceInsight
// Descripcion: Insights generados automaticamente
// Almacenamiento: Room cifrado (SQLCipher)
// Sync: LOCAL_ONLY - No se sincroniza (volatil)
// ============================================================

package com.medtime.data.entities

import androidx.room.*
import java.time.Instant
import java.util.UUID

@Entity(
    tableName = "cli_adherence_insights",
    indices = [
        Index(value = ["user_id", "is_read"]),
        Index(value = ["priority"]),
        Index(value = ["generated_at"])
    ]
)
data class AdherenceInsight(
    @PrimaryKey
    @ColumnInfo(name = "id")
    val id: String = UUID.randomUUID().toString(),

    @ColumnInfo(name = "user_id")
    val userId: String = "",

    // Tipo
    @ColumnInfo(name = "insight_type")
    val insightType: InsightType = InsightType.MOTIVATION,

    // Contenido
    @ColumnInfo(name = "title")
    val title: String = "",

    @ColumnInfo(name = "message")
    val message: String = "",

    @ColumnInfo(name = "action_text")
    val actionText: String? = null,

    // Prioridad
    @ColumnInfo(name = "priority")
    val priority: Priority = Priority.NORMAL,

    // Contexto
    @ColumnInfo(name = "related_pattern_id")
    val relatedPatternId: String? = null,

    @ColumnInfo(name = "related_medication_ids")
    val relatedMedicationIds: List<String> = emptyList(),

    @ColumnInfo(name = "related_dates")
    val relatedDates: List<Long> = emptyList(),  // Epochs

    // Estado
    @ColumnInfo(name = "is_read")
    val isRead: Boolean = false,

    @ColumnInfo(name = "is_dismissed")
    val isDismissed: Boolean = false,

    @ColumnInfo(name = "was_actioned")
    val wasActioned: Boolean = false,

    // Validez
    @ColumnInfo(name = "valid_until")
    val validUntil: Instant? = null,

    @ColumnInfo(name = "is_expired")
    val isExpired: Boolean = false,

    // Timestamps
    @ColumnInfo(name = "generated_at")
    val generatedAt: Instant = Instant.now(),

    @ColumnInfo(name = "read_at")
    val readAt: Instant? = null,

    @ColumnInfo(name = "dismissed_at")
    val dismissedAt: Instant? = null

    // NO HAY SYNC - LOCAL_ONLY
)

enum class InsightType {
    PATTERN_DETECTED,
    IMPROVEMENT_SUGGESTION,
    ACHIEVEMENT,
    ALERT,
    MOTIVATION
}

enum class Priority {
    LOW,
    NORMAL,
    HIGH,
    CRITICAL
}

2.7. cli_adherence_reports

Reportes generados para exportar y compartir con profesionales de salud.

2.7.1. Clasificacion

Campo Clasificacion Razon
reportData SYNCED_E2E (opcional) Contiene metricas y medicamentos (PHI)
fileUrl SYNCED_E2E (opcional) Archivo PDF/CSV puede contener PHI
sharedWith SYNCED_E2E (opcional) Metadata de compartir

2.7.2. iOS - Swift/Realm

// ============================================================
// MODELO: AdherenceReport
// Descripcion: Reportes generados para exportar
// Almacenamiento: Realm cifrado
// Sync: LOCAL_ONLY por defecto, SYNCED_E2E si usuario activa
// ============================================================

import RealmSwift

class AdherenceReport: Object, Identifiable {
    // Identificadores
    @Persisted(primaryKey: true) var id: String = UUID().uuidString
    @Persisted var userId: String = ""

    // Tipo de reporte
    @Persisted var reportType: String = "adherence"
    // adherence, medications, complete

    // Periodo
    @Persisted var startDate: Date = Date()
    @Persisted var endDate: Date = Date()
    @Persisted var periodDays: Int = 0

    // Configuracion del reporte
    @Persisted var includedMedicationIds: List<String>
    @Persisted var includedSections: List<String>
    // summary, by_medication, calendar, patterns, anomalies, graphs

    // Formato de salida
    @Persisted var outputFormat: String = "pdf"
    // pdf, csv, fhir

    // Archivo generado
    @Persisted var fileUrl: String?  // Local file path
    @Persisted var fileSizeBytes: Int = 0
    @Persisted var fileHash: String?  // SHA-256

    // Datos del reporte (JSON cifrado)
    @Persisted var reportData: Data?  // JSON serializado

    // Compartir
    @Persisted var isShared: Bool = false
    @Persisted var sharedWith: List<String>  // Emails o IDs
    @Persisted var sharedAt: Date?
    @Persisted var expiresAt: Date?  // Enlace temporal

    // Estado
    @Persisted var status: String = "draft"
    // draft, generating, ready, expired

    // Sync (solo si usuario activa)
    @Persisted var syncEnabled: Bool = false
    @Persisted var syncStatus: String = "local_only"
    @Persisted var lastSyncAt: Date?
    @Persisted var version: Int = 1

    // Timestamps
    @Persisted var createdAt: Date = Date()
    @Persisted var generatedAt: Date?
    @Persisted var updatedAt: Date = Date()
    @Persisted var isDeleted: Bool = false
}

2.7.3. Android - Kotlin/Room

// ============================================================
// MODELO: AdherenceReport
// Descripcion: Reportes generados para exportar
// Almacenamiento: Room cifrado (SQLCipher)
// Sync: LOCAL_ONLY por defecto, SYNCED_E2E si usuario activa
// ============================================================

package com.medtime.data.entities

import androidx.room.*
import java.time.Instant
import java.time.LocalDate
import java.util.UUID

@Entity(
    tableName = "cli_adherence_reports",
    indices = [
        Index(value = ["user_id", "created_at"]),
        Index(value = ["status"]),
        Index(value = ["report_type"])
    ]
)
data class AdherenceReport(
    @PrimaryKey
    @ColumnInfo(name = "id")
    val id: String = UUID.randomUUID().toString(),

    @ColumnInfo(name = "user_id")
    val userId: String = "",

    // Tipo
    @ColumnInfo(name = "report_type")
    val reportType: ReportType = ReportType.ADHERENCE,

    // Periodo
    @ColumnInfo(name = "start_date")
    val startDate: LocalDate = LocalDate.now(),

    @ColumnInfo(name = "end_date")
    val endDate: LocalDate = LocalDate.now(),

    @ColumnInfo(name = "period_days")
    val periodDays: Int = 0,

    // Configuracion
    @ColumnInfo(name = "included_medication_ids")
    val includedMedicationIds: List<String> = emptyList(),

    @ColumnInfo(name = "included_sections")
    val includedSections: List<String> = emptyList(),

    // Formato
    @ColumnInfo(name = "output_format")
    val outputFormat: OutputFormat = OutputFormat.PDF,

    // Archivo
    @ColumnInfo(name = "file_url")
    val fileUrl: String? = null,

    @ColumnInfo(name = "file_size_bytes")
    val fileSizeBytes: Int = 0,

    @ColumnInfo(name = "file_hash")
    val fileHash: String? = null,

    // Datos
    @ColumnInfo(name = "report_data")
    val reportData: ByteArray? = null,  // JSON cifrado

    // Compartir
    @ColumnInfo(name = "is_shared")
    val isShared: Boolean = false,

    @ColumnInfo(name = "shared_with")
    val sharedWith: List<String> = emptyList(),

    @ColumnInfo(name = "shared_at")
    val sharedAt: Instant? = null,

    @ColumnInfo(name = "expires_at")
    val expiresAt: Instant? = null,

    // Estado
    @ColumnInfo(name = "status")
    val status: ReportStatus = ReportStatus.DRAFT,

    // Sync
    @ColumnInfo(name = "sync_enabled")
    val syncEnabled: Boolean = false,

    @ColumnInfo(name = "sync_status")
    val syncStatus: SyncStatus = SyncStatus.LOCAL_ONLY,

    @ColumnInfo(name = "last_sync_at")
    val lastSyncAt: Instant? = null,

    @ColumnInfo(name = "version")
    val version: Int = 1,

    // Timestamps
    @ColumnInfo(name = "created_at")
    val createdAt: Instant = Instant.now(),

    @ColumnInfo(name = "generated_at")
    val generatedAt: Instant? = null,

    @ColumnInfo(name = "updated_at")
    val updatedAt: Instant = Instant.now(),

    @ColumnInfo(name = "is_deleted")
    val isDeleted: Boolean = false
)

enum class ReportType {
    ADHERENCE,
    MEDICATIONS,
    COMPLETE
}

enum class OutputFormat {
    PDF,
    CSV,
    FHIR
}

enum class ReportStatus {
    DRAFT,
    GENERATING,
    READY,
    EXPIRED
}

3. Servidor: NO HAY ENTIDADES SERVIDOR

3.1. Justificacion

El modulo de Adherencia es 100% local por las siguientes razones:

  1. Privacy-First: Datos de adherencia son extremadamente sensibles
  2. Offline-First: Metricas deben calcularse sin conexion
  3. ML On-Device: Patrones detectados por ML local NUNCA salen
  4. Zero-Knowledge: Servidor no debe ver comportamiento de adherencia

3.2. Sincronizacion Opcional (Pro/Perfect)

Si el usuario opta activamente por sincronizar adherencia (Pro/Perfect), los datos se sincronizan via srv_calendar_sync usando entity_type especificos:

entity_type Entidad Cliente Cifrado
adherence_daily cli_adherence_daily E2E
adherence_by_medication cli_adherence_by_medication E2E
adherence_streaks cli_adherence_streaks E2E
adherence_anomalies cli_adherence_anomalies E2E
adherence_reports cli_adherence_reports E2E

NOTA: cli_adherence_patterns y cli_adherence_insightsNUNCA se sincronizan, incluso en Pro/Perfect.

Flujo de Opt-In:

1. Usuario accede a Configuracion > Privacidad > Sincronizacion
2. Toggle "Sincronizar datos de adherencia" (OFF por defecto)
3. Sistema muestra dialogo:
   "Tus datos de adherencia son sensibles. Solo sincroniza si quieres
    acceder desde multiples dispositivos. Los datos se cifran E2E."
4. Usuario confirma
5. Sistema actualiza `syncEnabled = true` en entidades
6. Proxima sincronizacion incluye blobs de adherencia

4. Diagrama ER del Modulo

erDiagram
    %% CLIENTE
    cli_dose_records ||--o{ cli_adherence_daily : "calculates"
    cli_dose_records ||--o{ cli_adherence_by_medication : "calculates"
    cli_medications ||--|| cli_adherence_by_medication : "tracks"
    cli_adherence_daily ||--o{ cli_adherence_streaks : "generates"
    cli_adherence_daily ||--o{ cli_adherence_patterns : "analyzes"
    cli_adherence_patterns ||--o{ cli_adherence_insights : "generates"
    cli_medications ||--o{ cli_adherence_anomalies : "reports"
    cli_adherence_daily ||--o{ cli_adherence_reports : "includes"
    cli_adherence_by_medication ||--o{ cli_adherence_reports : "includes"

    %% Entidades
    cli_adherence_daily {
        string id PK
        string userId
        date date
        int dosesScheduled
        int dosesTaken
        double adherencePercentage
        string adherenceScore
        int currentStreak
        boolean syncEnabled
    }

    cli_adherence_by_medication {
        string id PK
        string userId
        string medicationId FK
        double adherencePercentage
        string trend
        string problematicTime_LOCAL_ONLY
        boolean syncEnabled
    }

    cli_adherence_streaks {
        string id PK
        string userId
        int streakDays
        date startDate
        date endDate
        boolean isCurrent
        boolean syncEnabled
    }

    cli_adherence_patterns {
        string id PK
        string userId
        string patternType
        string title
        double confidence
        boolean isActive
        NO_SYNC_LOCAL_ONLY
    }

    cli_adherence_anomalies {
        string id PK
        string userId
        string medicationId FK
        string anomalyType
        string severity
        datetime occurredAt
        string syncStatus
    }

    cli_adherence_insights {
        string id PK
        string userId
        string insightType
        string message
        boolean isRead
        NO_SYNC_LOCAL_ONLY
    }

    cli_adherence_reports {
        string id PK
        string userId
        string reportType
        date startDate
        date endDate
        string outputFormat
        boolean syncEnabled
    }

5. Reglas de Negocio Reflejadas en Modelo

ID Regla de Negocio Implementacion en Modelo
RN-ADH-001 Adherencia = Tomadas / Programadas x 100 cli_adherence_daily.adherencePercentage calculado
RN-ADH-002 Excelente >=95%, Buena 80-94%, Regular 60-79%, Baja <60% cli_adherence_daily.adherenceScore enum
RN-ADH-003 Racha se rompe con cualquier omision cli_adherence_streaks.isBroken = true cuando skip/missed
RN-ADH-004 Patrones requieren minimo 7 dias de datos cli_adherence_patterns.sampleSize >= 7
RN-ADH-005 Insights se generan maximo 1 por dia Logica en generador de insights
RN-ADH-006 Adherencia ponderada considera criticidad cli_adherence_by_medication con peso de criticidad
RN-ADH-007 Reportes incluyen periodo, metricas, patrones cli_adherence_reports.includedSections

6. Consideraciones de Cifrado

6.1. Campos PHI Cifrados E2E

Los siguientes campos contienen PHI y DEBEN cifrarse antes de sincronizar (si syncEnabled = true):

Entidad Campos PHI Nivel PHI Razon
cli_adherence_daily dosesScheduled, dosesTaken, adherencePercentage Alto Revela intensidad y cumplimiento de tratamiento
cli_adherence_by_medication medicationId, adherencePercentage, trend Alto Referencia directa a medicamentos
cli_adherence_streaks streakDays, startDate, endDate Medio Revela patron de cumplimiento
cli_adherence_anomalies medicationId, anomalyType, descriptionText Alto Problemas medicos (PHI critico)
cli_adherence_reports includedMedicationIds, reportData Alto Contiene resumen de tratamiento

6.2. Datos Sensibles (LOCAL_ONLY por defecto)

Los siguientes datos son LOCAL_ONLYyNUNCA se sincronizan:

Entidad Razon
cli_adherence_patterns Patrones ML on-device, altamente sensibles
cli_adherence_insights Volatiles, se regeneran localmente
cli_adherence_by_medication.problematicTime Patron detectado por ML local
cli_adherence_by_medication.problematicDay Patron detectado por ML local
cli_adherence_by_medication.problematicPattern Patron detectado por ML local

6.3. Padding de Blobs

Los siguientes campos contienen listas de longitud variable y DEBEN aplicar padding antes del cifrado E2E:

Campo Razon
cli_adherence_by_medication (lista por usuario) Cantidad de medicamentos es metadata sensible
cli_adherence_reports.includedMedicationIds Lista revela cantidad de medicamentos
cli_adherence_reports.sharedWith Lista de destinatarios
cli_adherence_patterns.affectedMedicationIds Lista de medicamentos afectados
cli_adherence_anomalies (lista por usuario) Cantidad de anomalias es metadata

Algoritmo de Padding: Ver MDL-CAL-001 seccion 6.3 o 04-seguridad-cliente.md seccion 3.7.


7. Calculos y Agregaciones

7.1. Formula de Adherencia

ADHERENCIA SIMPLE:
adherencePercentage = (dosesTaken / dosesScheduled) * 100

ADHERENCIA PUNTUAL:
punctualityPercentage = (dosesOnTime / dosesScheduled) * 100

ADHERENCIA PONDERADA:
adherenceWeighted = SUM(dosesTaken[med] * weight[med]) / SUM(dosesScheduled[med] * weight[med]) * 100

Donde:
- weight[med] = criticidad del medicamento (1.0 = normal, 1.5 = critico)
- dosesTaken[med] = tomas confirmadas del medicamento
- dosesScheduled[med] = tomas programadas del medicamento

7.2. Deteccion de Patrones

El motor de ML on-device analiza cli_dose_records para detectar patrones:

PATRONES DETECTADOS:
1. DAY_OF_WEEK: Analiza adherencia por dia de la semana
   - Input: 14+ dias de datos
   - Output: Dia con menor adherencia

2. TIME_OF_DAY: Analiza adherencia por hora del dia
   - Input: 14+ dias de datos
   - Output: Hora con mas omisiones

3. WEEKEND_EFFECT: Compara laborales vs fines de semana
   - Input: 21+ dias de datos
   - Output: Diferencia % laborales vs fines de semana

4. MONTHLY_CYCLE: Detecta patrones mensuales
   - Input: 60+ dias de datos
   - Output: Semana del mes con menor adherencia

ALGORITMO:
1. Obtener cli_dose_records de ultimos N dias
2. Agrupar por dimension (dia, hora, etc.)
3. Calcular adherencia por grupo
4. Detectar grupos con adherencia < 80%
5. Verificar significancia estadistica
6. Generar cli_adherence_pattern si confidence > 0.7

7.3. Generacion de Insights

INSIGHTS GENERADOS:
1. PATTERN_DETECTED: Cuando se detecta patron nuevo
   - "Olvidas mas las tomas de las 14:00"

2. IMPROVEMENT_SUGGESTION: Basado en patrones
   - "Cambia la toma de las 14:00 al almuerzo"

3. ACHIEVEMENT: Hitos alcanzados
   - "¡7 dias de adherencia perfecta!"

4. ALERT: Adherencia critica
   - "Tu adherencia a Metformina bajo a 65%"

5. MOTIVATION: Rachas y progreso
   - "Estas a 2 dias de tu mejor racha"

FRECUENCIA:
- PATTERN_DETECTED: Al detectar patron (max 1/semana)
- IMPROVEMENT_SUGGESTION: Max 1/dia
- ACHIEVEMENT: Al alcanzar hito
- ALERT: Al bajar de 80% por 3 dias
- MOTIVATION: Max 2/dia

8. Indices y Performance

8.1. Cliente (SQLite/Realm)

// iOS Realm - Indices criticos
class AdherenceDaily: Object {
    @Persisted(indexed: true) var date: Date
    @Persisted(indexed: true) var adherencePercentage: Double
}

class AdherenceByMedication: Object {
    @Persisted(indexed: true) var medicationId: String
    @Persisted(indexed: true) var adherencePercentage: Double
}

class AdherenceStreak: Object {
    @Persisted(indexed: true) var isCurrent: Bool
    @Persisted(indexed: true) var streakDays: Int
}
// Android Room - Indices
@Entity(
    tableName = "cli_adherence_daily",
    indices = [
        Index(value = ["date"], unique = true),  // Query por dia
        Index(value = ["adherence_percentage"]),  // Filtros
        Index(value = ["sync_enabled"])           // Sync queries
    ]
)

@Entity(
    tableName = "cli_adherence_by_medication",
    indices = [
        Index(value = ["medication_id"]),        // FK lookup
        Index(value = ["adherence_percentage"])  // Filtros
    ]
)

8.2. Queries Criticas

-- Query 1: Adherencia ultimos 30 dias
SELECT * FROM cli_adherence_daily
WHERE date >= date('now', '-30 days')
ORDER BY date DESC;

-- Query 2: Adherencia por medicamento
SELECT * FROM cli_adherence_by_medication
WHERE medication_id = ?
ORDER BY period_days DESC;

-- Query 3: Racha actual
SELECT * FROM cli_adherence_streaks
WHERE is_current = TRUE
ORDER BY streak_days DESC
LIMIT 1;

-- Query 4: Patrones activos
SELECT * FROM cli_adherence_patterns
WHERE is_active = TRUE
  AND confidence > 0.7
ORDER BY detected_at DESC;

-- Query 5: Insights no leidos
SELECT * FROM cli_adherence_insights
WHERE is_read = FALSE
  AND is_dismissed = FALSE
  AND (valid_until IS NULL OR valid_until > datetime('now'))
ORDER BY priority DESC, generated_at DESC;

9. Migraciones

9.1. Migration 001: Create Adherence Schema (Cliente iOS)

// Realm Migration - Version 1
let config = Realm.Configuration(
    schemaVersion: 1,
    migrationBlock: { migration, oldSchemaVersion in
        if oldSchemaVersion < 1 {
            // Primera version - crear esquema
            migration.enumerateObjects(ofType: AdherenceDaily.className()) { oldObject, newObject in
                // Valores por defecto
                newObject!["syncEnabled"] = false
                newObject!["syncStatus"] = "local_only"
            }

            migration.enumerateObjects(ofType: AdherencePattern.className()) { oldObject, newObject in
                // Patrones siempre local only
                newObject!["confidence"] = 0.0
            }
        }
    }
)

Realm.Configuration.defaultConfiguration = config

9.2. Migration 001: Create Adherence Schema (Cliente Android)

// Room Migration - Version 1 to 2
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        // Crear tabla adherence_daily
        database.execSQL("""
            CREATE TABLE IF NOT EXISTS cli_adherence_daily (
                id TEXT PRIMARY KEY NOT NULL,
                user_id TEXT NOT NULL,
                date TEXT NOT NULL,
                doses_scheduled INTEGER NOT NULL DEFAULT 0,
                doses_taken INTEGER NOT NULL DEFAULT 0,
                doses_skipped INTEGER NOT NULL DEFAULT 0,
                doses_missed INTEGER NOT NULL DEFAULT 0,
                adherence_percentage REAL NOT NULL DEFAULT 0.0,
                adherence_score TEXT NOT NULL DEFAULT 'POOR',
                current_streak INTEGER NOT NULL DEFAULT 0,
                max_streak INTEGER NOT NULL DEFAULT 0,
                sync_enabled INTEGER NOT NULL DEFAULT 0,
                sync_status TEXT NOT NULL DEFAULT 'LOCAL_ONLY',
                calculated_at INTEGER NOT NULL,
                updated_at INTEGER NOT NULL
            )
        """)

        // Indices
        database.execSQL("""
            CREATE UNIQUE INDEX IF NOT EXISTS idx_adherence_daily_date
            ON cli_adherence_daily(user_id, date)
        """)

        database.execSQL("""
            CREATE INDEX IF NOT EXISTS idx_adherence_daily_percentage
            ON cli_adherence_daily(adherence_percentage)
        """)

        // ... resto de tablas ...
    }
}

10. Referencias Cruzadas

Documento Relacion
DB-ERD-001 Diagrama ER completo
MTS-ADH-001 Especificacion funcional
MDL-CAL-001 Usa dose_records para calcular adherencia
MDL-MED-001 FK a medications
MDL-USR-001 FK a users
02-arquitectura-cliente-servidor.md Arquitectura dual
04-seguridad-cliente.md Cifrado E2E, ML on-device
07-testing-strategy.md Testing de calculos de adherencia

Documento generado por DatabaseDrone (Doce de Quince) - SpecQueen Technical Division "Adherencia es 100% LOCAL. Privacy-first, ML on-device, Zero-Knowledge."