# 07 — Integraciones externas

## 7.1 API Academusoft (sistema académico institucional)

**Cliente**: `app/models/SincronizacionModel.php`
**Base URL**: `https://api-academusoft-web.ucompensar.edu.co:8093/integrador-rest/servicios/`
**Método**: siempre `POST`, JSON.

### Autenticación

```
POST app-acceso/acceso
Header: WWW-Authenticate: Basic <credenciales del integrador>
Body:   { "codigoAplicacion": "EVADOCENTEKLEE" }
→ { "token": "..." }
```

El token se pide **en cada llamada** (no se cachea). Las llamadas de datos lo mandan como
`Authorization: Bearer <token>`.

### Endpoints consumidos

| Método PHP | Endpoint | Parámetros | Alimenta |
|---|---|---|---|
| `getEstudiantes($periodo)` | `app-integrador/aplicacion/Estudiantes` | `periodo1`, `periodo2`, `periodo3` | tabla `estudiantes` |
| `getDocentes($periodo)` | `app-integrador/aplicacion/Docentes` | `periodo` | tabla `docentes` |
| `getEstudiantesAsignaturas($periodo)` | `app-integrador/aplicacion/AsignaturasEstudiante` | `periodo` | `estudiantes_asignaturas` |
| `getDocentesAsignaturas($periodo)` | `app-integrador/aplicacion/AsignaturasDocente` | `periodo1`, `periodo2` | `docentes_asignaturas` |

Todos envían `"tipoRetorno": "DataObject"` y devuelven la carga útil en `data`.

Campos que se leen de la respuesta de estudiantes (nombres en minúscula, tal cual vienen):
`identificacion`, `usuario`, `nombre`, `apellido`, `email`, `programa`, `codprograma`,
`sede`, `metodologia`.

### Comportamiento y limitaciones

- `CURLOPT_TIMEOUT = 30` s; no hay reintentos ni backoff. Un timeout aborta el paso completo.
- Los errores se devuelven como `array('error' => "...")` — **quien llama debe comprobarlo**;
  varias rutas asumen que el retorno es una lista y fallarían si llega un string de error.
- El periodo se pasa como **nombre** (`"20242"`), no como Id.
- La sede y la metodología se mapean **por texto exacto** (`"ESCUELA DE FORMACIÓN EMPRESARIAL"`,
  `"PRESENCIAL"`, `"VIRTUAL"`). Un cambio de literal en origen rompe la sincronización en silencio
  (se registra en la consola como "Error En Sede/Metodologia").

## 7.2 Ollama (LLM self-hosted)

**Cliente**: `app/services/OllamaService.php`
**Config**: `ConfigEnv::$API_CONFIG['ollama']` → `url` (`http://<host>:11434/api/`) y `token`.

```php
OllamaService::generate($prompt, $model = "llama3.3:70b");
// POST {url}generate
// Header: Authorization: Bearer <token>
// Body:   { model, prompt, stream: false, think: false }
// → data['response']
```

- Modelos en uso: `llama3.3:70b` (clasificación de comentarios) y `qwen3.6:27b` (resúmenes).
- No hay timeout explícito en cURL: una petición puede colgarse. Si tarda más de 600 s se deja
  constancia en la tabla `debug`.
- Lanza `Exception` con el código HTTP o el errno de cURL; los llamadores usan ese código para
  decidir si reintentan (`503` → esperar) o abortan.
- El servidor apunta a una IP pública por HTTP plano y el token va en cabecera: el tráfico de
  comentarios de estudiantes viaja **sin cifrar**. Merece HTTPS o túnel.
- Queda código muerto de la integración anterior con OpenAI (`sgraaf/chatgpt-php` en
  `composer.json`, constante `OPENIA_API_KEY` y llamadas comentadas).

## 7.3 Microsoft Entra ID (SSO)

**Dónde**: `PublicController::validatedAction()`
**Tenant**: `4bf38ea2-832d-4552-b508-421570da43ff`
**Config**: `Config::$microsoft_login` (`client_id`, `client_secret`, `redirect_uri`).

Flujo OAuth2 *authorization code*:
1. Sin `?code` → redirige a `login.microsoftonline.com/<tenant>/oauth2/v2.0/authorize` con
   `scope = openid profile offline_access User.Read Mail.Read`.
2. Con `?code` → intercambia por token en `/oauth2/v2.0/token`.
3. Llama a `https://graph.microsoft.com/v1.0/me` y toma `userPrincipalName`.
4. Autentica sin contraseña, primero contra las tablas del portal y luego contra `usuarios`.

Notas: el `redirect_uri` está fijado a producción, así que el SSO **no funciona en local**
salvo que se registre otra URI de respuesta; no se validan `state` ni `nonce` (riesgo de CSRF en
el callback); no se comprueba `id_token`, se confía en la respuesta de Graph.

## 7.4 Correo (PHPMailer)

Configurado en `Config::$email_send`: `EmailHost`, `EmailPuerto`, `EmailUsuario`,
`EmailContrasena`, `EmailEncripcion`, `EmailNombre`, `EmailEncabezado`, `EmailPie`.
El valor por defecto en `Config.php` apunta a **Mailtrap (sandbox)**; producción debe
sobrescribirlo en `ConfigEnv.php`.

Uso: `AlertasEmailController::send()` / `sendCron()` — ver [05.6](05-flujos-clave.md#56-notificaciones-por-correo).

## 7.5 mPDF

`ArchivosController` usa `mpdf/mpdf` para:
- `exportPDF`: convierte HTML enviado por POST (con `app/views/archivos/pdf.css`).
- `generarCarta`: cartas oficiales de escalafón.
- Los `imprimir` de ODS y requisiciones usan el layout `impresiones`.

## 7.6 ipinfo.io

`LogAccionesModel::saveAccess()` consulta `https://ipinfo.io/{ip}/json` con timeout de conexión de
**0,5 s** para geolocalizar el acceso (`Country`, `ZipCode`, `Location`). Es *best-effort*: si falla,
los campos quedan vacíos. Implica que **cada primer acceso hace una llamada saliente** con la IP
del visitante a un tercero — relevante para el análisis de datos personales.

## 7.7 Assets front (CDN)

- Metronic 8.0.23 servido localmente desde `public/metronic_v8.0.23_html_demo1/` (**no versionado**: hay que copiarlo al desplegar).
- Google Fonts (Poppins), traducción de DataTables desde `cdn.datatables.net`, y
  `cdn.sheetjs.com` para exportar XLSX en algunos reportes. Requieren salida a Internet desde el navegador del usuario.

## 7.8 Integración de labores externa — ⚠️ rota / incompleta

`LaborDocenteController::importLaboresAction()` (acción **pública**, `'*'`) pretende importar
labores desde un servicio externo que devolvería JSON con campos `Id`, `Definicion`, `Acciones`,
`HorasSemana`, `Semanas`, `Impacto`, usando `labor_docente.IdImportado` como clave de *upsert*.

Estado real del código, verificado:

- **`LaborDocenteModel::importLabores()` no existe.** El controlador la invoca en dos sitios
  (líneas ~168 y ~226) y provocaría un `Error` fatal, capturado por el `try/catch` de
  `core/AutoLoad.php` (que lo imprime en pantalla).
- **`IdImportado` no está declarado** entre los atributos de `LaborDocenteModel`, así que
  `setIdImportado()` no tiene efecto y el campo no se persiste.

Conclusión: la acción es **código muerto o a medio migrar**. Antes de usarla hay que implementar
el cliente y declarar el atributo. Mientras tanto, conviene al menos cerrarla
(`'importLabores' => '@'` o quitarla del `AccessControl`), porque hoy es alcanzable sin sesión.

El campo `labor_docente.IdPanda` apunta a otra integración ("Panda") de la que no queda cliente
en el repositorio.
