# Documentación API Tresipunt Manager v1

## Información General

**Base URL**: `https://tu-dominio.com/api/v1`  
**Versión**: 1.0  
**Autenticación**: Bearer Token  
**Content-Type**: `application/json`

## Autenticación

Todas las peticiones requieren autenticación mediante Bearer Token en la cabecera `Authorization`.

```
Authorization: Bearer {tu_token_aqui}
```

### Validaciones del Token

El middleware valida:
- ✅ Existencia del token en la base de datos
- ✅ Token activo (`active = true`)
- ✅ Rango de fechas válido (`start_at <= ahora <= end_at`)
- ✅ Límite de peticiones (rate limiting)
- ✅ Host vinculado al token (excepto para acción `register`)

## Estructura de Respuesta Estándar

Todas las respuestas siguen el mismo formato:

### Respuesta Exitosa

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "nombre_accion",
  "data": {
    // Datos específicos de la acción
  }
}
```

### Respuesta de Error

```json
{
  "success": false,
  "error": "Mensaje de error legible",
  "code": 400,
  "action": "nombre_accion",
  "data": {}
}
```

## Códigos de Estado HTTP

- `200 OK`: Petición exitosa
- `400 Bad Request`: Error de validación o parámetros incorrectos
- `401 Unauthorized`: Token inválido o no autorizado
- `403 Forbidden`: Token inactivo, expirado o sin permisos
- `429 Too Many Requests`: Límite de peticiones excedido
- `500 Internal Server Error`: Error interno del servidor

## Códigos de Error Internos

| Código | Descripción |
|--------|-------------|
| 0 | Sin error (éxito) |
| 400 | Error de validación |
| 401 | Token no válido o no proporcionado |
| 403 | Token inactivo, expirado o sin permisos |
| 429 | Límite de peticiones excedido |
| 2001 | Licencia no incluye este plugin |
| 2002 | Entorno no encontrado o plugin no encontrado |
| 3000 | Producto no validado (SCSS/SCSS CDN/Setup/Features) |
| 3002 | No se encontró versión compatible o archivos (SCSS/SCSS CDN/Setup/Features) |
| 3003 | Formato de versión inválido o archivo inválido (SCSS/SCSS CDN/Setup/Features) |
| 4000 | Producto no validado (JS/Tutorials) |
| 4002 | No se encontró versión compatible o archivos (JS/Tutorials) |
| 4003 | Formato de versión inválido (JS/Tutorials) |
| 5000 | Producto no validado (Resources) |
| 5002 | No se encontró versión compatible de recursos (Resources) |
| 5003 | Formato de versión inválido (Resources) |
| 500 | Error interno del servidor |

## Endpoint Principal

### POST `/api/v1/`

Endpoint único que maneja todas las acciones mediante el parámetro `action` en el payload.

## Payload Base

Todos los requests deben incluir estos campos base:

```json
{
  "action": "nombre_accion",
  "plugin": "nombre_plugin",
  "host": "https://moodle.ejemplo.com",
  "version": "2025110401",
  "environment": {
    "version": "2025081101",
    "release": "4.5.1",
    "lastversion": "4.5",
    "lastminor": "4.5.3",
    "token": "token_moodle_opcional",
    "type": "moodle",
    "env": "pro"
  },
  // Nota: También se acepta "site" para compatibilidad con versiones anteriores
  "site": {
    "version": "2025081101",
    "release": "4.5.1",
    "lastversion": "4.5",
    "lastminor": "4.5.3",
    "token": "token_moodle_opcional",
    "type": "moodle",
    "env": "pro"
  },
  "projectid": "S3-3434",
  "data": {}
}
```

### Campos Obligatorios

| Campo | Tipo | Descripción | Ejemplo |
|-------|------|-------------|---------|
| `action` | string | Acción a ejecutar | `register`, `licence`, `products`, `scss`, `scss-cdn`, `js`, `setup`, `features`, `tutorials`, `resources`, `data`, `plugins` |
| `plugin` | string | Identificador del plugin | `theme_fresk`, `block_tresipuntsepe`, `local_tresipunt` |
| `host` | string (URL) | URL base del Moodle | `https://moodle.ejemplo.com` |
| `version` | string | Versión del plugin (YYYYMMDDXX) | `2025110401` |
| `environment.version` | string | Versión técnica de Moodle | `2025081101` |
| `environment.release` | string | Versión legible de Moodle | `4.5.1` |

### Campos Opcionales

| Campo | Tipo | Descripción | Valores Permitidos |
|-------|------|-------------|-------------------|
| `plugin` | string | Identificador del plugin | Opcional para `products`, requerido para otras acciones |
| `version` | string | Versión del plugin (YYYYMMDDXX) | Opcional para `products`, requerido para otras acciones |
| `environment.lastversion` | string | Última versión mayor conocida | - |
| `environment.lastminor` | string | Última versión minor conocida | - |
| `environment.token` | string | Token de Moodle para comunicación | - |
| `environment.type` | string | Tipo de plataforma | `moodle`, `workplace`, `goodle`, `wordpress`, `other` |
| `environment.env` | string | Entorno del entorno | `pro`, `pre`, `develop`, `local` |
| `projectid` | string | Identificador de proyecto | - |
| `data` | object | Datos específicos de la acción | Requerido para `data`, `plugins` |

---

## Acciones Disponibles

### 1. Sync

Actualiza los datos de un sitio existente en el Manager. **No crea nuevos sitios**, solo actualiza información de sitios que ya existen.

**Plugin**: Siempre `local_tresipunt`  
**Requiere `data`**: No (aunque puede estar vacío)  
**Requisito**: El sitio DEBE existir previamente en el Manager

#### Request

```json
{
  "action": "sync",
  "plugin": "local_tresipunt",
  "host": "https://moodle.ejemplo.com",
  "version": "2025110401",
  "site": {
    "version": "2025081101",
    "release": "4.5.1",
    "lastversion": "4.5",
    "lastminor": "4.5.3",
    "token": "token_moodle_secreto",
    "type": "moodle",
    "env": "pro"
  },
  "projectid": "S3-3434"
}
```

#### Response Exitosa (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "sync",
  "data": {
    "id": 1,
    "name": "moodle.ejemplo.com",
    "domain": "moodle.ejemplo.com",
    "version": "2025081101",
    "env": "pro",
    "type": "moodle",
    "lastversion": "4.5",
    "lastminor": "4.5.3",
    "active": true,
    "has_support": false
  }
}
```

#### Response Error - Environment No Encontrado (404)

```json
{
  "success": false,
  "error": "Environment not found for this host",
  "code": 404,
  "action": "sync",
  "data": {
    "searched_host": "https://sitio-inexistente.com",
    "original_host": "https://sitio-inexistente.com"
  }
}
```

#### Response Error - Environment Vinculado a Otro Token (401)

```json
{
  "success": false,
  "error": "El host proporcionado no está vinculado a este token",
  "code": 401,
  "action": "sync",
  "data": {}
}
```

#### Notas

- **IMPORTANTE**: Esta acción NO crea nuevos sitios, solo actualiza sitios existentes
- El sitio debe estar previamente creado en el Manager desde el panel de administración
- El `host` se normaliza para extraer solo el dominio
- Actualiza: versión, entorno, tipo, token de Moodle, projectid, y fecha de última sincronización
- El `moodletoken` se guarda pero NO se devuelve en la respuesta
- Si el sitio no existe, devuelve error 404
- Si el sitio existe pero está vinculado a otro token, devuelve error 401

---

### 2. Licence

Verifica si un plugin tiene licencia válida.

**Plugin**: Cualquier plugin de producto (ej: `theme_fresk`, `block_tresipuntsepe`)  
**Requiere `data`**: No  
**Validaciones Especiales**: 
- El site DEBE existir
- El plugin DEBE estar asociado al token como producto activo

#### Request

```json
{
  "action": "licence",
  "plugin": "theme_fresk",
  "host": "https://moodle.ejemplo.com",
  "version": "2025110401",
  "site": {
    "version": "2025081101",
    "release": "4.5.1",
    "env": "pro"
  }
}
```

#### Response Exitosa (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "licence",
  "data": {
    "expires_at": "2025-12-01",
    "product": "theme_fresk",
    "products": ["theme_fresk", "block_tresipuntsepe"],
    "environmentid": 3,
    "support_active": false,
    "warnings": []
  }
}
```


#### Response Error - Environment No Encontrado (401)

```json
{
  "success": false,
  "error": "Environment not found for this host",
  "code": 2002,
  "action": "licence",
  "data": {}
}
```

#### Notas

- `expires_at`: Fecha de expiración más restrictiva entre el token y el producto validado
- `product`: Slug del producto validado en la petición
- `products`: Array de slugs de todos los productos autorizados para el token
- `environmentid`: ID del entorno asociado al token
- `support_active`: Indica si el entorno tiene soporte activo (actualmente siempre `false`)
- `warnings`: Array de advertencias (actualmente siempre vacío)

---

### 2.1. Products

Obtiene todos los productos activos asociados a un sitio.

**Plugin**: No requerido (opcional)  
**Requiere `data`**: No  
**Validaciones Especiales**: 
- El site DEBE existir
- No requiere validación de plugin específico

#### Request

```json
{
  "action": "products",
  "host": "https://moodle.ejemplo.com",
  "site": {
    "version": "2025100601.03",
    "release": "5.1.1+ (Build: 20251219)",
    "env": "pro"
  }
}
```

#### Response Exitosa (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "products",
  "data": {
    "products": ["theme_fresk", "block_tresipuntsepe"],
    "siteid": 3,
    "support_active": false,
    "warnings": []
  }
}
```

#### Response Error - Site No Encontrado (401)

```json
{
  "success": false,
  "error": "Environment not found for this host. Host searched: https://sitio-inexistente.com. Available hosts: https://moodle.ejemplo.com",
  "code": 2002,
  "action": "products",
  "data": {
    "searched_host": "https://sitio-inexistente.com",
    "original_host": "https://sitio-inexistente.com",
    "available_hosts": ["https://moodle.ejemplo.com"]
  }
}
```

#### Notas

- Devuelve todos los productos activos asociados al token de licencia del sitio
- Los productos se filtran por estado `active` y fechas válidas (dentro del rango `start_at` y `end_at`)
- `support_active` indica si el sitio tiene soporte activo (`has_support`)
- `warnings` está preparado para futuras implementaciones
- No requiere el parámetro `plugin` ni `version` en el request

---

### 3. SCSS

Solicita archivos SCSS para personalización del plugin. Busca la versión compatible de archivos SCSS (versión exacta o la más alta compatible menor o igual a la versión del plugin).

**Plugin**: Cualquier plugin  
**Requiere `data`**: Opcional (para especificar archivos específicos)

#### Request - Obtener todos los archivos

```json
{
  "action": "scss",
  "plugin": "theme_fresk",
  "host": "https://moodle.ejemplo.com",
  "version": "2025110401",
  "site": {
    "version": "2025081101",
    "release": "4.5.1"
  }
}
```

#### Request - Obtener archivos específicos

```json
{
  "action": "scss",
  "plugin": "theme_fresk",
  "host": "https://moodle.ejemplo.com",
  "version": "2025110401",
  "site": {
    "version": "2025081101",
    "release": "4.5.1"
  },
  "data": {
    "files": ["variables", "mixins"]
  }
}
```

#### Response Exitosa (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "scss",
  "data": {
    "files": [
      {
        "filename": "variables",
        "found": true,
        "content": "$primary-color: #007bff;\n$secondary-color: #6c757d;"
      },
      {
        "filename": "mixins",
        "found": true,
        "content": "@mixin button-style {\n  padding: 10px 20px;\n  border-radius: 4px;\n}"
      }
    ]
  }
}
```

#### Response Error - Sin versión compatible (404)

```json
{
  "success": false,
  "error": "No compatible SCSS version found for this product version",
  "code": 3002,
  "action": "scss",
  "data": {}
}
```

#### Response Error - Sin archivos (404)

```json
{
  "success": false,
  "error": "No SCSS files found for this version",
  "code": 3002,
  "action": "scss",
  "data": {}
}
```

#### Response Error - Formato de versión inválido (500)

```json
{
  "success": false,
  "error": "Invalid version format. Expected YYYYMMDDXX format (10 digits)",
  "code": 3003,
  "action": "scss",
  "data": {}
}
```

#### Notas

- Si no se especifica `data.files`, devuelve todos los archivos de la versión compatible
- Si se especifican archivos en `data.files`, devuelve solo esos archivos (incluso si no existen, con `found: false`)
- Los archivos se buscan por nombre sin extensión (ej: `variables` busca `variables.scss`)
- La versión compatible se busca automáticamente: primero versión exacta, luego la más alta compatible (≤ versión del plugin)

#### Códigos de Error SCSS

| Código | Descripción | HTTP Status |
|--------|------------|-------------|
| 3000 | Producto no validado | 401 |
| 3002 | No se encontró versión compatible o archivos | 404 |
| 3003 | Formato de versión inválido | 500 |

---

### 3.1. SCSS CDN

Solicita archivos SCSS CDN marcados como servibles del bundle compatible. Los imports se procesan dinámicamente y se reemplazan por URLs firmadas con expiración de 1 hora.

**Plugin**: Cualquier plugin  
**Requiere `data`**: No (siempre devuelve todos los archivos servibles)

#### Request

```json
{
  "action": "scss-cdn",
  "plugin": "theme_fresk",
  "host": "https://moodle.ejemplo.com",
  "version": "2025110401",
  "site": {
    "version": "2025081101",
    "release": "4.5.1"
  }
}
```

#### Response Exitosa (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "scss-cdn",
  "data": {
    "files": [
      {
        "filename": "main",
        "found": true,
        "content": "@import url('https://tu-dominio.com/api/cdn/scss/TOKEN_1H/main.scss?signature=...');\n$primary-color: #007bff;\n$secondary-color: #6c757d;"
      }
    ]
  }
}
```

#### Response Error - Sin bundle compatible (404)

```json
{
  "success": false,
  "error": "No compatible SCSS CDN bundle found for this product version",
  "code": 3002,
  "action": "scss-cdn",
  "data": {}
}
```

#### Response Error - Sin archivos servibles (404)

```json
{
  "success": false,
  "error": "No servable SCSS files found for this bundle",
  "code": 3002,
  "action": "scss-cdn",
  "data": {}
}
```

#### Response Error - Formato de versión inválido (500)

```json
{
  "success": false,
  "error": "Invalid version format. Expected YYYYMMDDXX format (10 digits)",
  "code": 3003,
  "action": "scss-cdn",
  "data": {}
}
```

#### Notas

- Siempre devuelve todos los archivos marcados como servibles (`is_servable = true`) del bundle compatible
- Los imports (`@import`) se procesan dinámicamente en cada request y se reemplazan por `@import url('URL_FIRMADA')`
- Las URLs firmadas tienen expiración de 1 hora, suficiente para que Moodle compile el SCSS
- Los archivos importados también reciben URLs firmadas de 1 hora
- La versión compatible se busca automáticamente: primero versión exacta, luego la más alta compatible (≤ versión del plugin)
- El procesamiento de imports es dinámico: no se guarda el contenido procesado, se genera en cada request

#### Códigos de Error SCSS CDN

| Código | Descripción | HTTP Status |
|--------|------------|-------------|
| 3000 | Producto no validado | 401 |
| 3002 | No se encontró bundle compatible o archivos servibles | 404 |
| 3003 | Formato de versión inválido | 500 |

---

### 4. JS

Solicita archivos JavaScript necesarios para el plugin. Busca la versión compatible de archivos JS (versión exacta o la más alta compatible menor o igual a la versión del plugin).

**Plugin**: Cualquier plugin  
**Requiere `data`**: Opcional (para especificar archivos específicos)

#### Request - Obtener todos los archivos

```json
{
  "action": "js",
  "plugin": "theme_fresk",
  "host": "https://moodle.ejemplo.com",
  "version": "2025110401",
  "site": {
    "version": "2025081101",
    "release": "4.5.1"
  }
}
```

#### Request - Obtener archivos específicos

```json
{
  "action": "js",
  "plugin": "theme_fresk",
  "host": "https://moodle.ejemplo.com",
  "version": "2025110401",
  "site": {
    "version": "2025081101",
    "release": "4.5.1"
  },
  "data": {
    "files": ["main", "utils"]
  }
}
```

#### Response Exitosa (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "js",
  "data": {
    "files": [
      {
        "filename": "main",
        "found": true,
        "content": "console.log('Plugin theme_fresk loaded');\nfunction initTheme() {\n  // Código de inicialización\n}"
      },
      {
        "filename": "utils",
        "found": true,
        "content": "export function formatDate(date) {\n  return new Date(date).toLocaleDateString();\n}"
      }
    ]
  }
}
```

#### Response Error - Sin versión compatible (404)

```json
{
  "success": false,
  "error": "No compatible JS version found for this product version",
  "code": 4002,
  "action": "js",
  "data": {}
}
```

#### Response Error - Sin archivos (404)

```json
{
  "success": false,
  "error": "No JS files found for this version",
  "code": 4002,
  "action": "js",
  "data": {}
}
```

#### Response Error - Formato de versión inválido (500)

```json
{
  "success": false,
  "error": "Invalid version format. Expected YYYYMMDDXX format (10 digits)",
  "code": 4003,
  "action": "js",
  "data": {}
}
```

#### Notas

- Si no se especifica `data.files`, devuelve todos los archivos de la versión compatible
- Si se especifican archivos en `data.files`, devuelve solo esos archivos (incluso si no existen, con `found: false`)
- Los archivos se buscan por nombre sin extensión (ej: `main` busca `main.js`)
- La versión compatible se busca automáticamente: primero versión exacta, luego la más alta compatible (≤ versión del plugin)

#### Códigos de Error JS

| Código | Descripción | HTTP Status |
|--------|------------|-------------|
| 4000 | Producto no validado | 401 |
| 4002 | No se encontró versión compatible o archivos | 404 |
| 4003 | Formato de versión inválido | 500 |

---

### 5. Setup

Obtiene la configuración automatizada recomendada para el plugin desde archivos YAML almacenados en la base de datos. Busca la versión compatible de configuración (versión exacta o la más alta compatible menor o igual a la versión del plugin).

**Plugin**: Cualquier plugin  
**Requiere `data`**: No

#### Request

```json
{
  "action": "setup",
  "plugin": "theme_fresk",
  "host": "https://moodle.ejemplo.com",
  "version": "2025110401",
  "site": {
    "version": "2025081101",
    "release": "4.5.1"
  }
}
```

#### Response Exitosa (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "setup",
  "data": {
    "rules": {
      "rule1": true,
      "rule2": false
    },
    "defaults": {
      "enabled": true,
      "auto_update": false
    },
    "settings": {
      "key1": "value1",
      "key2": "value2"
    }
  }
}
```

#### Response Error - Sin versión compatible (404)

```json
{
  "success": false,
  "error": "No compatible setup configuration found for this product version",
  "code": 3002,
  "action": "setup",
  "data": {}
}
```

#### Response Error - Archivo YAML inválido (500)

```json
{
  "success": false,
  "error": "Invalid or unreadable setup configuration file",
  "code": 3003,
  "action": "setup",
  "data": {}
}
```

#### Notas

- La configuración se almacena en formato YAML en la base de datos
- Se parsea automáticamente y se devuelve como objeto JSON
- La versión compatible se busca automáticamente: primero versión exacta, luego la más alta compatible (≤ versión del plugin)
- La estructura del objeto devuelto depende del contenido del YAML almacenado

#### Códigos de Error Setup

| Código | Descripción | HTTP Status |
|--------|------------|-------------|
| 3000 | Producto no validado | 401 |
| 3002 | No se encontró configuración compatible | 404 |
| 3003 | Formato de versión inválido o archivo YAML inválido | 500 |

---

### 5.1. Features

Obtiene las funcionalidades (features) publicadas de un producto. Busca la versión compatible de features (versión exacta o la más alta compatible menor o igual a la versión del plugin). Solo devuelve features con `status = published`, ordenadas por `sort_order` ascendente.

**Plugin**: Cualquier plugin  
**Requiere `data`**: No

#### Request

```json
{
  "action": "features",
  "plugin": "theme_fresk",
  "host": "https://moodle.ejemplo.com",
  "version": "2025110401",
  "site": {
    "version": "2025081101",
    "release": "4.5.1"
  }
}
```

#### Response Exitosa (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "features",
  "data": [
    {
      "title": "Apariencia del curso",
      "description": "Configuración visual del curso",
      "content_html": "<p>Contenido HTML de la funcionalidad con imágenes y formato...</p>",
      "thumbnail_url": "https://tu-dominio.com/storage/features/1/thumbnail_xxx.jpg",
      "header_url": "https://tu-dominio.com/storage/features/1/header_xxx.jpg",
      "sort_order": 1,
      "updated_at": "2026-01-13T10:30:00Z"
    },
    {
      "title": "Personalización avanzada",
      "description": "Opciones de personalización del tema",
      "content_html": "<p>Más contenido HTML...</p>",
      "thumbnail_url": null,
      "header_url": null,
      "sort_order": 2,
      "updated_at": "2026-01-14T15:45:00Z"
    }
  ]
}
```

#### Response Exitosa - Sin Features Publicadas (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "features",
  "data": []
}
```

#### Response Error - Producto No Encontrado (401)

```json
{
  "success": false,
  "error": "Product not validated",
  "code": 3000,
  "action": "features",
  "data": {}
}
```

#### Response Error - Sin Versión Compatible (404)

```json
{
  "success": false,
  "error": "No compatible features version found for this product version",
  "code": 3002,
  "action": "features",
  "data": {}
}
```

#### Response Error - Formato de Versión Inválido (500)

```json
{
  "success": false,
  "error": "Invalid version format. Expected YYYYMMDDXX format (10 digits)",
  "code": 3003,
  "action": "features",
  "data": {}
}
```

#### Campos de Respuesta

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `title` | string | Título de la funcionalidad |
| `description` | string\|null | Descripción breve de la funcionalidad |
| `content_html` | string\|null | Contenido HTML completo (sanitizado, sin iframes ni scripts) |
| `thumbnail_url` | string\|null | URL pública de la imagen miniatura |
| `header_url` | string\|null | URL pública de la imagen de cabecera |
| `sort_order` | integer\|null | Orden de visualización (menor = primero) |
| `updated_at` | string | Fecha de última actualización en formato ISO 8601 |

#### Notas

- Busca automáticamente la versión compatible de features: primero versión exacta, luego la más alta compatible (≤ versión del plugin)
- Solo devuelve features con `status = published`
- Las features se ordenan automáticamente por `sort_order` ascendente (null al final)
- Si la versión compatible no tiene features publicadas, devuelve un array vacío `[]` con `success: true`
- Si no existe ninguna versión compatible de features, devuelve error 404 (código 3002)
- Las URLs de imágenes son públicas y accesibles directamente si se conoce la URL
- El campo `content_html` contiene HTML sanitizado usando HTMLPurifier (sin iframes, scripts, objetos, embeds, forms, etc.)
- Las imágenes dentro de `content_html` también son URLs públicas

#### Códigos de Error Features

| Código | Descripción | HTTP Status |
|--------|------------|-------------|
| 3000 | Producto no validado o no encontrado | 401 |
| 3002 | No se encontró versión compatible de features | 404 |
| 3003 | Formato de versión inválido | 500 |

---

### 5.2. Tutoriales

Obtiene los tutoriales (vídeos) publicados de un producto. Busca la versión compatible de tutoriales (versión exacta o la más alta compatible menor o igual a la versión del plugin). Solo devuelve tutoriales con `status = published`, ordenados por `sort_order` ascendente.

**Plugin**: Cualquier plugin  
**Requiere `data`**: No

#### Request

```json
{
  "action": "tutorials",
  "plugin": "theme_fresk",
  "host": "https://moodle.ejemplo.com",
  "version": "2025110401",
  "site": {
    "version": "2025081101",
    "release": "4.5.1"
  }
}
```

#### Response Exitosa (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "tutorials",
  "data": [
    {
      "title": "Configuración inicial del tema",
      "desc": "Aprende a configurar el tema desde cero",
      "platform": "youtube",
      "videoid": "dQw4w9WgXcQ",
      "video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "embed_url": "https://www.youtube.com/embed/dQw4w9WgXcQ",
      "sort_order": 1,
      "updated_at": "2026-01-13T10:30:00Z"
    },
    {
      "title": "Personalización avanzada",
      "desc": "Tutorial sobre opciones avanzadas de personalización",
      "platform": "vimeo",
      "videoid": "123456789",
      "video_url": "https://vimeo.com/123456789",
      "embed_url": "https://player.vimeo.com/video/123456789",
      "sort_order": 2,
      "updated_at": "2026-01-14T15:45:00Z"
    }
  ]
}
```

#### Response Exitosa - Sin Tutoriales Publicados (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "tutorials",
  "data": []
}
```

#### Response Error - Producto No Encontrado (401)

```json
{
  "success": false,
  "error": "Product not validated",
  "code": 4000,
  "action": "tutorials",
  "data": {}
}
```

#### Response Error - Sin Versión Compatible (404)

```json
{
  "success": false,
  "error": "No compatible tutorials version found for this product version",
  "code": 4002,
  "action": "tutorials",
  "data": {}
}
```

#### Response Error - Formato de Versión Inválido (500)

```json
{
  "success": false,
  "error": "Invalid version format. Expected YYYYMMDDXX format (10 digits)",
  "code": 4003,
  "action": "tutorials",
  "data": {}
}
```

#### Campos de Respuesta

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `title` | string | Título del tutorial |
| `desc` | string\|null | Descripción breve del tutorial |
| `platform` | string | Plataforma del vídeo (`youtube` o `vimeo`) |
| `videoid` | string | ID del vídeo en la plataforma |
| `video_url` | string | URL completa del vídeo (para enlaces directos) |
| `embed_url` | string | URL de embed para iframe (para reproducir embebido) |
| `sort_order` | integer\|null | Orden de visualización (menor = primero) |
| `updated_at` | string | Fecha de última actualización en formato ISO 8601 |

#### Notas

- Busca automáticamente la versión compatible de tutoriales: primero versión exacta, luego la más alta compatible (≤ versión del plugin)
- Solo devuelve tutoriales con `status = published`
- Los tutoriales se ordenan automáticamente por `sort_order` ascendente (null al final)
- Si la versión compatible no tiene tutoriales publicados, devuelve un array vacío `[]` con `success: true`
- Si no existe ninguna versión compatible de tutoriales, devuelve error 404 (código 4002)
- Las URLs `video_url` y `embed_url` se construyen automáticamente según la plataforma
- Plataformas soportadas: `youtube` y `vimeo`
- La `video_url` es para enlaces directos (abrir en nueva pestaña)
- La `embed_url` es para reproducir el vídeo embebido en un iframe

#### Códigos de Error Tutoriales

| Código | Descripción | HTTP Status |
|--------|------------|-------------|
| 4000 | Producto no validado o no encontrado | 401 |
| 4002 | No se encontró versión compatible de tutoriales | 404 |
| 4003 | Formato de versión inválido | 500 |

---

### 5.3. Resources

Obtiene los recursos (material relacionado) publicados y públicos de un producto. Busca la versión compatible de recursos (versión exacta o la más alta compatible menor o igual a la versión del plugin). Solo devuelve recursos con `status = published` y `is_public = true`, ordenados por `sort_order` ascendente.

**Plugin**: Cualquier plugin  
**Requiere `data`**: No

#### Request

```json
{
  "action": "resources",
  "plugin": "theme_fresk",
  "host": "https://moodle.ejemplo.com",
  "version": "2025110401",
  "site": {
    "version": "2025081101",
    "release": "4.5.1"
  }
}
```

#### Response Exitosa (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "resources",
  "data": [
    {
      "title": "Guía de instalación",
      "description": "Documento PDF con instrucciones detalladas",
      "type": "file",
      "file_url": "https://tu-dominio.com/storage/resources/1/guia_instalacion_20260116115000_abc123.pdf",
      "original_filename": "guia_instalacion.pdf",
      "size": 524288,
      "mime": "application/pdf",
      "read_time": 300,
      "sort_order": 1,
      "updated_at": "2026-01-13T10:30:00Z"
    },
    {
      "title": "Documentación oficial",
      "description": "Enlace a la documentación completa",
      "type": "external",
      "url": "https://docs.ejemplo.com/theme_fresk",
      "read_time": 600,
      "sort_order": 2,
      "updated_at": "2026-01-14T15:45:00Z"
    }
  ]
}
```

#### Response Exitosa - Sin Recursos Publicados (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "resources",
  "data": []
}
```

#### Response Error - Producto No Encontrado (401)

```json
{
  "success": false,
  "error": "Product not validated",
  "code": 5000,
  "action": "resources",
  "data": {}
}
```

#### Response Error - Sin Versión Compatible (404)

```json
{
  "success": false,
  "error": "No compatible resources version found for this product version",
  "code": 5002,
  "action": "resources",
  "data": {}
}
```

#### Response Error - Formato de Versión Inválido (500)

```json
{
  "success": false,
  "error": "Invalid version format. Expected YYYYMMDDXX format (10 digits)",
  "code": 5003,
  "action": "resources",
  "data": {}
}
```

#### Campos de Respuesta

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `title` | string | Título del recurso |
| `description` | string\|null | Descripción breve del recurso |
| `type` | string | Tipo de recurso (`file` o `external`) |
| `file_url` | string\|null | URL pública del archivo (solo si `type = file` y `is_public = true`) |
| `original_filename` | string\|null | Nombre original del archivo (solo si `type = file`) |
| `size` | integer\|null | Tamaño del archivo en bytes (solo si `type = file`) |
| `mime` | string\|null | Tipo MIME del archivo (solo si `type = file`) |
| `url` | string\|null | URL externa (solo si `type = external`) |
| `read_time` | integer\|null | Tiempo estimado de lectura en segundos |
| `sort_order` | integer\|null | Orden de visualización (menor = primero) |
| `updated_at` | string | Fecha de última actualización en formato ISO 8601 |

#### Notas

- Busca automáticamente la versión compatible de recursos: primero versión exacta, luego la más alta compatible (≤ versión del plugin)
- Solo devuelve recursos con `status = published` y `is_public = true`
- Los recursos se ordenan automáticamente por `sort_order` ascendente (null al final)
- Si la versión compatible no tiene recursos publicados, devuelve un array vacío `[]` con `success: true`
- Si no existe ninguna versión compatible de recursos, devuelve error 404 (código 5002)
- Para recursos tipo `file`: la `file_url` solo se incluye si `is_public = true` y el archivo existe
- Para recursos tipo `external`: se incluye la `url` directamente
- Los archivos se sirven desde `/storage/...` mediante el enlace simbólico de Laravel
- Tipos de archivo soportados: PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX (máx. 10MB)

#### Códigos de Error Resources

| Código | Descripción | HTTP Status |
|--------|------------|-------------|
| 5000 | Producto no validado o no encontrado | 401 |
| 5002 | No se encontró versión compatible de recursos | 404 |
| 5003 | Formato de versión inválido | 500 |

---

### 6. Data

Envía datos de telemetría desde Moodle hacia Laravel.

**Plugin**: Cualquier plugin  
**Requiere `data`**: Sí

#### Request

```json
{
  "action": "data",
  "plugin": "local_tresipunt",
  "host": "https://moodle.ejemplo.com",
  "version": "2025110401",
  "site": {
    "version": "2025081101",
    "release": "4.5.1"
  },
  "data": {
    "courses": 150,
    "users": 5000,
    "activeusers": 3200,
    "enrolments": 12000,
    "moodlerelease": "4.5.1",
    "language": "es",
    "countrycode": "ES"
  }
}
```

#### Response Exitosa (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "data",
  "data": {
    "received": true,
    "site_id": 3
  }
}
```

#### Campos de Data Soportados

El campo `data` puede incluir cualquiera de estos campos (todos opcionales):

- `policyagreed`, `language`, `countrycode`, `privacy`
- `contactemail`, `contactable`, `emailalert`, `emailalertemail`
- `commnews`, `commnewsemail`, `contactname`, `description`
- `imageurl`, `contactphone`, `regioncode`, `geolocation`, `street`
- `courses`, `users`, `activeusers`, `enrolments`
- `posts`, `questions`, `resources`, `badges`, `issuedbadges`
- `participantnumberaverage`, `activeparticipantnumberaverage`, `modulenumberaverage`
- `moodlerelease`, `url`
- `mobileservicesenabled`, `mobilenotificationsenabled`
- `registereduserdevices`, `registeredactiveuserdevices`
- `analyticsenabledmodels`, `analyticspredictions`, `analyticsactions`, `analyticsactionsnotuseful`
- `availableupdatesfetch`

#### Notas

- Los datos se guardan o actualizan en la tabla `data` asociada al site
- Si el registro ya existe, se actualiza; si no, se crea

---

### 7. Plugins

Envía información sobre plugins instalados en Moodle.

**Plugin**: Cualquier plugin  
**Requiere `data`**: Sí (con array `plugins`)

#### Request

```json
{
  "action": "plugins",
  "plugin": "local_tresipunt",
  "host": "https://moodle.ejemplo.com",
  "version": "2025110401",
  "site": {
    "version": "2025081101",
    "release": "4.5.1"
  },
  "data": {
    "plugins": [
      {
        "name": "theme_fresk",
        "component": "theme_fresk",
        "type": "theme",
        "version": "2025110401",
        "versiondisk": "2025110401",
        "versiondb": "2025110401",
        "release": "1.0.0",
        "dependencies": [],
        "availableupdates": []
      },
      {
        "name": "block_tresipuntsepe",
        "component": "block_tresipuntsepe",
        "type": "block",
        "version": "2025110301",
        "release": "2.1.0"
      }
    ]
  }
}
```

#### Response Exitosa (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "plugins",
  "data": {
    "processed": 2,
    "total": 2,
    "errors": []
  }
}
```

#### Response con Errores Parciales (200)

```json
{
  "success": true,
  "error": "",
  "code": 0,
  "action": "plugins",
  "data": {
    "processed": 1,
    "total": 2,
    "errors": [
      {
        "plugin": "plugin_invalido",
        "error": "El plugin debe tener al menos name o component"
      }
    ]
  }
}
```

#### Campos de Plugin Soportados

Cada plugin en el array puede incluir:

- `name` (requerido si no hay `component`)
- `component` (requerido si no hay `name`)
- `type`: tipo de plugin (theme, block, mod, local, etc.)
- `version`, `versiondisk`, `versiondb`: versiones
- `versionrequires`: versión mínima requerida
- `pluginsupported`, `pluginincompatible`: compatibilidad
- `release`: versión legible
- `dependencies`: array de dependencias
- `availableupdates`: array de actualizaciones disponibles

#### Notas

- Los plugins se crean o actualizan en la tabla `plugins` asociada al site
- Si un plugin falla, se registra en `errors` pero se continúa procesando los demás
- Se retorna el total procesado y los errores encontrados

---

## Ejemplos de Errores Comunes

### Token No Proporcionado (401)

```json
{
  "success": false,
  "error": "Token no proporcionado",
  "code": 401,
  "action": null,
  "data": {}
}
```

### Token Inválido (401)

```json
{
  "success": false,
  "error": "Token no válido",
  "code": 401,
  "action": null,
  "data": {}
}
```

### Token Inactivo (403)

```json
{
  "success": false,
  "error": "Token inactivo",
  "code": 403,
  "action": null,
  "data": {}
}
```

### Token Expirado (403)

```json
{
  "success": false,
  "error": "Token expirado. Fecha de expiración: 2025-01-01 00:00:00",
  "code": 403,
  "action": null,
  "data": {}
}
```

### Host No Vinculado (403)

```json
{
  "success": false,
  "error": "El host proporcionado no está vinculado a este token",
  "code": 403,
  "action": null,
  "data": {}
}
```

### Límite de Peticiones Excedido (429)

```json
{
  "success": false,
  "error": "Límite de peticiones excedido. Intente nuevamente en 5 minuto(s)",
  "code": 429,
  "action": null,
  "data": {}
}
```

### Error de Validación (400)

```json
{
  "success": false,
  "error": "El campo action es obligatorio.",
  "code": 400,
  "action": null,
  "data": {}
}
```

---

## Rate Limiting

El sistema implementa rate limiting por token:

- **Límite por defecto**: 10,000 peticiones
- **Límite personalizado**: Configurable por token en `usage_limit`
- **Ventana**: 60 segundos
- **Respuesta**: HTTP 429 cuando se excede el límite

---

## Notas de Implementación

### Mapeo Plugin → Producto

Los plugins se mapean directamente a productos mediante el `slug`:
- `theme_fresk` → Producto con `slug = "theme_fresk"`
- `block_tresipuntsepe` → Producto con `slug = "block_tresipuntsepe"`
- `local_tresipunt` → Producto con `slug = "local_tresipunt"`

### Normalización de Dominio

El sistema normaliza automáticamente el `host` para extraer solo el dominio:
- `https://moodle.ejemplo.com` → `moodle.ejemplo.com`
- `http://moodle.ejemplo.com/path` → `moodle.ejemplo.com`

### Creación de Sitios

**IMPORTANTE**: Los sitios NO se crean mediante la API. Deben ser creados previamente desde el panel de administración del Manager. La acción `sync` solo actualiza información de sitios existentes. Si intentas hacer `sync` con un sitio que no existe, recibirás un error 404.

### Validaciones Específicas por Acción

- **sync**: Requiere que el site exista previamente (solo actualiza, NO crea nuevos sitios)
- **licence**: Requiere que el site exista y que el plugin esté autorizado
- **products**: Requiere que el site exista (no requiere validación de plugin específico)
- **data, plugins**: Requieren que el site exista
- **scss, scss-cdn, js, setup, features, tutorials**: Requieren que el producto esté validado
  - **scss/js**: Buscan versión compatible de archivos (exacta o más alta compatible ≤ versión del plugin)
  - **scss-cdn**: Busca bundle compatible y devuelve archivos servibles con imports procesados dinámicamente
  - **setup**: Busca versión compatible de configuración YAML (exacta o más alta compatible ≤ versión del plugin)
  - **features**: Busca versión compatible de features (exacta o más alta compatible ≤ versión del plugin) y devuelve features publicadas, ordenadas por sort_order
  - **tutorials**: Busca versión compatible de tutoriales (exacta o más alta compatible ≤ versión del plugin) y devuelve tutoriales publicados, ordenados por sort_order
  - **resources**: Busca versión compatible de recursos (exacta o más alta compatible ≤ versión del plugin) y devuelve recursos publicados y públicos, ordenados por sort_order

---

## Versiones Futuras

La API está preparada para futuras versiones:

- `/api/v1/` - Versión actual
- `/api/v2/` - Preparado para futuras implementaciones

Cada versión puede tener su propio controlador y lógica específica.

---

## Soporte

Para soporte técnico o consultas sobre la API, contactar con el equipo de desarrollo.

**Última actualización**: 2025-01-XX

