Saltar a contenido

Modelo de Datos: Alertas y Notificaciones

Identificador: MDL-ALT-001 Version: 1.0.0 Fecha: 2025-12-07 Autor: SpecQueen Technical Division Modulo Funcional: MTS-ALT-001


1. Resumen

Este documento define el modelo de datos para el sistema de alertas y notificaciones de MedTime, incluyendo:

  • Gestion de tokens push (FCM/APNs)
  • Cola de notificaciones del servidor
  • Configuracion de alertas por usuario
  • Historial de notificaciones
  • Contactos de emergencia
  • Logs de SMS y escalamientos

2. Arquitectura de Datos

┌─────────────────────────────────────────────────────────────────┐
│                    ARQUITECTURA DUAL                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  SERVIDOR (PostgreSQL + RLS)                                    │
│  ├── srv_push_tokens         → Tokens FCM/APNs                  │
│  ├── srv_notification_queue  → Cola de envio                    │
│  ├── srv_sms_log             → Auditoria SMS                    │
│  └── srv_escalation_log      → Log escalamientos                │
│                                                                  │
│  CLIENTE (SQLite/Realm/Room cifrado)                            │
│  ├── cli_scheduled_alerts    → Alertas programadas              │
│  ├── cli_alert_config        → Configuracion usuario            │
│  ├── cli_alert_history       → Historial local                  │
│  ├── cli_emergency_contacts  → Contactos emergencia             │
│  └── cli_dnd_schedule        → Modo no molestar                 │
│                                                                  │
│  SYNC (Blobs E2E)                                               │
│  ├── alert_config            → Cifrado, synced                  │
│  ├── emergency_contacts      → Cifrado, synced                  │
│  └── alert_history           → Cifrado, synced (90 dias)        │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

3. Clasificacion de Datos

Entidad Clasificacion PHI/PII Cifrado Sync
srv_push_tokens SERVER_ONLY No TLS No
srv_notification_queue SERVER_ONLY No TLS No
srv_sms_log SERVER_ONLY No (solo metadata) TLS No
srv_escalation_log SERVER_ONLY No (solo IDs) TLS No
cli_scheduled_alerts SYNCED_E2E Si (med names) AES-256 Si
cli_alert_config SYNCED_E2E No AES-256 Si
cli_alert_history SYNCED_E2E Si AES-256 Si
cli_emergency_contacts SYNCED_E2E Si (nombres, tel) AES-256 Si
cli_dnd_schedule LOCAL_ONLY No AES-256 No

3.1. Padding de Blobs (Prevencion de Metadata Attack)

Los blobs cifrados E2E que contienen listas de longitud variable (como medicationIds en cli_scheduled_alerts) deben aplicar padding antes del cifrado para ocultar el tamano real de los datos.

Campos que requieren padding:

  • cli_scheduled_alerts.medicationIds - Lista de IDs de medicamentos
  • cli_alert_history.medicationIds - Lista de IDs de medicamentos
  • cli_alert_history.escalatedTo - Lista de IDs de cuidadores

Implementacion: Ver 04-seguridad-cliente.md seccion 3.7 para detalles del algoritmo de padding PKCS#7 a multiplos de 1KB.

Razon: Sin padding, un atacante podria inferir la cantidad de medicamentos de un usuario analizando el tamano del blob cifrado, constituyendo un metadata attack que viola el principio Zero-Knowledge.


4. Modelos del Servidor

4.1. srv_push_tokens

Almacena tokens de push notification por dispositivo.

-- ============================================================
-- TABLA: srv_push_tokens
-- Descripcion: Tokens FCM/APNs para push notifications
-- RLS: Usuario solo ve sus propios tokens
-- ============================================================

CREATE TABLE srv_push_tokens (
    -- Identificadores
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID NOT NULL REFERENCES srv_users(id) ON DELETE CASCADE,
    device_id       UUID NOT NULL REFERENCES srv_devices(id) ON DELETE CASCADE,

    -- Token data
    token           TEXT NOT NULL,
    platform        VARCHAR(10) NOT NULL CHECK (platform IN ('ios', 'android', 'web')),
    provider        VARCHAR(10) NOT NULL CHECK (provider IN ('fcm', 'apns')),

    -- Estado
    is_active       BOOLEAN DEFAULT TRUE,
    last_used_at    TIMESTAMPTZ,
    failed_count    INTEGER DEFAULT 0,

    -- Metadata
    app_version     VARCHAR(20),
    os_version      VARCHAR(20),

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

    -- Constraints
    CONSTRAINT uq_push_token UNIQUE (token),
    CONSTRAINT uq_device_token UNIQUE (device_id, provider)
);

-- Indices
CREATE INDEX idx_push_tokens_user ON srv_push_tokens(user_id) WHERE is_active = TRUE;
CREATE INDEX idx_push_tokens_platform ON srv_push_tokens(platform, is_active);

-- RLS
ALTER TABLE srv_push_tokens ENABLE ROW LEVEL SECURITY;

CREATE POLICY push_tokens_user_policy ON srv_push_tokens
    FOR ALL
    USING (user_id = current_setting('app.current_user_id')::UUID);

-- Trigger updated_at
CREATE TRIGGER trg_push_tokens_updated
    BEFORE UPDATE ON srv_push_tokens
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();

4.2. srv_notification_queue

Cola de notificaciones pendientes de envio.

-- ============================================================
-- TABLA: srv_notification_queue
-- Descripcion: Cola de notificaciones push/sms pendientes
-- RLS: Solo sistema puede acceder (service role)
-- ============================================================

CREATE TABLE srv_notification_queue (
    -- Identificadores
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID NOT NULL REFERENCES srv_users(id) ON DELETE CASCADE,

    -- Contenido (NO PHI - solo referencias)
    notification_type VARCHAR(50) NOT NULL,
    -- Tipos: reminder, escalation, emergency, system, quota_warning

    template_id     VARCHAR(50) NOT NULL,
    -- Templates genericos: generic_alert, missed_dose, escalation_alert, etc.
    -- NUNCA contienen PHI - solo IDs para mostrar texto generico

    content_blob_id UUID,
    -- Referencia al blob cifrado E2E con contenido real (titulo, cuerpo, PHI)
    -- El cliente descifra localmente para mostrar contenido completo

    encrypted_preview BYTEA,
    -- Preview cifrado E2E (opcional) para evitar round-trip al servidor

    -- Targeting
    target_devices  UUID[] DEFAULT '{}',  -- NULL = todos los dispositivos
    channels        VARCHAR(20)[] DEFAULT ARRAY['push'],
    -- Channels: push, sms, email, call

    -- Scheduling
    scheduled_for   TIMESTAMPTZ NOT NULL,
    priority        VARCHAR(10) DEFAULT 'normal'
                    CHECK (priority IN ('low', 'normal', 'high', 'critical')),

    -- Estado
    status          VARCHAR(20) DEFAULT 'pending'
                    CHECK (status IN ('pending', 'processing', 'sent', 'failed', 'cancelled')),
    attempts        INTEGER DEFAULT 0,
    max_attempts    INTEGER DEFAULT 3,
    last_attempt_at TIMESTAMPTZ,
    error_message   TEXT,

    -- Metadata (sin PHI)
    action_type     VARCHAR(50),
    -- action_type: view_alert, view_medication, view_escalation
    related_entity_id UUID,
    -- ID de la entidad relacionada (alerta, medicamento, etc.)

    -- Timestamps
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    sent_at         TIMESTAMPTZ,

    -- TTL
    expires_at      TIMESTAMPTZ DEFAULT (NOW() + INTERVAL '24 hours')
);

-- Indices
CREATE INDEX idx_notif_queue_pending ON srv_notification_queue(scheduled_for, status)
    WHERE status = 'pending';
CREATE INDEX idx_notif_queue_user ON srv_notification_queue(user_id, created_at DESC);
CREATE INDEX idx_notif_queue_expires ON srv_notification_queue(expires_at)
    WHERE status = 'pending';

-- Particionado por fecha (opcional para alto volumen)
-- CREATE TABLE srv_notification_queue_y2025m12 PARTITION OF srv_notification_queue
--     FOR VALUES FROM ('2025-12-01') TO ('2026-01-01');

-- Cleanup job (ejecutar periodicamente)
-- DELETE FROM srv_notification_queue WHERE expires_at < NOW() AND status IN ('sent', 'cancelled');

4.3. srv_sms_log

Log de SMS enviados para auditoria y billing.

-- ============================================================
-- TABLA: srv_sms_log
-- Descripcion: Log de SMS enviados (billing, audit)
-- RLS: Usuario ve sus propios logs, admin ve todos
-- ============================================================

CREATE TABLE srv_sms_log (
    -- Identificadores
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID NOT NULL REFERENCES srv_users(id) ON DELETE CASCADE,
    notification_id UUID REFERENCES srv_notification_queue(id),

    -- Destino (hash, no numero real)
    phone_hash      VARCHAR(64) NOT NULL,  -- SHA-256 del numero
    country_code    VARCHAR(5),

    -- Tipo de SMS
    sms_type        VARCHAR(20) NOT NULL
                    CHECK (sms_type IN ('reminder', 'escalation', 'emergency', 'verification')),

    -- Provider
    provider        VARCHAR(20) DEFAULT 'twilio',
    provider_id     VARCHAR(100),  -- ID del mensaje en Twilio

    -- Estado
    status          VARCHAR(20) DEFAULT 'queued'
                    CHECK (status IN ('queued', 'sent', 'delivered', 'failed', 'undelivered')),
    error_code      VARCHAR(20),
    error_message   TEXT,

    -- Billing
    segments        INTEGER DEFAULT 1,
    cost_cents      INTEGER,

    -- Timestamps
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    sent_at         TIMESTAMPTZ,
    delivered_at    TIMESTAMPTZ,

    -- Periodo de facturacion
    billing_period  VARCHAR(7) NOT NULL DEFAULT TO_CHAR(NOW(), 'YYYY-MM')
);

-- Indices
CREATE INDEX idx_sms_log_user_period ON srv_sms_log(user_id, billing_period);
CREATE INDEX idx_sms_log_status ON srv_sms_log(status, created_at) WHERE status = 'queued';

-- RLS
ALTER TABLE srv_sms_log ENABLE ROW LEVEL SECURITY;

CREATE POLICY sms_log_user_policy ON srv_sms_log
    FOR SELECT
    USING (user_id = current_setting('app.current_user_id')::UUID);

-- Vista para cuota mensual
CREATE VIEW v_sms_quota AS
SELECT
    user_id,
    billing_period,
    COUNT(*) FILTER (WHERE sms_type != 'emergency') as regular_sms_count,
    COUNT(*) FILTER (WHERE sms_type = 'emergency') as emergency_sms_count,
    SUM(cost_cents) as total_cost_cents
FROM srv_sms_log
WHERE status IN ('sent', 'delivered')
GROUP BY user_id, billing_period;

4.4. srv_escalation_log

Log de escalamientos a cuidadores.

-- ============================================================
-- TABLA: srv_escalation_log
-- Descripcion: Log de escalamientos a cuidadores
-- RLS: Paciente y cuidador ven sus escalamientos
-- ============================================================

CREATE TABLE srv_escalation_log (
    -- Identificadores
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    patient_id      UUID NOT NULL REFERENCES srv_users(id) ON DELETE CASCADE,
    caregiver_id    UUID NOT NULL REFERENCES srv_users(id) ON DELETE CASCADE,
    relation_id     UUID REFERENCES srv_user_relations(id),

    -- Contexto (sin PHI)
    escalation_type VARCHAR(30) NOT NULL
                    CHECK (escalation_type IN (
                        'missed_dose',
                        'critical_value',
                        'panic_button',
                        'inactivity',
                        'pattern_anomaly'
                    )),

    -- Referencias a blobs (no datos directos)
    related_blob_id UUID,  -- Referencia al blob cifrado

    -- Timing
    triggered_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    window_minutes  INTEGER,  -- Ventana de espera antes de escalar

    -- Estado
    status          VARCHAR(20) DEFAULT 'pending'
                    CHECK (status IN ('pending', 'notified', 'acknowledged', 'resolved', 'expired')),

    -- Respuesta del cuidador
    acknowledged_at TIMESTAMPTZ,
    resolved_at     TIMESTAMPTZ,
    resolution      VARCHAR(30)
                    CHECK (resolution IN (
                        'confirmed_ok',
                        'marked_taken',
                        'contacted_patient',
                        'called_emergency',
                        'false_alarm',
                        'no_response'
                    )),

    -- Canales usados
    channels_used   VARCHAR(20)[] DEFAULT '{}',

    -- Timestamps
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    expires_at      TIMESTAMPTZ DEFAULT (NOW() + INTERVAL '4 hours')
);

-- Indices
CREATE INDEX idx_escalation_patient ON srv_escalation_log(patient_id, triggered_at DESC);
CREATE INDEX idx_escalation_caregiver ON srv_escalation_log(caregiver_id, status);
CREATE INDEX idx_escalation_pending ON srv_escalation_log(status, triggered_at)
    WHERE status = 'pending';

-- RLS
ALTER TABLE srv_escalation_log ENABLE ROW LEVEL SECURITY;

CREATE POLICY escalation_patient_policy ON srv_escalation_log
    FOR ALL
    USING (
        patient_id = current_setting('app.current_user_id')::UUID
        OR caregiver_id = current_setting('app.current_user_id')::UUID
    );

5. Modelos del Cliente

5.1. cli_scheduled_alerts (iOS - Swift/Realm)

// ============================================================
// MODELO: ScheduledAlert
// Descripcion: Alertas programadas localmente
// Almacenamiento: Realm cifrado
// Sync: E2E via EncryptedBlob
// ============================================================

import RealmSwift

class ScheduledAlert: Object, Identifiable {
    // Identificadores
    @Persisted(primaryKey: true) var id: String = UUID().uuidString
    @Persisted var oderId: String = ""  // ID de referencia al schedule

    // Tipo de alerta
    @Persisted var alertType: String = ""  // reminder, refill, expiry, measurement, critical

    // Scheduling
    @Persisted var scheduledFor: Date = Date()
    @Persisted var repeatPattern: String?  // daily, weekly, custom
    @Persisted var repeatDays: List<Int>   // 0=Sun, 1=Mon, etc.

    // Contenido (puede contener PHI)
    @Persisted var title: String = ""
    @Persisted var body: String = ""
    @Persisted var medicationIds: List<String>  // Referencias a cli_medications

    // Configuracion
    @Persisted var priority: String = "normal"  // low, normal, high, critical
    @Persisted var soundName: String = "default"
    @Persisted var vibrationPattern: String = "default"
    @Persisted var showMedicationNames: Bool = false  // Privacidad

    // Recordatorios
    @Persisted var preAlertMinutes: Int = 0  // 0 = sin pre-alerta
    @Persisted var reminderIntervals: List<Int>  // [15, 30, 60] minutos
    @Persisted var maxReminders: Int = 3

    // Estado
    @Persisted var isActive: Bool = true
    @Persisted var currentReminderCount: Int = 0
    @Persisted var lastTriggeredAt: Date?

    // iOS Notification IDs
    @Persisted var notificationIds: List<String>  // UNNotification identifiers

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

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

    // Computed
    var isOverdue: Bool {
        return scheduledFor < Date() && isActive
    }
}

// Embedded: Configuracion de escalamiento
class EscalationConfig: EmbeddedObject {
    @Persisted var isEnabled: Bool = false
    @Persisted var windowMinutes: Int = 60  // 15-90
    @Persisted var caregiverIds: List<String>
    @Persisted var notifyOnCriticalOnly: Bool = false
    @Persisted var scheduleStart: String?  // "08:00"
    @Persisted var scheduleEnd: String?    // "22:00"
}

5.2. cli_scheduled_alerts (Android - Kotlin/Room)

// ============================================================
// MODELO: ScheduledAlert
// Descripcion: Alertas programadas localmente
// Almacenamiento: Room cifrado (SQLCipher)
// Sync: E2E via EncryptedBlob
// ============================================================

package com.medtime.data.entities

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

@Entity(
    tableName = "cli_scheduled_alerts",
    indices = [
        Index(value = ["scheduled_for", "is_active"]),
        Index(value = ["alert_type"])
    ]
)
data class ScheduledAlert(
    @PrimaryKey
    @ColumnInfo(name = "id")
    val id: String = UUID.randomUUID().toString(),

    @ColumnInfo(name = "order_id")
    val orderId: String = "",

    // Tipo
    @ColumnInfo(name = "alert_type")
    val alertType: AlertType = AlertType.REMINDER,

    // Scheduling
    @ColumnInfo(name = "scheduled_for")
    val scheduledFor: Instant = Instant.now(),

    @ColumnInfo(name = "repeat_pattern")
    val repeatPattern: RepeatPattern? = null,

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

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

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

    @ColumnInfo(name = "medication_ids")
    val medicationIds: List<String> = emptyList(),  // TypeConverter

    // Configuracion
    @ColumnInfo(name = "priority")
    val priority: AlertPriority = AlertPriority.NORMAL,

    @ColumnInfo(name = "sound_name")
    val soundName: String = "default",

    @ColumnInfo(name = "vibration_pattern")
    val vibrationPattern: String = "default",

    @ColumnInfo(name = "show_medication_names")
    val showMedicationNames: Boolean = false,

    // Recordatorios
    @ColumnInfo(name = "pre_alert_minutes")
    val preAlertMinutes: Int = 0,

    @ColumnInfo(name = "reminder_intervals")
    val reminderIntervals: List<Int> = listOf(15, 30, 60),

    @ColumnInfo(name = "max_reminders")
    val maxReminders: Int = 3,

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

    @ColumnInfo(name = "current_reminder_count")
    val currentReminderCount: Int = 0,

    @ColumnInfo(name = "last_triggered_at")
    val lastTriggeredAt: Instant? = null,

    // Android Alarm IDs
    @ColumnInfo(name = "alarm_ids")
    val alarmIds: List<Int> = emptyList(),

    // 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()
)

enum class AlertType {
    REMINDER,      // Recordatorio de toma
    REFILL,        // Reabastecimiento
    EXPIRY,        // Caducidad
    MEASUREMENT,   // Recordatorio medicion
    CRITICAL,      // Alerta critica
    APPOINTMENT,   // Cita medica
    SYSTEM         // Sistema
}

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

enum class RepeatPattern {
    DAILY, WEEKLY, CUSTOM
}

// Embedded escalation config
data class EscalationConfig(
    val isEnabled: Boolean = false,
    val windowMinutes: Int = 60,
    val caregiverIds: List<String> = emptyList(),
    val notifyOnCriticalOnly: Boolean = false,
    val scheduleStart: String? = null,
    val scheduleEnd: String? = null
)

5.3. cli_alert_config

// iOS - Swift/Realm
class AlertConfig: Object {
    @Persisted(primaryKey: true) var id: String = "user_config"

    // Recordatorios
    @Persisted var preAlertDefault: Int = 0  // minutos
    @Persisted var reminder1Minutes: Int = 15
    @Persisted var reminder2Minutes: Int = 30
    @Persisted var markMissedMinutes: Int = 60

    // Sonidos y vibracion
    @Persisted var defaultSound: String = "default"
    @Persisted var vibrationEnabled: Bool = true
    @Persisted var ledColor: String = "blue"  // Android only

    // Privacidad
    @Persisted var showMedicationNames: Bool = false
    @Persisted var showOnLockScreen: Bool = true
    @Persisted var privateNotifications: Bool = false  // Solo "Recordatorio MedTime"

    // Escalamiento (Pro/Perfect)
    @Persisted var escalationEnabled: Bool = false
    @Persisted var escalationConfig: EscalationConfig?

    // Canales (Pro/Perfect)
    @Persisted var pushEnabled: Bool = true
    @Persisted var smsEnabled: Bool = false
    @Persisted var emailEnabled: Bool = false

    // Sync
    @Persisted var syncStatus: String = "pending"
    @Persisted var version: Int = 1
    @Persisted var updatedAt: Date = Date()
}

5.4. cli_alert_history

// iOS - Swift/Realm
class AlertHistory: Object, Identifiable {
    @Persisted(primaryKey: true) var id: String = UUID().uuidString
    @Persisted var alertId: String = ""

    // Evento
    @Persisted var event: String = ""
    // created, scheduled, triggered, snoozed, acknowledged, missed, cancelled

    // Contexto
    @Persisted var alertType: String = ""
    @Persisted var medicationIds: List<String>
    @Persisted var scheduledTime: Date?

    // Respuesta del usuario
    @Persisted var responseType: String?  // taken, snoozed, skipped
    @Persisted var responseTime: Date?
    @Persisted var snoozeMinutes: Int?
    @Persisted var note: String?

    // Metricas
    @Persisted var responseDelaySeconds: Int?  // Tiempo desde alerta hasta respuesta
    @Persisted var reminderCount: Int = 0

    // Escalamiento
    @Persisted var wasEscalated: Bool = false
    @Persisted var escalatedTo: List<String>  // caregiver IDs

    // Timestamps
    @Persisted var timestamp: Date = Date()

    // Sync
    @Persisted var syncStatus: String = "pending"
    @Persisted var version: Int = 1
}

5.5. cli_emergency_contacts

NOTA: La definicion canonica de cli_emergency_contacts se encuentra en MDL-USR-001-usuarios.md seccion 2.3. La implementacion aqui es para referencia del uso en el contexto de alertas y escalamiento. Ver MDL-USR-001 para la especificacion completa.

// iOS - Swift/Realm
class EmergencyContact: Object, Identifiable {
    @Persisted(primaryKey: true) var id: String = UUID().uuidString

    // Datos del contacto (PHI)
    @Persisted var name: String = ""
    @Persisted var phone: String = ""
    @Persisted var email: String?
    @Persisted var relationship: String = ""  // family, friend, doctor, other

    // Orden de prioridad
    @Persisted var priority: Int = 1  // 1 = primero en contactar

    // Permisos
    @Persisted var canReceiveAlerts: Bool = true
    @Persisted var canReceiveEmergency: Bool = true
    @Persisted var autoNotifyOnEmergency: Bool = false  // Requiere opt-in explicito

    // Disponibilidad
    @Persisted var availableStart: String?  // "08:00"
    @Persisted var availableEnd: String?    // "22:00"
    @Persisted var availableDays: List<Int> // 0=Sun, 1=Mon...

    // Estado
    @Persisted var isActive: Bool = true
    @Persisted var lastContactedAt: Date?

    // Sync
    @Persisted var syncStatus: String = "pending"
    @Persisted var version: Int = 1
    @Persisted var createdAt: Date = Date()
    @Persisted var updatedAt: Date = Date()
}

// Limites por tier
// Free: 2 contactos
// Pro: 5 contactos
// Perfect: 10 contactos

5.6. cli_dnd_schedule (Do Not Disturb)

// iOS - Swift/Realm
class DNDSchedule: Object, Identifiable {
    @Persisted(primaryKey: true) var id: String = UUID().uuidString

    // Nombre del horario
    @Persisted var name: String = ""  // "Noche", "Trabajo", etc.

    // Horario
    @Persisted var startTime: String = ""  // "22:00"
    @Persisted var endTime: String = ""    // "07:00"
    @Persisted var daysOfWeek: List<Int>   // 0=Sun, 1=Mon...

    // Comportamiento
    @Persisted var blockAllAlerts: Bool = false
    @Persisted var allowCritical: Bool = true  // Siempre permitir criticas
    @Persisted var allowEmergency: Bool = true

    // Excepciones (IDs de medicamentos que ignoran DND)
    @Persisted var exceptionMedicationIds: List<String>

    // Sync con sistema
    @Persisted var respectSystemDND: Bool = true

    // Estado
    @Persisted var isActive: Bool = true

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

// NOTA: LOCAL_ONLY - No se sincroniza entre dispositivos
// Cada dispositivo tiene su propia configuracion DND

6. Relaciones entre Entidades

┌─────────────────────────────────────────────────────────────────┐
│                    DIAGRAMA DE RELACIONES                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  srv_users                                                       │
│      │                                                           │
│      ├──< srv_push_tokens (1:N)                                 │
│      │       └── device_id → srv_devices                        │
│      │                                                           │
│      ├──< srv_notification_queue (1:N)                          │
│      │                                                           │
│      ├──< srv_sms_log (1:N)                                     │
│      │       └── notification_id → srv_notification_queue       │
│      │                                                           │
│      └──< srv_escalation_log (1:N como patient)                 │
│              └── caregiver_id → srv_users                       │
│              └── relation_id → srv_user_relations               │
│                                                                  │
│  ─────────────────────────────────────────────────────────────  │
│                                                                  │
│  cli_scheduled_alerts                                            │
│      │                                                           │
│      ├──> cli_medications (N:M via medicationIds)               │
│      │                                                           │
│      └──< cli_alert_history (1:N)                               │
│                                                                  │
│  cli_alert_config                                                │
│      └── Usuario tiene 1 config                                  │
│                                                                  │
│  cli_emergency_contacts                                          │
│      └── Usuario tiene N contactos (limite por tier)            │
│                                                                  │
│  cli_dnd_schedule                                                │
│      └── Usuario tiene N horarios DND                           │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

7. Indices y Performance

7.1. Servidor (PostgreSQL)

-- Push tokens
CREATE INDEX idx_push_tokens_user_active ON srv_push_tokens(user_id)
    WHERE is_active = TRUE;

-- Notification queue (critico para worker)
CREATE INDEX idx_notif_queue_worker ON srv_notification_queue(scheduled_for, status, priority)
    WHERE status = 'pending';

-- SMS log para cuota
CREATE INDEX idx_sms_quota ON srv_sms_log(user_id, billing_period, sms_type)
    WHERE status IN ('sent', 'delivered');

-- Escalation para dashboard cuidador
CREATE INDEX idx_escalation_caregiver_pending ON srv_escalation_log(caregiver_id, status, triggered_at)
    WHERE status IN ('pending', 'notified');

7.2. Cliente (SQLite/Realm)

// iOS Realm - Indices automaticos en @Persisted(indexed: true)
class ScheduledAlert: Object {
    @Persisted(indexed: true) var scheduledFor: Date
    @Persisted(indexed: true) var alertType: String
    @Persisted(indexed: true) var isActive: Bool
}

// Android Room - Indices en @Entity
@Entity(indices = [
    Index(value = ["scheduled_for", "is_active"]),
    Index(value = ["alert_type"]),
    Index(value = ["sync_status"])
])

8. Migraciones

8.1. Servidor

-- Migration: 001_create_alert_tables
-- Up
CREATE TABLE srv_push_tokens (...);
CREATE TABLE srv_notification_queue (...);
CREATE TABLE srv_sms_log (...);
CREATE TABLE srv_escalation_log (...);

-- Down
DROP TABLE IF EXISTS srv_escalation_log;
DROP TABLE IF EXISTS srv_sms_log;
DROP TABLE IF EXISTS srv_notification_queue;
DROP TABLE IF EXISTS srv_push_tokens;

8.2. Cliente

// iOS Realm Migration
let config = Realm.Configuration(
    schemaVersion: 2,
    migrationBlock: { migration, oldSchemaVersion in
        if oldSchemaVersion < 2 {
            // Agregar campo showMedicationNames con default
            migration.enumerateObjects(ofType: ScheduledAlert.className()) { oldObject, newObject in
                newObject!["showMedicationNames"] = false
            }
        }
    }
)

9. Validaciones

9.1. Servidor

-- Constraints en tablas
CHECK (platform IN ('ios', 'android', 'web'))
CHECK (provider IN ('fcm', 'apns'))
CHECK (priority IN ('low', 'normal', 'high', 'critical'))
CHECK (status IN ('pending', 'processing', 'sent', 'failed', 'cancelled'))

-- Trigger para validar cuota SMS
CREATE OR REPLACE FUNCTION check_sms_quota()
RETURNS TRIGGER AS $$
DECLARE
    current_count INTEGER;
    user_tier VARCHAR(10);
    tier_limit INTEGER;
BEGIN
    -- Obtener tier del usuario
    SELECT tier INTO user_tier FROM srv_users WHERE id = NEW.user_id;

    -- Definir limite por tier
    tier_limit := CASE user_tier
        WHEN 'pro' THEN 3
        WHEN 'perfect' THEN 10
        ELSE 0
    END;

    -- Emergencias son ilimitadas
    IF NEW.sms_type = 'emergency' THEN
        RETURN NEW;
    END IF;

    -- Contar SMS del periodo
    SELECT COUNT(*) INTO current_count
    FROM srv_sms_log
    WHERE user_id = NEW.user_id
      AND billing_period = NEW.billing_period
      AND sms_type != 'emergency'
      AND status IN ('sent', 'delivered');

    IF current_count >= tier_limit THEN
        RAISE EXCEPTION 'SMS quota exceeded for tier %', user_tier;
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_check_sms_quota
    BEFORE INSERT ON srv_sms_log
    FOR EACH ROW EXECUTE FUNCTION check_sms_quota();

9.2. Cliente

// iOS Validation
extension ScheduledAlert {
    func validate() throws {
        guard !title.isEmpty else {
            throw AlertValidationError.emptyTitle
        }
        guard scheduledFor > Date() || repeatPattern != nil else {
            throw AlertValidationError.pastDate
        }
        guard reminderIntervals.count <= 3 else {
            throw AlertValidationError.tooManyReminders
        }
    }
}

extension EmergencyContact {
    func validate(currentCount: Int, tier: UserTier) throws {
        let limit = tier.emergencyContactLimit
        guard currentCount < limit else {
            throw ContactValidationError.limitExceeded(limit: limit)
        }
        guard !phone.isEmpty else {
            throw ContactValidationError.emptyPhone
        }
        guard phone.isValidPhoneNumber else {
            throw ContactValidationError.invalidPhone
        }
    }
}

10. Referencias

Documento Relacion
MTS-ALT-001 Especificacion funcional
MDL-AUTH-001 FK a users, devices
MDL-MED-001 FK a medications, schedules
API-SYNC-001 Protocolo de sync
04-seguridad-cliente.md Cifrado E2E
05-seguridad-servidor.md RLS policies

Modelo generado por SpecQueen Technical Division - IT-05