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
- 1.1. Principios
- 1.2. Clasificacion de Datos
- 2. Entidades del Cliente (LOCAL/SYNCED_E2E)
- 2.1. cli_adherence_daily
- 2.2. cli_adherence_by_medication
- 2.3. cli_adherence_streaks
- 2.4. cli_adherence_patterns
- 2.5. cli_adherence_anomalies
- 2.6. cli_adherence_insights
- 2.7. cli_adherence_reports
- 3. Servidor: NO HAY ENTIDADES SERVIDOR
- 3.1. Justificacion
- 3.2. Sincronizacion Opcional (Pro/Perfect)
- 4. Diagrama ER del Modulo
- 5. Reglas de Negocio Reflejadas en Modelo
- 6. Consideraciones de Cifrado
- 6.1. Campos PHI Cifrados E2E
- 6.2. Datos Sensibles (LOCAL_ONLY por defecto)
- 6.3. Padding de Blobs
- 7. Calculos y Agregaciones
- 7.1. Formula de Adherencia
- 7.2. Deteccion de Patrones
- 7.3. Generacion de Insights
- 8. Indices y Performance
- 8.1. Cliente (SQLite/Realm)
- 8.2. Queries Criticas
- 9. Migraciones
- 9.1. Migration 001: Create Adherence Schema (Cliente iOS)
- 9.2. Migration 001: Create Adherence Schema (Cliente Android)
- 10. Referencias Cruzadas
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:
- Privacy-First: Datos de adherencia son extremadamente sensibles
- Offline-First: Metricas deben calcularse sin conexion
- ML On-Device: Patrones detectados por ML local NUNCA salen
- 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_patternsycli_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."