# ESQUEMA-ACTUAL.md — Estado real de la BD (migraciones as-built)

Refleja **lo que realmente crean las migraciones** de `tutor-backend` al
**2026-08-05**, no lo planificado en `tutortrack/docs/BD-BACKEND.md`. Ese doc
es la fuente de verdad del *diseño completo* (34 tablas, M1–M4); **este** doc
es la foto de lo *ya construido*.

> Estamos en **desarrollo**: cualquier cambio de esquema se aplica con
> `migrate:fresh --seed`. No se crean migraciones `alter`; se **edita la
> migración `create`** correspondiente y se re-siembra. Por eso este doc y las
> migraciones deben mantenerse en paralelo.

Convención del proyecto: el dominio se nombra en **español** (doc, UI) y se
**traduce a inglés** en tablas/modelos.

## Cobertura actual

| Módulo | Estado |
|--------|--------|
| Infraestructura (framework, Sanctum, RBAC, auditoría) | Construido |
| M1 — Identidad / catálogos / perfiles (docente, estudiante, receptor, apoderado) | Construido |
| M2 — Estructura académica (periodo, ciclo_periodo, temario, matrículas) | **Construido** (con `campus_id` en `cycle_periods`) |
| M3 — Fichas | **No construido** |
| M4 — Alertas IA / derivación | **No construido** |

---

## 1. Identidad y acceso

### `users` — persona / identidad (una fila por documento)
Eje de identidad. **No** guarda datos de rol; esos van en los perfiles.

| Columna | Tipo | Notas |
|---------|------|-------|
| `id` | bigint PK | |
| `document_type_id` | FK → `document_types` | |
| `document_number` | varchar(15) | |
| `first_names` | varchar(100) | |
| `paternal_surname` | varchar(60) | |
| `maternal_surname` | varchar(60) | |
| `email` | varchar(150) **unique** | correo de acceso |
| `personal_email` | varchar(150) null | |
| `password` | varchar (hash bcrypt, cast `hashed`) | |
| `profile_photo_path` | varchar(255) null | path en disco privado (local) |
| `sex` | char(1) null | `M` \| `F` \| `N` (valida el FormRequest) |
| `birth_date` | date null | |
| `primary_phone` / `secondary_phone` | varchar(20) null | |
| `active` | boolean default true | habilita/deshabilita el **acceso** |
| `academic_degree_id` | FK null → `academic_degrees` | grado a nivel **persona** (uno) |
| `remember_token`, `timestamps`, `deleted_at` | | soft delete |

**Únicos:** `email`; **compuesto `(document_type_id, document_number)`** — un
documento no se repite por tipo. Base del patrón *reutilizar-por-documento*.

### `document_types` — catálogo
`id` · `code` varchar(10) **unique** (DNI/CE/PAS) · `name` varchar(60)
**unique** · `active` · timestamps.

### Tablas de framework / auth (estándar Laravel)
- `password_reset_tokens`, `sessions` — creadas junto a `users`.
- `cache`, `cache_locks`, `jobs`, `job_batches`, `failed_jobs` — soporte.
- `personal_access_tokens` — **Sanctum** (tokens de API).

### RBAC — `spatie/laravel-permission` (estándar)
`roles`, `permissions`, `model_has_roles`, `model_has_permissions`,
`role_has_permissions`. La migración `add_columns_to_permission_tables` agrega
las columnas de `team`/guard según el paquete. Seeders: `PermissionSeeder`,
`RoleSeeder` (rol `admin` sembrado; `AdminUserSeeder` crea el admin
placeholder `administrador@unamba.edu.pe`).

### `audits` — auditoría (trazabilidad; reemplaza `estado_derivacion`)
`id` · `user_id` FK null (`nullOnDelete`, null = sistema) · `event`
varchar(30) · `auditable_type` varchar(100) (alias morph) · `auditable_id`
bigint null · `old_values`/`new_values` json · `ip_address` · `user_agent` ·
`url` · `created_at` (única marca, inmutable). Índices por
`(auditable_type, auditable_id)`, `user_id`, `created_at`.

---

## 2. Catálogos

| Tabla | Columnas clave | Únicos |
|-------|----------------|--------|
| `academic_degrees` | `name`(60), `abbreviation`(10), `sort_order` null, `active` | `name`, `abbreviation` |
| `specialties` | `name`(120), `active` | `name` |
| `user_specialties` | pivote N:N **persona↔especialidad** · `user_id` · `specialty_id` · `active` bool default true · timestamps | **unique `(user_id, specialty_id)`** |
| `cycles` | `name`(60), `sort_order` (progresión), `active` | `name`, `sort_order` |
| `form_types` | `code`(30), `name`(80), `description` null, `sort_order`, `active` | `code`, `name` |
| `areas` | `code`(40) (clave estable IA), `name`(120), `description`, `active` | `code`, `name` |
| `receiving_entities` | `code`(40), `name`(120), `description`, `active` | `code`, `name` |

---

## 2b. M2 — Organización académica

Traducción canónica: `periodo_academico` → `academic_periods`,
`ciclo_periodo` → `cycle_periods`, `docente_ciclo_periodo` →
`cycle_period_teachers`, `temario` → `syllabus_items`,
`estudiante_ciclo_periodo` → `enrollments`. **Sin soft deletes.**

### `academic_periods`
`id` · `name`(20) **unique** · `start_date`/`end_date` date null ·
`active` bool default false · timestamps. Solo uno vigente (regla de app
vía `PATCH .../activar`).

### `cycle_periods` — nodo central (ciclo × período × sede)
`id` · `cycle_id` FK → `cycles` · `academic_period_id` FK →
`academic_periods` · `campus_id` FK → `campuses` NOT NULL · timestamps ·
**unique `(cycle_id, academic_period_id, campus_id)`**.

### `cycle_period_teachers`
`id` · `teacher_id` FK → `teachers` · `cycle_period_id` FK →
`cycle_periods` · timestamps · **unique `(teacher_id, cycle_period_id)`**.

### `syllabus_items` — temario (árbol)
`id` · `cycle_period_id` FK · `parent_id` FK null → self · `topic`(255) ·
`sort_order` smallint · timestamps.

### `enrollments` — matrícula de tutoría
`id` · `student_id` FK → `students` · `cycle_period_id` FK ·
`teacher_id` FK → `teachers` · timestamps · **unique
`(student_id, cycle_period_id)`**. Regla app: una matrícula por período
académico (vía `cycle_period.academic_period_id`).

API (rutas español, JSON inglés): `/periodos-academicos`,
`/ciclos-periodos/{id}/docentes|temario|estudiantes`, clonar y avanzar.
Permisos: `periodos.ver` / `periodos.gestionar`. Filtrado operativo por
`X-Campus-Id` (fail-closed).

---

## 3. Perfiles (cuelgan de `users`; **varios por sede**)

Una persona (`users`) puede tener **varios perfiles del mismo tipo**, uno por
sede (o por entidad receptora). Ver §6.

### `teachers` — perfil docente por sede
`id` · `user_id` FK → `users` · `campus_id` FK → `campuses` · `active` bool
(activo en esa sede) ·
`orcid_code` varchar(19) null · `cv_url`(255) null · `biography` text null ·
timestamps · soft delete · **unique `(user_id, campus_id)`**.

Datos de persona (compartidos entre sedes):
- Grado académico: `users.academic_degree_id` (FK único, uno por persona).
- Especialidades: `user_specialties` (N:N con `active` por vínculo).

API create/update docentes: `academic_degree_id` y `specialty_ids: [{ id, active }, …]`
se aplican al `user`. Show/list: `academic_degree_id` / `academic_degree` /
`specialties` salen del user (mismo contrato).
### `students` — perfil estudiante por sede
`id` · `user_id` FK → `users` · `campus_id` FK → `campuses` ·
`university_code` varchar(20) **unique** (global) · `orcid_code`(19) null ·
`status` varchar(10) default `active`
(`active`|`graduated`|`withdrawn`, estado **académico** ≠ `users.active`) ·
timestamps · soft delete · índice en `status` · **unique `(user_id, campus_id)`**.

### `receivers` — perfil receptor por entidad (sede vía entidad)
`id` · `user_id` FK → `users` · `receiving_entity_id` FK →
`receiving_entities` · `active` bool · timestamps · soft delete ·
**unique `(user_id, receiving_entity_id)`**.

---

## 4. Apoderados

### `guardians` — apoderado (entidad propia, NO es `user`)
`id` · `document_type_id` FK · `document_number`(15) · `first_names`(100) ·
`paternal_surname`(60) · `maternal_surname`(60) · `primary_phone`/
`secondary_phone`(20) null · `email`(150) null · `occupation`(100) null ·
`address`(255) null · timestamps · soft delete · **unique
`(document_type_id, document_number)`**.

### `student_guardian` — pivote estudiante ↔ apoderado
`id` · `student_id` FK · `guardian_id` FK · `relationship`(30)
(padre|madre|abuelo|tutor_legal|otro) · `is_primary` boolean default false ·
timestamps · **unique `(student_id, guardian_id)`**.

---

## 5. Orden de seeders (fuente de verdad de datos base)

`DatabaseSeeder`:
`DocumentType → Permission → Role → Campus → AdminUser → AcademicDegree →
Specialty → Cycle → AcademicPeriod → FormType → Area → ReceivingEntity →
Teacher → Student → Receiver → Guardian`.

---

## 6. Multisede — APLICADO (perfil por sede)

Decisión 2026-08-05 (`DECISION-MULTISEDE.md`). Traducción: sede → `campus`.

Estado as-built tras rebanada M1:

1. **`campuses`**: `id` · `code`(10) unique · `name`(80) unique · `active` · timestamps.
2. **`campus_user`**: pivote admin; unique `(user_id, campus_id)`.
3. **`teachers`**: `campus_id` NOT NULL, `active` bool; unique `(user_id, campus_id)`.
4. **`students`**: `campus_id` NOT NULL; unique `(user_id, campus_id)`; `university_code` unique global.
5. **`receivers`**: `active` bool; unique `(user_id, receiving_entity_id)`; sede vía entidad.
6. **`receiving_entities`**: `campus_id` NOT NULL; unique `(campus_id, code)` y `(campus_id, name)`.
7. **`users`**: sin cambios (identidad global).
8. **API:** header `X-Campus-Id`; `/auth/me` → `allowed_campuses`; middleware `SetActiveCampus`.
   Endpoints operativos **fail-closed**: sin sede concreta (o `all`) → 422;
   `show`/`update`/`destroy` de perfiles/entidades verifican sede (404 cross-campus).
9. **Seeders:** `CampusSeeder` (ABA + SED2 PLACEHOLDER) antes de AdminUser; admin en ambas sedes.

M2 `cycle_periods.campus_id` **aplicado**. Pendiente: reportes "Todas las sedes".
