Saltar a contenido

Modelo de Datos: Usuarios

Identificador: MDL-USR-001 Version: 1.0.0 Fecha: 2025-12-07 Estado: Aprobado Autor: DatabaseDrone / SpecQueen Technical Division Modulo Funcional: MTS-USR-001



1. Introduccion

Este documento define el modelo de datos para el modulo de usuarios de MedTime. Incluye perfiles, dependientes, cuidadores, contactos de emergencia y preferencias. Todos los datos de usuario son SYNCED_E2E, excepto preferencias que son LOCAL_ONLY.

1.1. Principios

Principio Aplicacion
PHI Cifrado Todos los datos medicos cifrados E2E
PII Protegido Datos personales con blind index
Perfiles Anidados PD tiene perfil dentro de cuenta CR
Preferencias Locales Configuracion de UI nunca sincroniza

1.2. Clasificacion de Datos

Entidad Clasificacion Cifrado Sync
cli_user SYNCED_E2E Si Si
cli_medical_profile SYNCED_E2E Si Si
cli_emergency_contact SYNCED_E2E Si Si
cli_dependents SYNCED_E2E Si Si
cli_caregiver_perms SYNCED_E2E Si Si
cli_preferences LOCAL_ONLY Si No

2. Entidades del Cliente

2.1. cli_user

Perfil principal del usuario.

// iOS - Swift/Realm
class User: Object {
    @Persisted(primaryKey: true) var userId: String = UUID().uuidString

    // Identidad (Cifrada E2E, blind index en servidor)
    @Persisted var email: String?
    @Persisted var phone: String?
    @Persisted var displayName: String

    // Clasificacion
    @Persisted var role: String              // PI, CR, CS
    @Persisted var tier: String              // free, pro, perfect

    // Perfil Basico
    @Persisted var photoUrl: String?
    @Persisted var locale: String = "es-MX"  // es-MX, en-US, pt-BR
    @Persisted var timezone: String          // America/Mexico_City

    // Estado de Cuenta
    @Persisted var isActive: Bool = true
    @Persisted var emailVerified: Bool = false
    @Persisted var phoneVerified: Bool = false
    @Persisted var mfaEnabled: Bool = false

    // Onboarding
    @Persisted var onboardingCompleted: Bool = false
    @Persisted var termsAcceptedAt: Date?
    @Persisted var privacyAcceptedAt: Date?

    // Sync
    @Persisted var createdAt: Date = Date()
    @Persisted var updatedAt: Date = Date()
    @Persisted var syncVersion: Int64 = 0

    // Relationships
    @Persisted var medicalProfile: MedicalProfile?
    @Persisted var emergencyContacts: List<EmergencyContact>
    @Persisted var dependents: List<Dependent>
    @Persisted var caregiverPermissions: List<CaregiverPermission>
    @Persisted var preferences: Preferences?
}
// Android - Kotlin/Room
@Entity(tableName = "cli_users")
data class User(
    @PrimaryKey
    val userId: String = UUID.randomUUID().toString(),

    // Identity
    val email: String?,
    val phone: String?,
    val displayName: String,

    // Classification
    val role: String,       // PI, CR, CS
    val tier: String,       // free, pro, perfect

    // Profile
    val photoUrl: String?,
    val locale: String = "es-MX",
    val timezone: String,

    // Account Status
    val isActive: Boolean = true,
    val emailVerified: Boolean = false,
    val phoneVerified: Boolean = false,
    val mfaEnabled: Boolean = false,

    // Onboarding
    val onboardingCompleted: Boolean = false,
    val termsAcceptedAt: Long?,
    val privacyAcceptedAt: Long?,

    // Sync
    val createdAt: Long = System.currentTimeMillis(),
    val updatedAt: Long = System.currentTimeMillis(),
    val syncVersion: Long = 0
)

2.1.1. Campos PHI/PII

Campo Tipo Nivel Tratamiento
email PII Medio Blind index en servidor
phone PII Medio Blind index en servidor
displayName PII Bajo Cifrado E2E
photoUrl PII Bajo Referencia a imagen cifrada

2.1.2. Roles y Tiers

Rol Descripcion Tiers Permitidos
PI Paciente Independiente free, pro, perfect
CR Cuidador Responsable free, pro, perfect
CS Cuidador Solidario free unicamente

2.2. cli_medical_profile

Perfil medico del usuario (1:1 con User).

// iOS - Swift/Realm
class MedicalProfile: Object {
    @Persisted(primaryKey: true) var profileId: String = UUID().uuidString

    // Foreign Key
    @Persisted(originProperty: "medicalProfile") var user: LinkingObjects<User>

    // Datos Basicos
    @Persisted var birthDate: Date?
    @Persisted var biologicalSex: String?    // male, female, intersex
    @Persisted var genderIdentity: String?   // man, woman, non-binary, other

    // Medidas
    @Persisted var heightCm: Double?
    @Persisted var weightKg: Double?
    @Persisted var bloodType: String?        // A+, A-, B+, B-, AB+, AB-, O+, O-

    // Condiciones Medicas
    @Persisted var allergies: List<Allergy>
    @Persisted var conditions: List<Condition>
    @Persisted var surgeries: List<Surgery>

    // Estilo de Vida
    @Persisted var smokingStatus: String?    // never, former, current
    @Persisted var alcoholUse: String?       // none, occasional, moderate, heavy
    @Persisted var exerciseFrequency: String? // none, light, moderate, active

    // Notas
    @Persisted var notes: String?

    // Sync
    @Persisted var updatedAt: Date = Date()
    @Persisted var syncVersion: Int64 = 0
}

// Embedded Objects for Medical Data
class Allergy: EmbeddedObject {
    @Persisted var name: String
    @Persisted var severity: String          // mild, moderate, severe
    @Persisted var reaction: String?
    @Persisted var diagnosedAt: Date?
}

class Condition: EmbeddedObject {
    @Persisted var name: String
    @Persisted var icd10Code: String?
    @Persisted var status: String            // active, resolved, chronic
    @Persisted var diagnosedAt: Date?
    @Persisted var treatingDoctor: String?
}

class Surgery: EmbeddedObject {
    @Persisted var name: String
    @Persisted var performedAt: Date?
    @Persisted var hospital: String?
    @Persisted var surgeon: String?
    @Persisted var notes: String?
}

2.2.1. Campos PHI (Todos Cifrados E2E)

Campo Nivel PHI Justificacion
birthDate Alto Identificador demografico
biologicalSex Alto Dato medico sensible
bloodType Alto Dato medico critico
allergies Critico Puede revelar condiciones
conditions Critico Diagnosticos medicos
surgeries Alto Historial medico

2.3. cli_emergency_contact

Contactos de emergencia.

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

    // Foreign Key
    @Persisted(originProperty: "emergencyContacts") var user: LinkingObjects<User>

    // Contacto
    @Persisted var name: String
    @Persisted var phone: String
    @Persisted var email: String?

    // Relacion
    @Persisted var relationship: String      // spouse, parent, child, sibling, friend, doctor
    @Persisted var isPrimary: Bool = false

    // Prioridad (1 = mas alta)
    @Persisted var priority: Int = 1

    // Permisos de Notificacion
    @Persisted var notifyOnMissedDose: Bool = false
    @Persisted var notifyOnEmergency: Bool = true
    @Persisted var notifyOnLowStock: Bool = false

    // Sync
    @Persisted var createdAt: Date = Date()
    @Persisted var updatedAt: Date = Date()
    @Persisted var syncVersion: Int64 = 0
}

2.3.1. Campos PII

Campo Nivel Tratamiento
name PII Medio Cifrado E2E
phone PII Alto Cifrado E2E
email PII Medio Cifrado E2E
relationship PII Bajo Cifrado E2E

2.4. cli_dependents

Pacientes dependientes (gestionados por CR).

// iOS - Swift/Realm
class Dependent: Object {
    @Persisted(primaryKey: true) var dependentId: String = UUID().uuidString

    // Foreign Key - Guardian (CR)
    @Persisted(originProperty: "dependents") var guardian: LinkingObjects<User>

    // Datos Basicos
    @Persisted var name: String
    @Persisted var nickname: String?
    @Persisted var birthDate: Date
    @Persisted var photoUrl: String?

    // Relacion
    @Persisted var relationship: String      // child, parent, spouse, sibling, ward

    // Perfil Medico (embebido, no relacion separada)
    @Persisted var medicalProfile: DependentMedicalProfile?

    // Acceso (para PD > 13 anos)
    @Persisted var hasOwnAccess: Bool = false
    @Persisted var accessPin: String?        // PIN hasheado si tiene acceso

    // Estado
    @Persisted var isActive: Bool = true

    // Sync
    @Persisted var createdAt: Date = Date()
    @Persisted var updatedAt: Date = Date()
    @Persisted var syncVersion: Int64 = 0

    // Computed
    var age: Int {
        Calendar.current.dateComponents([.year], from: birthDate, to: Date()).year ?? 0
    }

    var canHaveOwnAccess: Bool {
        age >= 13
    }
}

// Perfil Medico de Dependiente (embebido)
class DependentMedicalProfile: EmbeddedObject {
    @Persisted var biologicalSex: String?
    @Persisted var heightCm: Double?
    @Persisted var weightKg: Double?
    @Persisted var bloodType: String?
    @Persisted var allergies: List<Allergy>
    @Persisted var conditions: List<Condition>
    @Persisted var notes: String?
}

2.4.1. Reglas de Negocio

Regla Descripcion
RN-USR-001 Solo CR puede crear dependientes
RN-USR-002 Limite de dependientes segun tier: Free=1, Pro=5, Perfect=10
RN-USR-003 PD >= 13 anos puede tener acceso de lectura
RN-USR-004 PD >= 18 anos debe migrar a cuenta PI

2.5. cli_caregiver_perms

Permisos granulares para cuidadores solidarios.

// iOS - Swift/Realm
class CaregiverPermission: Object {
    @Persisted(primaryKey: true) var permissionId: String = UUID().uuidString

    // Relacion Paciente-Cuidador
    @Persisted var patientUserId: String     // PI que otorga permiso
    @Persisted var caregiverUserId: String   // CS que recibe permiso

    // Permiso Especifico
    @Persisted var permissionType: String    // ver tipo abajo

    // Estado
    @Persisted var isGranted: Bool = true

    // Auditoria
    @Persisted var grantedAt: Date = Date()
    @Persisted var grantedBy: String         // user_id de quien otorgo
    @Persisted var revokedAt: Date?
    @Persisted var revokedBy: String?

    // Sync
    @Persisted var syncVersion: Int64 = 0
}

2.5.1. Tipos de Permiso

Tipo Descripcion Default
view_medications Ver lista de medicamentos Si
view_adherence Ver estadisticas de tomas Si
confirm_doses Confirmar toma en nombre del PI No
receive_missed_alerts Recibir alerta de dosis omitida Si
view_prescriptions Ver recetas digitalizadas No
view_appointments Ver agenda de citas No
view_lab_results Ver resultados de laboratorio No
view_medical_profile Ver perfil medico No

2.6. cli_preferences

Preferencias locales (nunca sincroniza).

// iOS - Swift/Realm
class Preferences: Object {
    @Persisted(primaryKey: true) var preferenceId: String = "preferences"

    // Foreign Key
    @Persisted(originProperty: "preferences") var user: LinkingObjects<User>

    // Apariencia
    @Persisted var theme: String = "system"  // light, dark, system
    @Persisted var fontSize: String = "medium" // small, medium, large
    @Persisted var highContrast: Bool = false

    // Notificaciones
    @Persisted var notificationsEnabled: Bool = true
    @Persisted var reminderSound: String = "default"
    @Persisted var reminderVibrate: Bool = true
    @Persisted var quietHoursEnabled: Bool = false
    @Persisted var quietHoursStart: String = "22:00"
    @Persisted var quietHoursEnd: String = "07:00"

    // Idioma
    @Persisted var language: String = "es"   // es, en, pt

    // Seguridad Local
    @Persisted var biometricEnabled: Bool = false
    @Persisted var pinHash: String?          // Argon2 hash
    @Persisted var autoLockMinutes: Int = 5
    @Persisted var requireAuthOnOpen: Bool = true

    // Datos
    @Persisted var syncOnCellular: Bool = false
    @Persisted var lowDataMode: Bool = false
    @Persisted var cacheImages: Bool = true

    // Privacidad
    @Persisted var shareAnonymousData: Bool = true  // Para mejora de catalogos
    @Persisted var showMedicationNames: Bool = true // En widgets/notificaciones

    // Actualizacion
    @Persisted var updatedAt: Date = Date()
}

2.6.1. Clasificacion: LOCAL_ONLY

Caracteristica Valor
Sincronizacion Nunca
Cifrado En reposo (Keychain/Keystore)
Backup Incluido en backup local
Multi-dispositivo Cada dispositivo tiene sus propias preferencias

3. Diagrama ER del Modulo

erDiagram
    cli_user ||--|| cli_medical_profile : "has"
    cli_user ||--o{ cli_emergency_contact : "has"
    cli_user ||--o{ cli_dependents : "manages"
    cli_user ||--o{ cli_caregiver_perms : "as_patient"
    cli_user ||--o{ cli_caregiver_perms : "as_caregiver"
    cli_user ||--|| cli_preferences : "has"
    cli_dependents ||--|| cli_dependent_medical_profile : "has"

    cli_user {
        uuid userId PK
        string email
        string phone
        string displayName
        string role
        string tier
        boolean isActive
    }

    cli_medical_profile {
        uuid profileId PK
        uuid userId FK
        date birthDate
        string bloodType
        json allergies
        json conditions
    }

    cli_emergency_contact {
        uuid contactId PK
        uuid userId FK
        string name
        string phone
        string relationship
        int priority
    }

    cli_dependents {
        uuid dependentId PK
        uuid guardianId FK
        string name
        date birthDate
        string relationship
        boolean hasOwnAccess
    }

    cli_caregiver_perms {
        uuid permissionId PK
        uuid patientUserId FK
        uuid caregiverUserId FK
        string permissionType
        boolean isGranted
    }

    cli_preferences {
        uuid preferenceId PK
        uuid userId FK
        string theme
        boolean biometricEnabled
        string language
    }

4. Funciones de Validacion

4.1. Validar Limite de Dependientes

// iOS
func canAddDependent(for user: User) -> Bool {
    guard user.role == "CR" else { return false }

    let currentCount = user.dependents.filter { $0.isActive }.count
    let limit: Int

    switch user.tier {
    case "free": limit = 1
    case "pro": limit = 5
    case "perfect": limit = 10
    default: limit = 0
    }

    return currentCount < limit
}

4.2. Validar Edad para Acceso PD

// iOS
func canGrantAccessToDependent(_ dependent: Dependent) -> Bool {
    let age = Calendar.current.dateComponents([.year], from: dependent.birthDate, to: Date()).year ?? 0
    return age >= 13
}

func mustMigrateToPI(_ dependent: Dependent) -> Bool {
    let age = Calendar.current.dateComponents([.year], from: dependent.birthDate, to: Date()).year ?? 0
    return age >= 18
}

4.3. Validar Permisos CS

// iOS
func hasPermission(caregiver: User, patient: User, permission: String) -> Bool {
    guard caregiver.role == "CS" else { return false }

    let perms = patient.caregiverPermissions.filter {
        $0.caregiverUserId == caregiver.userId &&
        $0.permissionType == permission &&
        $0.isGranted &&
        $0.revokedAt == nil
    }

    return !perms.isEmpty
}

5. Serializacion para Sync E2E

5.1. Estructura del Blob Cifrado

{
  "entity_type": "user_profile",
  "version": 1,
  "encrypted_at": "2025-12-07T12:00:00Z",
  "data": {
    "user": {
      "userId": "uuid",
      "email": "encrypted_base64",
      "phone": "encrypted_base64",
      "displayName": "encrypted_base64",
      "role": "PI",
      "tier": "pro"
    },
    "medicalProfile": {
      "birthDate": "encrypted_base64",
      "bloodType": "encrypted_base64",
      "allergies": "encrypted_base64_json_array",
      "conditions": "encrypted_base64_json_array"
    },
    "emergencyContacts": [
      {
        "contactId": "uuid",
        "name": "encrypted_base64",
        "phone": "encrypted_base64",
        "relationship": "encrypted_base64"
      }
    ]
  },
  "checksum": "sha256_of_plaintext"
}

5.2. Campos No Cifrados (Metadata)

Campo Razon
entity_type Routing de sync
version Versionado de schema
encrypted_at Timestamp de cifrado
checksum Verificacion de integridad

6. Migraciones

6.1. Esquema Cliente (SQLite/Realm)

-- SQLite Migration for Android Room
-- Version: 1

CREATE TABLE IF NOT EXISTS users (
    userId TEXT PRIMARY KEY NOT NULL,
    email TEXT,
    phone TEXT,
    displayName TEXT NOT NULL,
    role TEXT NOT NULL,
    tier TEXT NOT NULL,
    photoUrl TEXT,
    locale TEXT NOT NULL DEFAULT 'es-MX',
    timezone TEXT NOT NULL,
    isActive INTEGER NOT NULL DEFAULT 1,
    emailVerified INTEGER NOT NULL DEFAULT 0,
    phoneVerified INTEGER NOT NULL DEFAULT 0,
    mfaEnabled INTEGER NOT NULL DEFAULT 0,
    onboardingCompleted INTEGER NOT NULL DEFAULT 0,
    termsAcceptedAt INTEGER,
    privacyAcceptedAt INTEGER,
    createdAt INTEGER NOT NULL,
    updatedAt INTEGER NOT NULL,
    syncVersion INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE IF NOT EXISTS medical_profiles (
    profileId TEXT PRIMARY KEY NOT NULL,
    userId TEXT NOT NULL,
    birthDate INTEGER,
    biologicalSex TEXT,
    genderIdentity TEXT,
    heightCm REAL,
    weightKg REAL,
    bloodType TEXT,
    allergiesJson TEXT,
    conditionsJson TEXT,
    surgeriesJson TEXT,
    smokingStatus TEXT,
    alcoholUse TEXT,
    exerciseFrequency TEXT,
    notes TEXT,
    updatedAt INTEGER NOT NULL,
    syncVersion INTEGER NOT NULL DEFAULT 0,
    FOREIGN KEY (userId) REFERENCES users(userId) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS emergency_contacts (
    contactId TEXT PRIMARY KEY NOT NULL,
    userId TEXT NOT NULL,
    name TEXT NOT NULL,
    phone TEXT NOT NULL,
    email TEXT,
    relationship TEXT NOT NULL,
    isPrimary INTEGER NOT NULL DEFAULT 0,
    priority INTEGER NOT NULL DEFAULT 1,
    notifyOnMissedDose INTEGER NOT NULL DEFAULT 0,
    notifyOnEmergency INTEGER NOT NULL DEFAULT 1,
    notifyOnLowStock INTEGER NOT NULL DEFAULT 0,
    createdAt INTEGER NOT NULL,
    updatedAt INTEGER NOT NULL,
    syncVersion INTEGER NOT NULL DEFAULT 0,
    FOREIGN KEY (userId) REFERENCES users(userId) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS dependents (
    dependentId TEXT PRIMARY KEY NOT NULL,
    guardianId TEXT NOT NULL,
    name TEXT NOT NULL,
    nickname TEXT,
    birthDate INTEGER NOT NULL,
    photoUrl TEXT,
    relationship TEXT NOT NULL,
    medicalProfileJson TEXT,
    hasOwnAccess INTEGER NOT NULL DEFAULT 0,
    accessPinHash TEXT,
    isActive INTEGER NOT NULL DEFAULT 1,
    createdAt INTEGER NOT NULL,
    updatedAt INTEGER NOT NULL,
    syncVersion INTEGER NOT NULL DEFAULT 0,
    FOREIGN KEY (guardianId) REFERENCES users(userId) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS caregiver_permissions (
    permissionId TEXT PRIMARY KEY NOT NULL,
    patientUserId TEXT NOT NULL,
    caregiverUserId TEXT NOT NULL,
    permissionType TEXT NOT NULL,
    isGranted INTEGER NOT NULL DEFAULT 1,
    grantedAt INTEGER NOT NULL,
    grantedBy TEXT NOT NULL,
    revokedAt INTEGER,
    revokedBy TEXT,
    syncVersion INTEGER NOT NULL DEFAULT 0,
    FOREIGN KEY (patientUserId) REFERENCES users(userId) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS preferences (
    preferenceId TEXT PRIMARY KEY NOT NULL DEFAULT 'preferences',
    userId TEXT NOT NULL,
    theme TEXT NOT NULL DEFAULT 'system',
    fontSize TEXT NOT NULL DEFAULT 'medium',
    highContrast INTEGER NOT NULL DEFAULT 0,
    notificationsEnabled INTEGER NOT NULL DEFAULT 1,
    reminderSound TEXT NOT NULL DEFAULT 'default',
    reminderVibrate INTEGER NOT NULL DEFAULT 1,
    quietHoursEnabled INTEGER NOT NULL DEFAULT 0,
    quietHoursStart TEXT NOT NULL DEFAULT '22:00',
    quietHoursEnd TEXT NOT NULL DEFAULT '07:00',
    language TEXT NOT NULL DEFAULT 'es',
    biometricEnabled INTEGER NOT NULL DEFAULT 0,
    pinHash TEXT,
    autoLockMinutes INTEGER NOT NULL DEFAULT 5,
    requireAuthOnOpen INTEGER NOT NULL DEFAULT 1,
    syncOnCellular INTEGER NOT NULL DEFAULT 0,
    lowDataMode INTEGER NOT NULL DEFAULT 0,
    cacheImages INTEGER NOT NULL DEFAULT 1,
    shareAnonymousData INTEGER NOT NULL DEFAULT 1,
    showMedicationNames INTEGER NOT NULL DEFAULT 1,
    updatedAt INTEGER NOT NULL,
    FOREIGN KEY (userId) REFERENCES users(userId) ON DELETE CASCADE
);

-- Indexes
CREATE INDEX idx_users_role_tier ON users(role, tier);
CREATE INDEX idx_medical_profiles_user ON medical_profiles(userId);
CREATE INDEX idx_emergency_contacts_user ON emergency_contacts(userId);
CREATE INDEX idx_dependents_guardian ON dependents(guardianId);
CREATE INDEX idx_caregiver_perms_patient ON caregiver_permissions(patientUserId);
CREATE INDEX idx_caregiver_perms_caregiver ON caregiver_permissions(caregiverUserId);

7. Procedimiento de Migracion: PD a PI (DV2-P3)

7.1. Contexto: DB-BAJO-003

Problema: Cuando un Paciente Dependiente (PD) cumple 18 anos, DEBE migrar a cuenta independiente (Paciente Independiente - PI). El proceso debe ser:

  • Seguro (preservar todos los datos medicos)
  • Completo (migrar medicamentos, logs, recetas, etc.)
  • Reversible (en caso de error)
  • Auditable (registro completo de migracion)

7.2. Regla de Negocio: RN-USR-004

Campo Valor
ID RN-USR-004
Descripcion PD >= 18 anos debe migrar a cuenta PI
Trigger Cumpleanos 18 (batch job diario) o accion manual CR
Estado previo PD tiene perfil dentro de cuenta CR
Estado post PD se convierte en PI con cuenta propia

7.3. Proceso de Migracion

7.3.1. Pre-requisitos

-- Verificar que PD es elegible para migracion
SELECT
    d.dependent_id,
    d.name,
    d.birth_date,
    EXTRACT(YEAR FROM AGE(d.birth_date)) as age,
    d.guardian_id
FROM cli_dependents d
WHERE EXTRACT(YEAR FROM AGE(d.birth_date)) >= 18
  AND d.is_active = TRUE
  AND d.has_own_access = FALSE;  -- Aun no tiene cuenta propia

7.3.2. Pasos de Migracion (Cliente)

IMPORTANTE: La migracion se ejecuta EN EL CLIENTE, no en servidor. El servidor solo ve blobs cifrados.

// iOS - Swift
class DependentToIndependentMigration {

    func migrate(dependent: Dependent, guardian: User) async throws -> User {
        let realm = try await Realm()

        try await realm.write {
            // 1. Crear nueva cuenta PI
            let newUser = User()
            newUser.userId = UUID().uuidString
            newUser.email = nil  // Usuario debera configurar
            newUser.phone = nil  // Usuario debera configurar
            newUser.displayName = dependent.name
            newUser.role = "PI"  // Paciente Independiente
            newUser.tier = guardian.tier  // Hereda tier de CR (por 30 dias)
            newUser.locale = guardian.locale
            newUser.timezone = guardian.timezone

            // 2. Migrar perfil medico
            if let depProfile = dependent.medicalProfile {
                let newProfile = MedicalProfile()
                newProfile.profileId = UUID().uuidString
                newProfile.birthDate = depProfile.birthDate
                newProfile.biologicalSex = depProfile.biologicalSex
                newProfile.heightCm = depProfile.heightCm
                newProfile.weightKg = depProfile.weightKg
                newProfile.bloodType = depProfile.bloodType
                newProfile.allergies = depProfile.allergies
                newProfile.conditions = depProfile.conditions
                newProfile.notes = depProfile.notes
                newUser.medicalProfile = newProfile
            }

            // 3. Migrar medicamentos
            let medications = realm.objects(Medication.self)
                .filter("userId == %@", guardian.userId)
                .filter("profileId == %@", dependent.dependentId)

            for oldMed in medications {
                let newMed = Medication()
                newMed.medicationId = UUID().uuidString
                newMed.userId = newUser.userId
                newMed.profileId = nil  // Ya no es dependiente
                newMed.customName = oldMed.customName
                newMed.dosage = oldMed.dosage
                newMed.unit = oldMed.unit
                newMed.form = oldMed.form
                newMed.route = oldMed.route
                newMed.instructions = oldMed.instructions
                newMed.isActive = oldMed.isActive
                newMed.startDate = oldMed.startDate
                newMed.endDate = oldMed.endDate

                // Migrar schedules
                for oldSchedule in oldMed.schedules {
                    let newSchedule = Schedule()
                    newSchedule.scheduleId = UUID().uuidString
                    newSchedule.frequencyType = oldSchedule.frequencyType
                    newSchedule.timesPerDay = oldSchedule.timesPerDay
                    newSchedule.specificTimes = oldSchedule.specificTimes
                    newSchedule.daysOfWeek = oldSchedule.daysOfWeek
                    newSchedule.intervalHours = oldSchedule.intervalHours
                    newSchedule.startDate = oldSchedule.startDate
                    newSchedule.endDate = oldSchedule.endDate
                    newSchedule.isActive = oldSchedule.isActive
                    newMed.schedules.append(newSchedule)

                    // Migrar dose logs (ultimos 90 dias)
                    let ninetyDaysAgo = Date().addingTimeInterval(-90 * 24 * 3600)
                    let recentLogs = oldSchedule.doseLogs.filter { $0.scheduledTime >= ninetyDaysAgo }
                    for oldLog in recentLogs {
                        let newLog = DoseLog()
                        newLog.logId = UUID().uuidString
                        newLog.scheduledTime = oldLog.scheduledTime
                        newLog.actualTime = oldLog.actualTime
                        newLog.status = oldLog.status
                        newLog.skippedReason = oldLog.skippedReason
                        newLog.notes = oldLog.notes
                        newSchedule.doseLogs.append(newLog)
                    }
                }

                realm.add(newMed)
            }

            // 4. Migrar recetas (ultimas 2 anos)
            let twoYearsAgo = Date().addingTimeInterval(-2 * 365 * 24 * 3600)
            let prescriptions = realm.objects(Prescription.self)
                .filter("userId == %@", guardian.userId)
                .filter("profileId == %@", dependent.dependentId)
                .filter("prescriptionDate >= %@", twoYearsAgo)

            for oldRx in prescriptions {
                let newRx = Prescription()
                newRx.id = UUID().uuidString
                newRx.userId = newUser.userId
                newRx.profileId = nil
                newRx.prescriberId = oldRx.prescriberId
                newRx.prescriberName = oldRx.prescriberName
                newRx.institutionId = oldRx.institutionId
                newRx.institutionName = oldRx.institutionName
                newRx.prescriptionDate = oldRx.prescriptionDate
                newRx.diagnosis = oldRx.diagnosis
                newRx.notes = oldRx.notes

                // Migrar imagenes y OCR results
                for oldImg in oldRx.images {
                    let newImg = PrescriptionImage()
                    newImg.id = UUID().uuidString
                    newImg.userId = newUser.userId
                    newImg.originalFileName = oldImg.originalFileName
                    newImg.originalFileUrl = oldImg.originalFileUrl
                    newImg.originalFileHash = oldImg.originalFileHash
                    newRx.images.append(newImg)
                }

                realm.add(newRx)
            }

            // 5. Crear auditoria de migracion
            let audit = MigrationAudit()
            audit.auditId = UUID().uuidString
            audit.migrationType = "PD_TO_PI"
            audit.fromUserId = guardian.userId
            audit.fromDependentId = dependent.dependentId
            audit.toUserId = newUser.userId
            audit.migratedAt = Date()
            audit.medicationsCount = medications.count
            audit.prescriptionsCount = prescriptions.count
            realm.add(audit)

            // 6. Marcar dependiente como migrado
            dependent.isActive = false
            dependent.migratedToUserId = newUser.userId
            dependent.migratedAt = Date()

            realm.add(newUser)
        }

        return newUser
    }
}

7.3.3. Entidades Migradas

Entidad Se Migra Periodo Notas
cli_user ✅ Nuevo N/A Se crea cuenta PI nueva
cli_medical_profile ✅ Completo Todos Copia completa
cli_medications ✅ Completo Todos Solo activos + ultimos 2 anos
cli_schedules ✅ Completo Todos Schedules activos
cli_dose_logs ✅ Parcial Ultimos 90 dias Historial reciente
cli_prescriptions ✅ Parcial Ultimos 2 anos Recetas recientes
cli_prescription_images ✅ Completo Segun recetas Solo de recetas migradas
cli_emergency_contacts ❌ No N/A Usuario debera configurar nuevos
cli_preferences ❌ No N/A Usuario configura desde cero

7.3.4. Esquema de Auditoria

// Nuevo modelo para auditar migraciones
class MigrationAudit: Object {
    @Persisted(primaryKey: true) var auditId: String = UUID().uuidString

    @Persisted var migrationType: String  // PD_TO_PI, PI_TO_PD (reversa)

    // From
    @Persisted var fromUserId: String
    @Persisted var fromDependentId: String?

    // To
    @Persisted var toUserId: String
    @Persisted var toDependentId: String?

    // Estadisticas
    @Persisted var medicationsCount: Int = 0
    @Persisted var schedulesCount: Int = 0
    @Persisted var doseLogsCount: Int = 0
    @Persisted var prescriptionsCount: Int = 0

    // Metadata
    @Persisted var migratedAt: Date = Date()
    @Persisted var migratedBy: String  // user_id de quien ejecuto
    @Persisted var wasAutomatic: Bool = false  // true si fue batch job

    // Reversible
    @Persisted var isReversed: Bool = false
    @Persisted var reversedAt: Date?
    @Persisted var reversedBy: String?
}

7.4. Flujo de Usuario (UX)

7.4.1. Notificacion al CR (Cuidador Responsable)

TRIGGER: 30 dias antes del cumpleanos 18 del PD

MENSAJE:
"Tu paciente dependiente [Nombre] cumplira 18 anos el [Fecha].
Deberas ayudarle a crear su cuenta independiente.

Opciones:
1. Migrar ahora (adelantado)
2. Migrar en la fecha
3. Recordarme en 7 dias"

7.4.2. Wizard de Migracion

PASO 1: Confirmacion
- "¿Seguro que deseas convertir a [Nombre] en paciente independiente?"
- "[Nombre] podra gestionar sus propios medicamentos y datos medicos"

PASO 2: Configuracion de Cuenta
- Email del nuevo usuario PI
- Telefono (opcional)
- Contraseña temporal

PASO 3: Revision de Datos
- Mostrar resumen de datos a migrar:
  * N medicamentos activos
  * N recetas (ultimos 2 anos)
  * N registros de tomas (ultimos 90 dias)

PASO 4: Ejecucion
- Barra de progreso
- Migrando medicamentos...
- Migrando recetas...
- Creando cuenta...

PASO 5: Completado
- "¡Migracion exitosa!"
- "Hemos enviado instrucciones a [email] para que [Nombre] configure su cuenta"
- Codigo de activacion temporal: XXXX-XXXX

7.5. Rollback y Reversa

7.5.1. Rollback (en caso de error)

Si la migracion falla a mitad de proceso:

func rollbackMigration(audit: MigrationAudit) async throws {
    let realm = try await Realm()

    try await realm.write {
        // 1. Eliminar cuenta PI creada
        if let newUser = realm.object(ofType: User.self, forPrimaryKey: audit.toUserId) {
            realm.delete(newUser)
        }

        // 2. Reactivar dependiente
        if let dependent = realm.object(ofType: Dependent.self, forPrimaryKey: audit.fromDependentId) {
            dependent.isActive = true
            dependent.migratedToUserId = nil
            dependent.migratedAt = nil
        }

        // 3. Marcar audit como fallido
        audit.isReversed = true
        audit.reversedAt = Date()
        audit.reversedBy = "system_rollback"
    }
}

7.5.2. Reversa (PI vuelve a PD)

Si el usuario PI desea volver a ser PD (raro, pero posible):

  • Solo permitido en primeros 30 dias post-migracion
  • Requiere autorizacion del CR original
  • Proceso inverso al de migracion

7.6. Consideraciones de Sincronizacion

IMPORTANTE: La migracion genera dos eventos de sync independientes:

  1. Desactivacion de PD en cuenta CR → Sync E2E de cli_dependents
  2. Creacion de cuenta PI → Sync E2E de nuevo user + entidades

Estos eventos NO estan vinculados en el servidor (zero-knowledge). El servidor solo ve:

  • Blob cifrado de dependent con is_active=false
  • Blob cifrado de nuevo user PI (sin relacion visible con PD)

8. Referencias Cruzadas

Documento Relacion
DB-ERD-001 Diagrama ER completo
MDL-AUTH-001 Modelo de autenticacion
MTS-USR-001 Especificacion funcional
MTS-AUTH-001 Roles y tiers
MTS-PRI-001 Consentimientos
04-seguridad-cliente.md Cifrado E2E

Documento generado por DatabaseDrone - SpecQueen Technical Division "Cada perfil es una historia de salud que merece proteccion absoluta."