# STC_CORE_backend

## Proyecto
API REST del sistema STC. Gestiona autenticación, inscripciones, notificaciones por email, sesiones Zoom y administración de partners/roles.

## Stack
- Node.js + Express (ES modules)
- PostgreSQL 9.6 (via `pg` pool)
- JWT (jsonwebtoken)
- Nodemailer (Gmail Workspace SMTP)
- Desplegado en o2switch con Passenger

## Estructura
```
src/
  controllers/   authController.js
  database/      pool.js — conexión PostgreSQL con ruta absoluta (requerido por Passenger)
  middleware/
    authMiddleware.js  — verifica JWT, adjunta user al request
    roles.js           — middlewares: AUTH, PENDING, TUTOR, ADMIN, STUDENT (COURSE_ADMIN en adminRoutes.js)
    validate.js
  routes/
    authRoutes.js      — login Google, validación partnerID, registro tutor, enroll dupla
    adminRoutes.js     — gestión de niveles, emails, partners, configuración de curso
    tutorRoutes.js     — listado de alumnos, verificación de alumno
    studentRoutes.js   — datos de matrícula del alumno
  services/
    emailService.js         — envío SMTP; lee credenciales de course_config (fallback a .env)
    notificationService.js  — dispara emails según event_type usando fn_prepare_notifications
    authDbService.js
database/
  schema/
    functions/   — una función por fichero, extraída con extract_schema.sh
    tables/      — una tabla por fichero
    views/       — una vista por fichero
    types/       — dominios y enums
    seed/        — datos base (courses, levels, registration_messages, email_templates, email_dispatch_rules)
  migrations/archive/  — migraciones históricas (solo referencia)
  extract_schema.sh    — extrae schema real de la BD (compatible PG 9.6: usa proisagg, no prokind)
```

## Middlewares de autorización
```js
AUTH         — cualquier JWT válido (STUDENT, TUTOR, PROFESSOR, SUPERADMIN)
PENDING      — AUTH + PENDING_REGISTRATION (usado en /register/tutor)
TUTOR        — TUTOR, PROFESSOR, SUPERADMIN
ADMIN        — PROFESSOR, SUPERADMIN
STUDENT      — STUDENT, TUTOR, PROFESSOR, SUPERADMIN
COURSE_ADMIN — SUPERADMIN o professor designado como course_admin del curso (:id); definido en adminRoutes.js; usa fn_check_course_access
```

## Endpoints principales

### Auth (`/api/auth`)
| Método | Ruta | Middleware | Descripción |
|--------|------|-----------|-------------|
| POST | `/google` | — | Login con Google, emite JWT |
| POST | `/validate-partner-id` | — | Valida PartnerID Zinzino |
| POST | `/register/tutor` | PENDING | Crea partner + tutor_profile, emite JWT TUTOR |
| POST | `/enroll/dupla` | TUTOR | Inscribe dupla tutor-alumno |
| POST | `/elevate` | AUTH | Eleva JWT a SUPERADMIN |

### Admin (`/api/admin`)
| Método | Ruta | Middleware | Descripción |
|--------|------|-----------|-------------|
| GET | `/courses` | ADMIN | Lista todos los cursos |
| POST | `/courses` | ADMIN | Crea curso |
| PATCH | `/courses/:id/toggle` | ADMIN | Toggle activo/inactivo del curso (SQL directo, sin función) |
| GET | `/courses/:id/config` | COURSE_ADMIN | Lee config del curso (Zoom, SMTP) |
| PUT | `/courses/:id/config` | COURSE_ADMIN | Guarda config del curso |
| GET | `/courses/:id/professors` | COURSE_ADMIN | Asignaciones profesor↔nivel |
| POST | `/courses/:id/professors` | COURSE_ADMIN | Añade asignación |
| PATCH | `/courses/:id/professors/:pid` | COURSE_ADMIN | Actualiza nivel de asignación existente |
| DELETE | `/courses/:id/professors/:pid` | COURSE_ADMIN | Elimina asignación |
| GET | `/courses/:id/admins` | COURSE_ADMIN | Lista course admins del curso |
| POST | `/courses/:id/admins` | ADMIN | Añade course admin |
| DELETE | `/courses/:id/admins/:adminId` | ADMIN | Elimina course admin |
| GET | `/levels` | AUTH | Listado de niveles con conteos |
| POST | `/levels` | ADMIN | Crear nivel |
| PUT | `/levels/:id` | ADMIN | Editar nivel |
| DELETE | `/levels/:id` | ADMIN | Toggle activo/inactivo |
| GET | `/email/rules` | ADMIN | Reglas de despacho de email |
| PATCH | `/email/rules/:id/toggle` | ADMIN | Toggle activo de regla |
| POST | `/email/rules` | ADMIN | Crea regla de despacho |
| DELETE | `/email/rules/:id` | ADMIN | Elimina regla |
| GET | `/email/templates` | ADMIN | Plantillas de email |
| PUT | `/email/templates/:key/:lang` | ADMIN | Editar plantilla |
| POST | `/email/templates` | ADMIN | Crea plantilla nueva |
| DELETE | `/email/templates/:key/:lang` | ADMIN | Elimina plantilla |
| POST | `/email/test` | ADMIN | Enviar email de prueba |
| GET | `/guests` | ADMIN | Listado de invitados (sin partner_id) |
| GET | `/partners/list?filter=` | ADMIN | Listado partners por rol |
| GET | `/partners/search?q=` | ADMIN | Búsqueda por ID o email |
| GET | `/partners/:id/detail` | ADMIN | Detalle completo del partner |
| POST | `/partners/:id/roles` | ADMIN | Asignar/revocar rol |
| GET | `/promotion-reasons` | ADMIN | Lista todos los motivos (activos + inactivos) |
| POST | `/promotion-reasons` | ADMIN | Crear motivo |
| PUT | `/promotion-reasons/:id` | ADMIN | Editar motivo |
| PATCH | `/promotion-reasons/:id/toggle` | ADMIN | Toggle activo/inactivo |
| GET | `/system-config/:key?lang=` | AUTH | Lee valor de config del sistema para un idioma |
| PATCH | `/system-config/:key` | AUTH | Actualiza config del sistema `{ value, lang }` (solo SUPERADMIN comprueba rol dentro) |

### Professor (`/api/professor`)
| Método | Ruta | Descripción |
|--------|------|-------------|
| GET | `/admin-courses` | Cursos que el profesor puede configurar (devuelve lista para AdminHub) |
| GET | `/sessions` | Lista todas las sesiones del curso (todos los profesores las ven) |
| POST | `/sessions` | Crea sesión; body: `title`, `scheduled_at`, `duration_min`, `course_id`, `participant_roles text[]`, `create_breakout_rooms bool` |
| PATCH | `/sessions/:id/cancel` | Cancela sesión; requiere `reason`; notifica participantes (SESSION_CANCELLED); registra en session_log |

### Tutor (`/api/tutor`)
| Método | Ruta | Descripción |
|--------|------|-------------|
| GET | `/students` | Alumnos del tutor con datos de contacto |
| POST | `/check-student` | Verifica alumno antes de inscribir |
| PATCH | `/students/:id/status` | Cambia estado del alumno |
| GET | `/promotion-reasons` | Lista motivos activos para cambio de nivel |
| PATCH | `/students/:id/promote` | Cambia nivel del alumno (escribe en lifecycle_log) |
| POST | `/guests/convert` | Convierte invitado a partner; dispara email GUEST_PROMOTED |

## Funciones PostgreSQL

### Auth / inscripción
| Función | Descripción |
|---------|-------------|
| `fn_validate_partner_id(bigint, email)` | Valida identidad del partner en Zinzino |
| `fn_register_tutor(jsonb)` | Inserta/actualiza partner + tutor_profile atómicamente |
| `fn_enroll_partner_system(jsonb)` | Motor de inscripción atómico; devuelve `status`, `msg_key`, `message` |
| `fn_get_user_session_context(bigint)` | Datos de sesión del usuario autenticado |

### Tutor
| Función | Descripción |
|---------|-------------|
| `fn_tutor_list_students(bigint, int)` | Alumnos del tutor (partners + guests UNION ALL) con datos de contacto |
| `fn_tutor_check_student(bigint, text, bigint)` | Verifica alumno antes de inscribir |
| `fn_tutor_update_student_status(bigint, text, bigint, integer, text)` | Cambia estado del alumno; valida cupo al activar vía fn_check_enrollment_quota; devuelve datos de contacto para notificación; escribe en lifecycle_log |
| `fn_tutor_list_promotion_reasons()` | Lista motivos de cambio de nivel activos |
| `fn_tutor_promote_student(bigint, bigint, int, int, int, text)` | Cambia nivel del alumno; escribe LEVEL_CHANGE en lifecycle_log |
| `fn_tutor_convert_guest(bigint, text, bigint, text)` | Convierte guest a partner: inserta en partners + student_profiles, borra de guests |

### Professor — sesiones
| Función | Descripción |
|---------|-------------|
| `fn_professor_list_admin_courses(bigint)` | Cursos que el profesor puede configurar (usado en `/professor/admin-courses`) |
| `fn_professor_list_sessions(bigint, int)` | Todas las sesiones del curso; devuelve `created_by_name` |
| `fn_professor_create_session(bigint, int, text, timestamptz, int, text[])` | Crea sesión filtrando participantes por `p_participant_roles` (STUDENT/TUTOR/PROFESSOR); guarda roles en `sessions.participant_roles`; registra CREATED en session_log |
| `fn_professor_save_session_zoom(int, text, text, jsonb)` | Actualiza sesión con datos Zoom e inserta participantes |
| `fn_professor_cancel_session(bigint, int, text)` | Cualquier profesor puede cancelar; guarda motivo; devuelve participantes + datos del curso para notificación; registra CANCELLED en session_log |

### Admin — guests
| Función | Descripción |
|---------|-------------|
| `fn_admin_list_guests()` | Lista invitados (sin partner_id) con datos del tutor |

### Admin — partners
| Función | Descripción |
|---------|-------------|
| `fn_admin_partner_detail(bigint)` | JSON completo del partner (roles, alumnos, niveles) |
| `fn_admin_list_partners(text)` | Lista partners filtrada por rol desde vw_partner_list |
| `fn_admin_search_partners(text)` | Búsqueda por ID o email |
| `fn_admin_toggle_partner_role(bigint, text, boolean)` | Activa/desactiva rol (TUTOR/PROFESSOR/SUPERADMIN) |
| `fn_admin_get_active_superadmin()` | Devuelve name+email del primer superadmin activo |

### Admin — niveles
| Función | Descripción |
|---------|-------------|
| `fn_admin_list_levels(int)` | Lista niveles con conteos |
| `fn_admin_upsert_level(jsonb)` | Crear o editar nivel |
| `fn_admin_toggle_level(int, int)` | Toggle activo/inactivo |

### Admin — cursos
| Función | Descripción |
|---------|-------------|
| `fn_admin_list_courses()` | Lista todos los cursos |
| `fn_admin_create_course(jsonb)` | Crea curso nuevo |
| `fn_check_course_access(bigint, int)` | Verifica acceso de un partner a un curso; devuelve `'superadmin'`, `'course_admin'`, `'professor'` o `null` |

### Admin — course admins
| Función | Descripción |
|---------|-------------|
| `fn_admin_list_course_admins(int)` | Lista professors con acceso de admin al curso |
| `fn_admin_add_course_admin(int, int)` | Concede acceso de course_admin a un profesor |
| `fn_admin_remove_course_admin(int, int)` | Revoca acceso de course_admin |

### Admin — course config
| Función | Descripción |
|---------|-------------|
| `fn_admin_get_course_config(int)` | Lee config del curso como jsonb |
| `fn_admin_set_course_config(int, jsonb)` | Upsert masivo de config (usa jsonb_each_text) |

### Admin — professor assignments
| Función | Descripción |
|---------|-------------|
| `fn_admin_list_professor_assignments(int)` | Lista asignaciones profesor↔nivel del curso |
| `fn_admin_add_professor_assignment(int, int, int)` | Añade asignación |
| `fn_admin_update_professor_assignment(int, int)` | Actualiza nivel de una asignación existente |
| `fn_admin_remove_professor_assignment(int, int)` | Elimina asignación |

### Admin — motivos de promoción
| Función | Descripción |
|---------|-------------|
| `fn_admin_list_promotion_reasons()` | Lista todos los motivos (activos + inactivos) |
| `fn_admin_upsert_promotion_reason(jsonb)` | Crear o editar motivo |
| `fn_admin_toggle_promotion_reason(int)` | Toggle activo/inactivo |

### Helpers compartidos
| Función | Descripción |
|---------|-------------|
| `fn_upsert_partner(bigint, valid_email, text×4, intl_phone, iso_country, text, text)` | Valida identidad y crea partner si no existe; devuelve NULL si ok o error_key si mismatch |
| `fn_check_enrollment_quota(tutor_id, level_id, course_id)` | Comprueba cupo del tutor en nivel/curso; devuelve 'ACTIVE' o 'PENDING' |

### Admin — email
| Función | Descripción |
|---------|-------------|
| `fn_admin_list_email_rules()` | Lista reglas de despacho |
| `fn_admin_toggle_email_rule(int)` | Toggle activo de regla |
| `fn_admin_create_email_rule(varchar×5)` | Crea regla de despacho |
| `fn_admin_delete_email_rule(int)` | Elimina regla |
| `fn_admin_list_email_templates()` | Lista plantillas |
| `fn_admin_update_email_template(text×5)` | Actualiza plantilla |
| `fn_admin_create_email_template(varchar×2, text×3)` | Crea plantilla nueva |
| `fn_admin_delete_email_template(varchar×2)` | Elimina plantilla (error si en uso) |
| `fn_prepare_notifications(text, jsonb)` | Prepara emails a despachar según event_type |
| `fn_substitute_template(text, jsonb)` | Sustituye placeholders `{{key}}` |

### Alumno
| Función | Descripción |
|---------|-------------|
| `fn_student_get_enrollment(bigint, int)` | Datos de matrícula del alumno |

## Vistas clave
- `vw_user_identity_roles` — partner con flags is_tutor/is_professor/is_superadmin/is_student
- `vw_partner_list` — extiende la anterior con student_count (partners + guests)
- `vw_student_management_master` — alumnos (partners + guests) con info del tutor

## Tablas de log
- `lifecycle_log` — cambios de estado y nivel de alumnos (LEVEL_CHANGE, STATUS_CHANGE)
- `session_log` — auditoría de sesiones Zoom: `action` (CREATED|CANCELLED), `performed_by`, `reason`, `created_at`
- `session_notifications_log` — emails disparados por sesión: `session_id`, `participant_email`, `event_type`, `sent_at`

## Tablas de configuración
- `system_config(config_key, language, config_value)` — configuración global del sistema por idioma; `fn_get_system_config(key, lang)` con fallback a `es`; `fn_set_system_config(key, lang, value)`

## Email

### Eventos de notificación
| event_type | Cuándo se dispara | Destinatario |
|-----------|-------------------|--------------|
| `TUTOR_REGISTERED` | Registro exitoso de tutor (`POST /api/auth/register/tutor`) | Tutor |
| `ENROLLMENT_ACTIVE` | Alumno inscrito y admitido | Alumno + Tutor |
| `ENROLLMENT_PENDING` | Cuota llena → lista de espera | Alumno + Tutor |
| `ENROLLMENT_REJECTED` | Inscripción no procesada | Tutor |
| `GUEST_PROMOTED` | Invitado promovido a Partner | Alumno |
| `CLASS_REMINDER` | Recordatorio de clase (Zoom) | Participantes con zoom_join_url |
| `SESSION_CANCELLED` | Sesión cancelada por el profesor | Todos los participantes |
| `STUDENT_CONFLICT` | Alumno ya vinculado a otro tutor | Superadmin |

### Contexto disponible por evento
- `TUTOR_REGISTERED`: `tutor_name`, `tutor_email`, `course_name`
- `ENROLLMENT_*`: `tutor_name`, `tutor_email`, `student_name`, `student_email`, `course_name`
- `STUDENT_CONFLICT`: ídem + `admin_email`, `admin_name`
- `CLASS_REMINDER`: `student_name`, `student_email`, `course_name`, `session_date`, `zoom_link`, `breakout_room`
- `SESSION_CANCELLED`: `student_name`, `student_email`, `course_name`, `session_date`, `reason`

### Helpers en notificationService.js
- `getCourseInfo(courseId)` — devuelve `{ name, language }` del curso
- `formatSessionDate(date, language)` — formatea fecha con locale correcto por idioma; fuente única de `LANG_LOCALE`
- `dispatchNotifications(eventType, context, lang)` — despacha emails según reglas activas; respeta `NOTIFY_DRY_RUN`

### Patrón fire-and-forget con log (professorRoutes.js)
```js
// dispatchAndLog — helper local en professorRoutes
dispatchAndLog(sessionId, email, eventType, context, lang)
// equivale a: dispatchNotifications(...).then(() => fn_log_session_notification(...)).catch(...)
```

### Otras notas
- Credenciales SMTP en `course_config` (tabla BD), fallback a `.env`
- `NOTIFY_DRY_RUN=true` en `.env` — loguea JSON a consola sin enviar emails reales (solo local)
- Todas las notificaciones son fire-and-forget — no bloquean la respuesta HTTP
- `session_notifications_log` registra cada despacho exitoso; evitar reenvíos consultando esta tabla

## Mensajes de inscripción (`registration_messages`)
| msg_key | Descripción |
|---------|-------------|
| `SUCCESS_ADMITTED` | Inscripción completada correctamente |
| `ERR_QUOTA_FULL` | Cuota llena → status PENDING |
| `ERR_FRAUD_STUDENT` | Alumno ya vinculado a otro tutor → STUDENT_CONFLICT email |
| `ERR_STUDENT_MISMATCH` | ID del alumno vinculado a otro email |
| `ERR_TUTOR_MISMATCH` | ID del tutor vinculado a otro email |
| `ERR_ALREADY_PARTNER` | Email ya registrado como partner |

## Despliegue (o2switch)
- Passenger gestiona el proceso Node
- `node_modules` es un symlink gestionado por CloudLinux — nunca hacer `rm -rf node_modules`, usar "Run NPM Install" desde cPanel
- Tiger Protect puede bloquear POST con 307 — desactivar en cPanel para `api.stc.stc-eu.com`
- Reiniciar desde cPanel tras cambios en backend

## Variables de entorno (.env)
```
NODE_ENV=production
JWT_SECRET=...
GOOGLE_CLIENT_ID=...
DATABASE_URL=...
MAIL_USER=...           # fallback si no hay SMTP en course_config
MAIL_APP_PASSWORD=...   # contraseña de aplicación Gmail Workspace
MAIL_FROM=...
ZOOM_ACCOUNT_ID=...
ZOOM_CLIENT_ID=...
ZOOM_CLIENT_SECRET=...
ZOOM_VERIFICATION_TOKEN=...
ZOOM_ENABLED=true       # false → modo prueba sin llamadas reales a Zoom
```

## Normas de trabajo
- **Código probado en local antes de desplegar — BD única sin entorno dev separado**
- **🔴 Backup completo con usuario `elfi1953` antes de cualquier cambio en BD** (DDL, destructivo o de datos): `PGPASSWORD='quGZ5!NZ(tKb' pg_dump -Fc -U elfi1953 -d elfi1953_stc-eu_db -f "database/backups/backup_$(date +%Y%m%d_%H%M%S).dump"`
- Conversación con el usuario en castellano; código, comentarios y commits en inglés
- Comentarios en código: documentar qué hace cada módulo, función y bloque lógico — no línea a línea; solo los comentarios necesarios para seguir el flujo
- Lógica de negocio en funciones PostgreSQL, no en JS
- Rutas JS como wrappers finos que llaman funciones de BD — sin SQL inline
- Evaluar reutilización antes de escribir código nuevo
- Tras crear tablas o funciones nuevas, conceder permisos al usuario de la app:
  ```sql
  GRANT SELECT, INSERT, UPDATE ON public.<tabla> TO "elfi1953_stc-eu";
  GRANT USAGE, SELECT ON SEQUENCE public.<tabla>_id_seq TO "elfi1953_stc-eu";
  GRANT EXECUTE ON FUNCTION public.<fn>(...) TO "elfi1953_stc-eu";
  ```
