# TuInternet Console — Integration Contract v1

URL pública del contrato: https://console.tuinternet.pe/contratomd

## Prompt para adaptar un sistema

Lee este contrato e implementa la integración completa de este producto con TuInternet Console. Revisa el código existente, identifica sus características reales y publica sus definiciones mediante `POST /api/v1/system/sync`. Usa claves estables que correspondan a controles implementados en el producto. No declares características que el producto no pueda respetar.

Sincroniza las empresas, guarda el `organizationId` central, consulta sus entitlements y aplica `active`, los límites, las funciones y `accessExpiresAt` antes de autorizar operaciones. Reporta uso de capacidad y descuenta cuotas o créditos por los endpoints de este contrato. Usa el token únicamente en el backend. Implementa también soporte temporal si el producto lo permite. Verifica los casos de límite alcanzado, acceso vencido, acceso sin vencimiento, empresa suspendida, saldo insuficiente y reintentos idempotentes. No mezcles usuarios, contraseñas ni datos operativos de productos distintos.

## Gestión sencilla en Console

La navegación principal tiene cuatro secciones: **Dashboard**, **Pagos**, **Sistemas vinculados** y **Planes**. Las empresas se administran desde Sistemas vinculados; la caja está dentro de Pagos y los planes asignados se consultan desde Planes.

El **contrato del sistema** define `features`: clave, etiqueta, tipo, unidad y modelo de cada característica. Console muestra esas claves en selectores. El operador elige lo que el sistema ofrece; no inventa claves ni cambia su tipo o modelo. Cada producto debe enviar el contrato completo al sincronizar.

Hay dos tipos de gestión:

1. **Por créditos:** se elige un recurso declarado con `kind: "credit"` y una cantidad de unidades. La vigencia es independiente: fecha de vencimiento o ilimitada. El saldo se entrega al completar un pago y el producto debe consumirlo mediante la API.
2. **Por características:** se elige una o varias características declaradas, por ejemplo `max_naps`, y su cantidad o `null` para cantidad ilimitada. La vigencia también puede tener fecha de vencimiento o ser ilimitada. Una cantidad ilimitada no implica acceso sin vencimiento.

La fecha de vencimiento (`fecha_vence` en términos comerciales) se devuelve al producto como **`accessExpiresAt`**. `null` significa acceso sin vencimiento, siempre sujeto a `active` y al estado de la empresa/suscripción/sistema. Las fechas se transmiten en ISO 8601 con zona horaria. El producto debe bloquear el acceso al llegar el vencimiento incluso si tiene una licencia en caché; nunca debe mantener una autorización más allá de su vencimiento o TTL.

Los planes se asignan a una empresa. Una nueva licencia con plan del catálogo queda sin acceso hasta completar su pago. Los cargos conservan una copia de cantidad, vigencia y características contratadas; cambiar o archivar el plan no altera esos cargos. Un pago parcial o pendiente de validación no entrega beneficios. El pago completo aplica los límites y la vigencia; una suspensión manual permanece vigente. El reverso restaura beneficios anteriores cuando corresponde y conserva otras compras o cambios administrativos posteriores.

Ejemplos de `features` que el producto puede presentar, únicamente si los implementa:

```json
{
  "max_naps": {"type":"integer","default":0,"label":"Cajas NAP","kind":"limit","unit":"NAP"},
  "credits": {"type":"integer","default":0,"label":"Créditos","kind":"credit","unit":"créditos"}
}
```

La integración no obliga a usar esas claves: cada sistema presenta sus propias características estables. El operador las selecciona por su etiqueta visible.

## Principio
- Console es dueña de organizaciones, productos, suscripciones, entitlements, cobros y soporte.
- Cada producto es dueño de sus usuarios operativos y de su lógica de negocio.
- Todo cliente es una organización, aunque tenga un solo usuario.

## Variables .env del producto
```env
CONSOLE_URL=https://console.tuinternet.pe
CONSOLE_SYSTEM_TOKEN=sys_xxxxxxxxxxxxxxxxx
```

El token es persistente y se muestra una sola vez al crear el sistema en Console.
Si se rota, el anterior queda revocado inmediatamente.

## 1. Presentación / sync del producto
`POST /api/v1/system/sync`

Header:
`Authorization: Bearer <CONSOLE_SYSTEM_TOKEN>`

Body:
```json
{
  "name": "SMARTEC FTTH",
  "version": "2.4.1",
  "baseUrl": "https://ftth.tuinternet.pe",
  "capabilities": {
    "support_login": true,
    "business_sync": true
  },
  "features": {
    "arcgis": {"type": "boolean", "default": false, "label": "ArcGIS"},
    "max_naps": {"type": "integer", "default": null, "label": "Cajas NAP", "kind": "limit", "unit": "NAP"},
    "max_mufas": {"type": "integer", "default": null, "label": "Mufas", "kind": "limit", "unit": "mufas"}
  },
  "endpoints": {
    "support_login": "https://ftth.tuinternet.pe/console/support-login"
  }
}
```

## 1.1 Registro de usuario / empresa en cada producto

El registro de clientes ocurre en el producto, no en Console.

Regla estándar:
- El formulario mínimo pide **correo + clave**.
- No se requiere verificación de correo.
- La clave se guarda únicamente en el producto, con hash seguro.
- Al registrarse, el producto crea automáticamente una empresa/organización local.
- El primer usuario queda como propietario/administrador de esa empresa.
- Inmediatamente el producto llama a `POST /api/v1/system/organizations/sync`.
- El `organizationId` devuelto por Console se guarda como `console_org_id`.
- Si Console está temporalmente fuera de línea, el registro local puede completarse y el producto debe reintentar el sync.
- Usuarios, claves y sesiones operativas permanecen en cada producto; Console no almacena las claves de clientes.
- Si Console devuelve `409 organization_link_required`, la cuenta local puede existir, pero queda pendiente de vinculación administrativa. No reintentar con otro RUC o ID para evitar el conflicto.

Flujo:

```text
correo + clave
      ↓
usuario local + empresa local
      ↓
POST /api/v1/system/organizations/sync
      ↓
console_org_id
```

## 2. Sincronizar empresa
`POST /api/v1/system/organizations/sync`

```json
{
  "externalId": "412",
  "name": "Internet Los Andes",
  "legalName": "Los Andes Networks SAC",
  "taxId": "20XXXXXXXXX",
  "email": "admin@example.com"
}
```

Respuesta:
```json
{
  "ok": true,
  "organizationId": "org_xxx",
  "externalId": "412"
}
```

Guardar `organizationId` como `console_org_id` en la empresa local.

### Identidad y propiedad de los datos

- El sync es idempotente por sistema autenticado + `externalId`. Ese identificador debe permanecer estable.
- El primer sync crea una empresa si no hay conflicto de RUC/documento. Los siguientes actualizan los datos reportados en el vínculo local, sin sobrescribir la ficha central.
- Un RUC coincidente **no autoriza** vincularse a una empresa existente. Console responde `409` con `{"error":"organization_link_required"}` sin entregar la identidad de la empresa existente.
- Un administrador verifica el ID local y lo autoriza desde **Empresa → Vínculos con sistemas → Vincular sistema**. Luego el producto repite el mismo sync y obtiene el `organizationId` autorizado.
- Sin RUC no se vincula automáticamente por correo o nombre. Si se sabe que la empresa ya existe, el administrador debe autorizar el ID local antes del primer sync.
- Un vínculo existente no se reasigna a otra empresa mediante esta API. La resolución de duplicados ya creados requiere un proceso de consolidación posterior.
- El sync no crea ni activa una suscripción. Si aún no se contrató el producto, entitlements responde `404 subscription_not_found`.

API administrativa (sesión de operador, no token de producto):

`POST /api/organizations/:id/system-links`

```json
{"systemId":"sys_xxx","externalId":"412","reason":"Identificador verificado con el responsable de la empresa"}
```

`GET /api/organizations/:id/system-links` permite ver los vínculos y el último dato reportado por cada uno.

`PATCH /api/organizations/:id` edita la ficha central; los campos omitidos se conservan. Un cambio de estado requiere `reason`. Estados admitidos: `active`, `suspended`, `cancelled`.

## 3. Obtener licencia / entitlements
`GET /api/v1/system/organizations/:console_org_id/entitlements`

Respuesta:
```json
{
  "organizationId": "org_xxx",
  "systemId": "sys_xxx",
  "active": true,
  "reason": null,
  "status": "active",
  "organizationStatus": "active",
  "plan": "Pro",
  "features": {
    "arcgis": true,
    "max_naps": 500
  },
  "renewsAt": "2026-11-01T00:00:00.000Z",
  "accessExpiresAt": null,
  "cacheTtlSeconds": 60,
  "issuedAt": "2026-10-05T12:00:00.000Z"
}
```

El producto debe cachear esta respuesta por `cacheTtlSeconds` y consultar de nuevo al vencer ese plazo. `active` es el acceso efectivo y debe verificarse antes de aplicar features. Los bloqueos se reflejan cuando el producto actualiza la caché; Console todavía no recibe una confirmación de ejecución.

`active` requiere empresa y suscripción activas y ausencia de vencimiento explícito de acceso. `reason` informa el bloqueo (`organization_suspended`, `organization_cancelled`, `subscription_suspended`, `subscription_cancelled`, `access_expired`). Un sistema inactivo no puede autenticarse con su token.

`renewsAt` es la próxima renovación comercial y **no corta el acceso** por sí sola. `accessExpiresAt` es opcional y bloquea la licencia al llegar a esa fecha. Las suscripciones existentes se migran con `accessExpiresAt: null`, sin inventar vencimientos. Un cargo puede indicar `accessUntil`: completar su pago confirmado renueva el acceso hasta esa fecha, conservando bloqueos manuales de empresa/suscripción. La política automática de deuda/gracia y el comportamiento del producto ante una caída de Console aún deben acordarse.

API administrativa: `POST /api/organizations/:id/subscriptions`, con `systemId` obligatorio. En actualizaciones se conservan los campos omitidos. Para borrar una fecha enviar `null`; para cambiar estado enviar también `reason`. Importes en centavos enteros no negativos; moneda válida en mayúsculas; features como objeto de valores escalares. Un límite numérico puede ser `null` (sin límite). Si el sistema declara features, los overrides deben respetar sus claves, tipos y límites `minimum`/`maximum`.

También se gestionan `billingModel` (`subscription`, `usage`, `prepaid`) y `billingCycle` (`monthly`, `yearly`, `none`). Describen el modelo y la frecuencia del precio. Esta versión genera cargos manuales; no factura automáticamente a partir del consumo ni ejecuta débitos bancarios recurrentes.

## 4. Soporte / autologin
Console crea un ticket de un solo uso y abre:
`https://producto/console/support-login?ticket=st_xxx`

El producto consume:
`POST /api/v1/system/support/consume`

```json
{"ticket":"st_xxx"}
```

Console devuelve:
- organizationId
- effectiveRole
- operador real de soporte
- modo de sesión

El producto crea una sesión LOCAL temporal (recomendado 30 min) para esa empresa.
No se usa ni modifica la contraseña de ningún usuario del cliente.
El ticket expira en 60 segundos y no puede reutilizarse.

Se revalidan los permisos y el estado activo del operador al consumirlo. Un ticket emitido antes de desactivar al operador o revocar su permiso de soporte devuelve `403 support_operator_revoked`.

Los modos admitidos son `admin` (rol efectivo `admin`) y `read_only` (rol efectivo `viewer`); el producto debe aplicar estos roles. El acceso de soporte puede utilizarse para atender una empresa con licencia suspendida, sin reactivar su licencia comercial.

El manifest debe declarar `baseUrl` HTTPS. `endpoints.support_login`, si se declara, debe usar el mismo origen, sin credenciales, fragmentos ni parámetro `ticket` preexistente. Si no se declara, se usa `/console/support-login` en ese origen.

## 5. Permisos de operadores y cambios de esquema

| Rol | Lectura de Console/auditoría | Sistemas, empresas y licencias | Acceso temporal de soporte |
| --- | --- | --- | --- |
| `platform_admin` | Sí | Administrar | Sí |
| `support` | Sí | No | Sí |
| `finance` | Sí | Configurar suscripciones; no administrar sistemas ni empresas | No |
| Rol desconocido | No | No | No |

`GET /api/me` devuelve `permissions`. El backend valida permisos en cada operación y la interfaz adapta los controles. Administradores y finanzas gestionan cargos, pagos y su propia caja; soporte consulta la cuenta corriente sin modificar dinero ni créditos. La administración de operadores sigue pendiente.

Las migraciones se registran en `schema_migrations` y se aplican al arrancar. Conservan las suscripciones y vínculos existentes. Las modificaciones de ficha, licencias, vínculos, tokens y tickets se guardan en la misma transacción que su auditoría; una falla revierte la operación. La auditoría de ficha/licencias incluye antes, después y motivo.

## 6. Recursos: capacidad, cuota y crédito

Los campos del manifest se transforman en controles de **Suscripciones y límites**. No hay límites fijos para un producto específico.

| `kind` | Significado | Ejemplo |
| --- | --- | --- |
| `limit` | Capacidad simultánea; el producto cuenta y aplica el límite | NAP, mufas, usuarios, metros de fibra |
| `quota` | Consumo acumulado que Console puede autorizar y descontar | Tokens mensuales |
| `credit` | Saldo prepago, con entradas y consumos auditados | Créditos por generación |
| `flag` | Función o característica | ArcGIS, Street View |

Si se omite `kind`, los tipos numéricos se muestran como capacidad y los demás como funciones. Cuotas y créditos requieren tipo `integer`. Los créditos declaran `default: 0`: el saldo se carga por pagos o ajustes, no cambiando un entitlement.

Ejemplo para un producto de IA:

```json
{
  "features": {
    "tokens": {"type":"integer","default":100000,"label":"Tokens mensuales","kind":"quota","unit":"tokens","period":"monthly"},
    "credits": {"type":"integer","default":0,"label":"Créditos","kind":"credit","unit":"créditos"}
  }
}
```

`period` de una cuota es `monthly` (mes calendario UTC) o `lifetime` (acumulado, valor por defecto). Un cupo numérico `null` significa sin límite. Los nombres de claves deben coincidir con los que utiliza el producto.

Las características se presentan desde el backend del producto mediante `POST /api/v1/system/sync`. En Console se consultan desde **Sistemas vinculados → Ver características del contrato** y se seleccionan en **Planes**. La ruta administrativa `PATCH /api/systems/:id/features` permite ajustar valores predeterminados existentes, pero rechaza claves nuevas y cambios de etiqueta, tipo, modelo, unidad, período o restricciones del contrato. Conserva ajustes previos por compatibilidad. No se puede retirar un recurso con saldos, planes o beneficios contratados activos.

La respuesta de entitlements añade `billingModel`, `billingCycle` y `resources`:

```json
{
  "resources": {
    "max_naps": {"kind":"limit","label":"Cajas NAP","limit":100,"used":42,"remaining":58,"reportedAt":"2026-10-05T12:00:00.000Z"},
    "tokens": {"kind":"quota","label":"Tokens mensuales","limit":100000,"used":45000,"remaining":55000,"periodKey":"2026-10"},
    "credits": {"kind":"credit","label":"Créditos","balance":150}
  }
}
```

Los ejemplos muestran un subconjunto de campos. `resources` también incluye clave, tipo, unidad y banderas de exceso de uso. La ausencia de reporte de capacidad se representa con `used: null`, no con un consumo inventado.

### Reportar capacidad usada

`POST /api/v1/system/usage` (token del producto):

```json
{"organizationId":"org_xxx","reportedAt":"2026-10-05T12:00:00.000Z","usage":{"max_naps":42,"max_mufas":8}}
```

Solo admite recursos `limit` declarados y empresas vinculadas al sistema. Reportes antiguos no sobrescriben los recientes. El producto debe aplicar localmente estos límites antes de crear más elementos; Console muestra el consumo y la condición configurada.

### Consumir tokens o créditos

`POST /api/v1/system/resources/consume` (token del producto):

```json
{"organizationId":"org_xxx","resource":"tokens","units":350,"eventId":"operacion-local-891"}
```

- Para cuotas devuelve `used`, `remaining` y `periodKey`; para créditos, `balance`.
- Repetir el mismo `eventId` con los mismos datos devuelve la misma respuesta sin consumir de nuevo. Cambiar los datos devuelve `409 idempotency_conflict`.
- No autoriza nuevas operaciones si la empresa/suscripción no tiene acceso activo, si excede la cuota o si faltan créditos. El consumo es atómico, incluso con solicitudes simultáneas.
- El producto debe integrar esta llamada en su operación y conservar IDs estables al reintentar. Consultar solamente el saldo no reserva recursos para solicitudes concurrentes.
- Estas llamadas no se añaden automáticamente a los otros repositorios al actualizar Console. Cada producto debe reportar su capacidad o usar este canal de consumo para que la vista central muestre su actividad real.

## 7. Cargos, pagos y caja

Guía de operación: [BILLING.md](BILLING.md).

API administrativa, con sesión y permiso `billing.manage`:

- `GET /api/billing`: cargos, pagos y saldos separados por moneda.
- `POST /api/charges`: `orgId`, `systemId`, `description`, `amountCents`, `currency`, `dueAt`, `idempotencyKey`; opcionales `periodKey`, `accessUntil`, `creditResource` + `creditUnits`.
- `POST /api/payments`: `chargeId`, `amountCents`, `method`, `idempotencyKey`; opcionales `status` (`pending`/`confirmed`, confirmado por defecto), `reference`, `cashSessionId`.
- `POST /api/payments/:id/confirm`: confirmar recepción; cuerpo `{}` o `cashSessionId`.
- `POST /api/payments/:id/reverse`: revertir/cancelar, con `reason` y opcional `cashSessionId` para devolución de efectivo.
- `POST /api/charges/:id/void`: anular un cargo sin pagos confirmados, con `reason`; también cancela recibos pendientes.

Un pago confirmado puede ser parcial. No se permite exceder el saldo del cargo. Se evita duplicar la misma clave idempotente y la misma referencia de operación del mismo medio mientras no esté revertida. Completar un cargo aplica una sola vez la renovación y/o entrega de créditos. Si sus créditos ya se consumieron y no hay saldo suficiente para retirarlos, el reverso se rechaza sin alterar el dinero ni la cuenta corriente.

Caja requiere `cash.manage`: `GET /api/cash`, `POST /api/cash/open` (`currency`, `openingCents`), `POST /api/cash/:id/movements` (`amountCents` firmado, `reason`, `idempotencyKey`) y `POST /api/cash/:id/close` (`countedCents`, `note` obligatoria si hay diferencia). Una caja abierta por operador y moneda. Efectivo requiere caja abierta; transferencias, Yape, Plin y tarjetas no aumentan el efectivo físico. No se modifica una caja cerrada para devolver dinero: la devolución se registra en una caja abierta actual.

Ajustes de cortesía/corrección: `POST /api/organizations/:id/credits`, con `systemId`, `resource`, `units` firmado, `reason`, `idempotencyKey`. Esta operación no registra dinero. Las ventas se registran como cargos y pagos.

## Reglas de seguridad
- Nunca guardar CONSOLE_SYSTEM_TOKEN en frontend.
- Solo backend -> Console.
- Usar HTTPS.
- El token del sistema identifica al producto, no al usuario.
- Support tickets son distintos del token persistente.
- Registrar auditoría de sesiones de soporte.
- Los usuarios operativos permanecen en cada sistema.


## 8. Catálogo de planes (API de operadores)

Estas rutas requieren sesión de operador y permiso `subscriptions.manage`; el token de un producto no administra planes.

`GET /api/plans` lista el catálogo. `POST /api/plans` crea y `PATCH /api/plans/:id` reemplaza los datos del plan. `kind: "time"` representa la gestión por características por compatibilidad; `kind: "credit"` representa créditos. La vigencia se define por `accessMode`, independientemente del tipo de gestión:

- `date`: requiere `expiresAt` (fecha de vencimiento ISO 8601).
- `unlimited`: sin vencimiento.
- `duration`: compatibilidad con planes por días/meses/años; requiere `durationCount` y `durationUnit`.

Ejemplo: 100 cajas NAP hasta una fecha:

```json
{
  "systemId":"sys_...",
  "name":"100 cajas NAP",
  "kind":"time",
  "priceCents":5000,
  "currency":"PEN",
  "entitlements":{"max_naps":100},
  "accessMode":"date",
  "expiresAt":"2027-12-31T23:59:59.000Z",
  "active":true
}
```

Ejemplo: 1.000 créditos sin vencimiento:

```json
{
  "systemId":"sys_...",
  "name":"1.000 créditos",
  "kind":"credit",
  "priceCents":2000,
  "currency":"PEN",
  "creditResource":"credits",
  "creditUnits":1000,
  "accessMode":"unlimited",
  "active":true
}
```

Para cantidades ilimitadas, enviar `null` en el entitlement numérico. `GET /api/plans` devuelve campos SQL como `access_mode`, `expires_at` y `entitlements_json`. La API del producto sigue devolviendo `features`, `resources` y `accessExpiresAt` con los valores efectivos de la empresa. El producto no debe inferir beneficios leyendo el catálogo.
