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 medicamentoscli_alert_history.medicationIds- Lista de IDs de medicamentoscli_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_contactsse 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