# Manual de Acceso API — NominaApp (Server 217)

**Para:** Equipo NominaApp / RRHH
**Version:** 1.8
**Fecha:** 2026-07-09
**Host:** Server 217 / FastAPI (`extract-api`)

## Contexto

> **ROTACION 2026-05-08**: token rotado. El anterior `ri_2a81e2f4...` (id=9)
> fue REVOCADO y devuelve HTTP 403 "Invalid API key". El nuevo token comparte
> los mismos 8 scopes y vence 2027-05-10. Reemplaza tu `NOMINAAPP_API_KEY`.


Endpoints REST en el backend **api-actualizacion** (FastAPI + asyncio + pyodbc)
diseñados para consumo desde NominaApp y otros clientes internos. Cada request
ejecuta queries en paralelo sobre las BD `Nomina` de los 19 hoteles del grupo
configurados en `config.json`.

- **Base URL:** `https://api.innfinancial.cloud`
- **Autenticacion:** Header `X-API-Key: <token>`
- **Cache:** TTL 60s en memoria, llave por hash del body normalizado
- **Rate-limit:** 30 requests/minuto por API key
- **Multi-tenant:** Una sola request consulta TODOS los hoteles disponibles

> **Advertencia de Seguridad:** El token nunca debe ir en código fuente, commits
> ni logs. Guardarlo en variable de entorno o secret manager.

## Detalles del Token

- **Owner:** NominaApp - Producción
- **Prefijo:** `ri_60415`
- **ID interno:** 9
- **Application:** `nominaapp`
- **Scopes Activos:**
  - `nomina:empleados-activos:read`
  - `nomina:reporte:read`
  - `rrhh:empleados:read`
  - `rrhh:ingresos-retiros:read` *(agregado 2026-04-23)*
  - `reportes:pms:forecast:read` *(documentado v1.6)*
  - `reportes:pms:auditoria-estadisticas:read` *(documentado v1.6)*
  - `reportes:pms:huespedes-segmentacion:read` *(documentado v1.6)*
  - `reportes:pms:reservas:read` *(documentado v1.6)*
- **Vencimiento:** 2027-05-10

---

## Endpoint 1 — Empleados Activos (sumario)

**`GET /api/v1/nomina/empleados-activos`**

Cuenta de empleados activos por hotel. Útil para dashboards.

```bash
curl -H "X-API-Key: $TOKEN" \
  https://api.innfinancial.cloud/api/v1/nomina/empleados-activos
```

---

## Endpoint 2 — Empleados Activos con Detalle Maestro

**`GET /api/v1/rrhh/empleados-activos-detalle?hotel_nit={nit}&agrupar_bu={bool}`**

Planta activa por hotel con datos maestros: identificación, nombre, fecha
nacimiento, sexo, ingreso, vencimiento contrato, centro de costo, cargo, sueldo,
**`bu_codigo` y `bu_nombre`** (v1.4) y **`contrato`** (v1.5, código del contrato
en Zeus — puede tener sufijos tipo `1031803359-1` cuando un empleado tuvo varios contratos).

### Query params

| Param | Tipo | Default | Descripción |
|---|---|---|---|
| `hotel_nit` | string | — | Opcional. Si se omite, devuelve todos los hoteles. |
| `agrupar_bu` | boolean | `false` | Si `true`, agrupa empleados por Unidad de Negocio dentro de cada hotel. |

### Ejemplos

```bash
# Planta plana (por defecto) — cada empleado trae bu_codigo y bu_nombre
curl -H "X-API-Key: $TOKEN" \
  "https://api.innfinancial.cloud/api/v1/rrhh/empleados-activos-detalle?hotel_nit=900345450"

# Misma planta agrupada por BU (BE / DI / Local / SIN_BU)
curl -H "X-API-Key: $TOKEN" \
  "https://api.innfinancial.cloud/api/v1/rrhh/empleados-activos-detalle?hotel_nit=900345450&agrupar_bu=true"
```

### Estructura con `agrupar_bu=true`

```json
{
  "grand_activos": 38,
  "hoteles": [{
    "hotel_nit":  "900345450",
    "hotel_name": "Faranda_Bogota",
    "activos":    38,
    "unidades_negocio": [
      {
        "bu_codigo": "BE",
        "bu_nombre": "BELVEDERE",
        "total_empleados": 15,
        "empleados": [ { "identificacion": "...", "nombre": "...", "bu_codigo": "BE", "bu_nombre": "BELVEDERE", ... } ]
      },
      { "bu_codigo": "Local", "bu_nombre": "CLUB HOUSE", "total_empleados": 23, "empleados": [ ... ] }
    ]
  }]
}
```

Empleados sin BU asignada caen en `bu_codigo: "SIN_BU"`, `bu_nombre: "Sin Unidad Asignada"`.
Orden de BUs: alfabético por código.

**Cache**: TTL 60s, llave incluye `agrupar_bu` (request sin agrupar no interfiere con request agrupado).

---

## Endpoint 3 — Reporte de Nómina por Empleado

**`POST /api/v1/nomina/reporte`**

Devengos, deducciones y conceptos de nómina detallados por empleado en un
periodo (año + mes). Incluye totales y desglose por código de concepto.

Cuando `incluir_empleados=true`, cada objeto del array `empleados[]` incluye
desde **v1.5**:

| Campo | Tipo | Descripción |
|---|---|---|
| `contrato` | string | Código del contrato en Zeus (`Nm_Contrato.Codigo`). Puede tener sufijos `-N` |
| `bu_codigo` | string | Código de Unidad de Negocio (BE / DI / Local / etc). Default `Local` si vacío |
| `bu_nombre` | string | Nombre de la BU (BELVEDERE / DON IGNAZIO / CLUB HOUSE / etc). Default `Local` |

Resto de campos: `identificacion`, `nombre`, `fecha_nacimiento`, `sexo`,
`fecha_ingreso`, `fecha_retiro`, `s_basico`, `centro_costo`, `cargo`, `dias`,
`total_devengos`, `total_deducciones`, `aportes_patronales`, `neto`, `conceptos[]`.

```bash
curl -X POST -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" \
  -d '{"hotel_nit":"890404970","anio":2025,"mes":11,"incluir_empleados":true}' \
  https://api.innfinancial.cloud/api/v1/nomina/reporte
```

Ejemplo de empleado en la respuesta (Faranda_Bogota nov 2025):

```json
{
  "identificacion": "1033767825",
  "nombre": "SOLER GOMEZ YEISON HUMBERTO",
  "contrato": "1033767825",
  "bu_codigo": "Local",
  "bu_nombre": "CLUB HOUSE",
  "centro_costo": "0201-RESTAURANTE",
  "cargo": "MESERO",
  "dias": 15,
  "total_devengos": 875000.0,
  "total_deducciones": 35000.0,
  "neto": 840000.0,
  "conceptos": [ ... ]
}
```

> **Nota sobre contratos múltiples**: si un empleado tuvo varios contratos en
> el mismo período, el campo `contrato` muestra `MAX(Codigo)` del agregado.
> Si necesitas el detalle por contrato, usa `/rrhh/empleados-activos-detalle`
> que devuelve un registro por contrato vigente.

---

## Endpoint 4 — Ingresos y Retiros (NUEVO v1.3)

**`POST /api/v1/rrhh/ingresos-retiros`**

Replica el reporte Zeus desktop **Reportes → Movimientos → Ingresos y/o Retiros**.
Devuelve la lista de empleados que ingresaron, se retiraron o ambos en un rango
de fechas, **paralelizado en TODAS las BD `Nomina` configuradas**.

Mapea uno-a-uno los parámetros del SP `SpRpt_NmIngresosyRetiros @Op='S'`.

### Body mínimo (recomendado — v1.8)

```json
{
  "fecha_inicial": "2025-01-01",
  "fecha_final":   "2025-12-31",
  "estado": "T"
}
```

> **Contrato recomendado: OMITE `tipo_nomina_id`.** Desde v1.8 el campo es
> **opcional**; al omitirlo (o enviar `0`/`null`) la API consulta **TODOS** los
> tipos de nómina (Semanal…Mensual) y **une** los resultados por hotel. Así el
> cliente **no necesita saber qué tipo usa cada hotel** — que es distinto entre
> ellos (p.ej. **Aloft es MENSUAL**, la mayoría es Quincenal). Con un solo request
> traes ingresos/retiros de todos los hoteles sin importar su tipo de nómina.
>
> Esta es la forma en que **el 217 define el contrato**; NominaApp debe adaptarse
> a él (omitir el campo), no al revés.

| Campo | Tipo | Descripción |
|---|---|---|
| `tipo_nomina_id` | int / null | **Opcional.** `null`/omitido/`0` = todos los tipos (recomendado). Un int concreto (1–5, ver tabla) filtra solo ese tipo — comportamiento legacy |
| `fecha_inicial` | date | YYYY-MM-DD |
| `fecha_final` | date | YYYY-MM-DD |
| `estado` | string | `T`=Todos, `I`=Solo Ingresos, `R`=Solo Retiros |

### Body completo opcional

```json
{
  "tipo_nomina_id": 4,
  "fecha_inicial": "2025-01-01",
  "fecha_final":   "2025-12-31",
  "estado": "T",
  "agrupar": {"cco": false, "bu": false, "cla": false, "causa": false, "cargo": false},
  "saltos":  {"cco": false, "bu": false, "cla": false, "causa": false, "cargo": false},
  "filtros": {
    "contrato":      ["", "zzz"],
    "empleado":      ["", "zzz"],
    "centro_costo":  ["", "zzz"],
    "bu":            ["", "zzz"],
    "clasificacion": ["", "zzz"],
    "cargo":         ["", "zzz"],
    "nivel":         ["", "zzz"],
    "causa_retiro":  ["", "zzz"]
  },
  "orden": ["C", "N", "I"],
  "forma_pago": 999,
  "hoteles": ["900115137", "890404970"]
}
```

**Códigos de orden** (`orden[1..3]`): `I`=Identificación, `N`=Nombre,
`C`=Contrato, `G`=Cargo, `T`=Categoría/Clasificación, `O`=Centro Costo, `B`=BU.
Default: `["C","N","I"]`.

**`hoteles`**: lista opcional de NITs o `hotel_name`. Si se omite, consulta
todos los hoteles con `nomina_db` configurada.

**`forma_pago`**: 999 = Todos.

### Tabla de IDs Nm_PeriodoTipo

| IDEN | Tipo |
|---|---|
| 1 | Semanal |
| 2 | Bisemanal |
| 3 | Decadal |
| 4 | Quincenal |
| 5 | Mensual |

> **Solo relevante si envías un `tipo_nomina_id` concreto.** El tipo de nómina
> difiere por hotel: **Aloft (NIT 900385136) corre MENSUAL (5)**, la mayoría
> corre Quincenal (4). Por eso un request con un tipo fijo devuelve
> `rows_count: 0` en los hoteles que usan otro tipo (fue el caso reportado:
> Aloft con `tipo_nomina_id: 4` daba 0; con `5` da los 10 reales).
>
> **Recomendación: no envíes `tipo_nomina_id`** y deja que la API una todos los
> tipos. Solo fíjalo si necesitas aislar un tipo puntual.

### Respuesta

```json
{
  "request": { "...echo del body normalizado..." },
  "elapsed_seconds": 5.21,
  "grand_total_rows": 961,
  "hoteles_ok":    18,
  "hoteles_error":  1,
  "hoteles": [
    {
      "hotel_nit":  "900115137",
      "hotel_name": "Hotel_Bantu",
      "rows_count": 19,
      "rows": [
        {
          "compania":              "BANTU HOTELS S.A.S.",
          "titulo":                "Relación de Ingresos y Retiros por Rango de Fecha",
          "rango":                 "Desde el 01 de enero de 2025 hasta el 31 de diciembre de 2025",
          "contrato":              "1001937454",
          "nombres":               "MARRUGO PUELLO RICARDO JOSE",
          "tipo_contrato":         "Termino fijo",
          "sueldo":                1423500.0,
          "cod_cargo":             "CAM",
          "cargo":                 "CAMARERA",
          "cuenta":                "...",
          "ingreso":               "2025-02-03",
          "vencimiento":           "2026/02/03",
          "codigo_cco":            "...",
          "nombre_cco":            "...",
          "clasificacion":         "",
          "clasificacion_nombre":  "",
          "bu":                    "",
          "bu_nombre":             "",
          "centro_costo":          "",
          "centro_costo_nombre":   "",
          "cargo_codigo":          "",
          "cargo_nombre":          "",
          "causa_retiro":          "Vencimiento de Contrato",
          "causa_agrupa":          "",
          "cod_causa_retiro":      "01",
          "tipo":                  "I"
        }
      ]
    }
  ],
  "errores": [
    {"hotel_nit":"901423747","hotel_name":"Hotel_Marques","error":"Login timeout expired"}
  ]
}
```

**Campo `tipo`**:
- `I` = solo ingreso en el rango (FechaInicio en rango)
- `R` = solo retiro en el rango (FechaRetiro en rango)
- `IR` = ingresó Y se retiró en el mismo rango

### Ejemplos curl

```bash
# RECOMENDADO (v1.8): SIN tipo_nomina_id -> une todos los tipos, todos los hoteles
curl -X POST -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" \
  -d '{"fecha_inicial":"2026-06-01","fecha_final":"2026-06-30","estado":"T"}' \
  https://api.innfinancial.cloud/api/v1/rrhh/ingresos-retiros

# RECOMENDADO: un hotel cualquiera sin importar su tipo (Aloft = mensual, sale igual)
curl -X POST -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" \
  -d '{"fecha_inicial":"2026-06-01","fecha_final":"2026-06-30","estado":"T","hoteles":["900385136"]}' \
  https://api.innfinancial.cloud/api/v1/rrhh/ingresos-retiros

# Legacy: filtrar por un tipo concreto (Quincenal Bantu) - año 2025 completo
curl -X POST -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" \
  -d '{"tipo_nomina_id":4,"fecha_inicial":"2025-01-01","fecha_final":"2025-12-31","estado":"T","hoteles":["900115137"]}' \
  https://api.innfinancial.cloud/api/v1/rrhh/ingresos-retiros

# Solo retiros mensual de TODOS los hoteles para Q4 2025
curl -X POST -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" \
  -d '{"tipo_nomina_id":5,"fecha_inicial":"2025-10-01","fecha_final":"2025-12-31","estado":"R"}' \
  https://api.innfinancial.cloud/api/v1/rrhh/ingresos-retiros

# Ingresos quincenales con agrupación por centro de costo
curl -X POST -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" \
  -d '{"tipo_nomina_id":4,"fecha_inicial":"2025-01-01","fecha_final":"2025-12-31","estado":"I","agrupar":{"cco":true}}' \
  https://api.innfinancial.cloud/api/v1/rrhh/ingresos-retiros

# NUEVO v1.4: agrupar por Unidad de Negocio (BU) — Faranda_Bogota 2025 completo
curl -X POST -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" \
  -d '{"tipo_nomina_id":4,"fecha_inicial":"2025-01-01","fecha_final":"2025-12-31","estado":"T","agrupar":{"bu":true},"hoteles":["900345450"]}' \
  https://api.innfinancial.cloud/api/v1/rrhh/ingresos-retiros
```

### Estructura con `agrupar.bu=true` (NUEVO v1.4)

Cuando se pide `agrupar.bu=true`, cada hotel devuelve `unidades_negocio[]` en
lugar de `rows[]`. Cada fila incluye `bu_codigo` y `bu_nombre`.

```json
{
  "hoteles": [{
    "hotel_nit":  "900345450",
    "hotel_name": "Faranda_Bogota",
    "rows_count": 66,
    "unidades_negocio": [
      {
        "bu_codigo": "BE",
        "bu_nombre": "BELVEDERE",
        "total": 36,
        "rows": [
          { "contrato": "...", "nombres": "...", "bu_codigo": "BE", "bu_nombre": "BELVEDERE", "tipo": "R", "..." }
        ]
      },
      { "bu_codigo": "Local", "bu_nombre": "CLUB HOUSE", "total": 30, "rows": [...] }
    ]
  }]
}
```

Orden alfabético por `bu_codigo`. Filas sin BU caen en `bu_codigo: "SIN_BU"`.

**Importante**: el SP Zeus `SpRpt_NmIngresosyRetiros` solo incluye la columna
`BU` en el SELECT cuando `@AgruparBU='S'`. Por tanto, **para obtener `bu_codigo`
en cada fila debes usar `agrupar.bu=true`**. Sin ese flag, `bu` y `bu_codigo`
vienen vacíos (comportamiento del SP, no del endpoint).

### Notas operacionales

- **Tiempos típicos** (multi-hotel, año completo): ~5s primer hit, <1s segundo hit (cache 60s).
- **Hoteles sin `nomina_db`**: omitidos silenciosamente (no salen en `hoteles[]` ni `errores[]`).
- **Hotel_Marques**: timeout recurrente conocido (problema de red, no del código). Aparece en `errores[]`.
- **Hotel_Aloft**: BD activa pero a veces no devuelve filas en periodos sin movimiento; sale en `hoteles[]` con `rows_count: 0`.
- **Multi-resultset**: el SP a veces devuelve resultsets vacíos antes del SELECT real; el servicio recorre con `cursor.nextset()` hasta encontrar el dataset con `description`.

---

---

## Reportes PMS (Endpoints 5-8)

Replican reportes del PMS Zeus (SQL Server zeus_xxx) en JSON. **Solo aplican
sobre los 3 hoteles del forecast valorizado**:

| `hotel_key` | `hotel_name` | NIT |
|---|---|---|
| `casa_canabal` | Casa Canabal | `900361237` |
| `fc_cucuta` | FC Cucuta | `901936953` |
| `la_mantilla` | La Mantilla | `900024971` |

Si `hoteles` se omite, consulta los 3 en paralelo. Acepta también `hotel_name`
o `hotel_nit` en el array.

Estructura común de respuesta:

```json
{
  "request":          { "...echo del body normalizado..." },
  "elapsed_seconds":  1.21,
  "grand_total_rows": 488,
  "hoteles_ok":       1,
  "hoteles_error":    0,
  "hoteles": [
    {
      "hotel_key":     "casa_canabal",
      "hotel_name":    "Casa Canabal",
      "hotel_nit":     "900361237",
      "zeus_database": "Zeus_CC",
      "rows_count":    488,
      "rows":          [ ... ]
    }
  ],
  "errores": []
}
```

---

## Endpoint 5 — Forecast Valorizado Histórico

**`POST /api/v1/reportes/pms/forecast`**

Replica el reporte que el cron envía cada noche por email a directores de
revenue. Mismo SP `Web.SpHo_Rpt_Fcvalorizado`, sin tocar la lógica Excel —
aquí JSON estructurado por hotel.

**Defaults** (cuando faltan en el body):
- `hoteles`: los 3 hoteles
- `fecha_inicio`: primer día del mes actual (YYYYMMDD)
- `fecha_fin`: último día del mes a +15 meses (YYYYMMDD)

### Body (puede ir vacío `{}`)

```json
{
  "hoteles": ["casa_canabal", "fc_cucuta", "la_mantilla"],
  "fecha_inicio": "20260501",
  "fecha_fin":    "20270831"
}
```

| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| `hoteles` | array | No | hotel_key/name/nit. Default: los 3 |
| `fecha_inicio` | string | No | YYYYMMDD |
| `fecha_fin` | string | No | YYYYMMDD |

### Estructura de cada `row` (un registro por fecha por hotel)

| Campo | Tipo | Descripción |
|---|---|---|
| `fecha` | date | Día de proyección |
| `rnights` | float | Room nights ocupadas |
| `bloq` | float | Habitaciones bloqueadas |
| `dispon` | float | Disponibles |
| `compli` | float | Cortesías |
| `house_use` | float | Uso casa |
| `total_habs` | float | Total habitaciones del día |
| `individuales` | float | Reservas individuales |
| `grupos` | float | Reservas grupos |
| `adultos` | float | Adultos esperados |
| `ninos` | float | Niños esperados |
| `check_in` | float | Check-ins esperados |
| `day_use` | float | Day use |
| `no_shows` | float | No shows previstos |
| `check_out` | float | Check-outs esperados |
| `tarifa_promedio` | float | ADR (COP) |
| `porc_ocupacion` | float | % ocupación |
| `revpar` | float | RevPAR (COP) |
| `revenue` | float | Revenue del día (COP) |

### Ejemplos

```bash
curl -X POST -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" \
  -d '{}' \
  https://api.innfinancial.cloud/api/v1/reportes/pms/forecast
```

```python
import os, requests
URL = "https://api.innfinancial.cloud/api/v1/reportes/pms/forecast"
HEADERS = {"X-API-Key": os.environ["NOMINAAPP_API_KEY"], "Content-Type": "application/json"}
r = requests.post(URL, json={"hoteles": ["casa_canabal"]}, headers=HEADERS, timeout=60)
r.raise_for_status()
for hotel in r.json()["hoteles"]:
    for row in hotel["rows"]:
        print(row["fecha"], row["porc_ocupacion"], row["revenue"])
```

Cache 60s · Rate limit 30/min · Scope `reportes:pms:forecast:read`.

---

## Endpoint 6 — Auditoría / Revisar Estadísticas

**`POST /api/v1/reportes/pms/auditoria-estadisticas`**

Replica la pantalla **Auditoría → Estadísticas → Revisar Estadísticas** de
Zeus Front. SP `Web.SpHo_Rpt_PgInformeRevisarEstadisticas`. Devuelve detalle
de check-ins/registros del rango.

### Body

```json
{
  "hoteles": ["casa_canabal"],
  "fecha_inicial": "2026-03-01",
  "fecha_final":   "2026-03-31",
  "filtrar_por":   "todos",
  "codigo_filtro": "",
  "incluir_pais":  false,
  "orden":         "fecha",
  "page_size":     50,
  "page_number":   1
}
```

| Campo | Tipo | Default | Descripción |
|---|---|---|---|
| `hoteles` | array | null=los 3 | hotel_key/name/nit |
| `fecha_inicial` | date | **requerido** | YYYY-MM-DD |
| `fecha_final` | date | **requerido** | YYYY-MM-DD |
| `filtrar_por` | string | `todos` | `todos`/`oficina`/`agencia`/`empresa`/`grupo`/`pais` (o letra T/O/A/E/G/P) |
| `codigo_filtro` | string | `""` | Código del filtro cuando `filtrar_por != todos` |
| `incluir_pais` | bool | `false` | Si true, incluye código país |
| `orden` | string | `fecha` | `fecha`/`nombre`/`agencia`/`empresa`/`oficina`/`habitacion` |
| `page_size` | int | `50` | 1-1000 |
| `page_number` | int | `1` | 1-based |

### Estructura `row`

Campos clave: `id_estadistica`, `nombre`, `fecha`, `registro`, `num_reserva`,
`codigo_oficina`/`nombre_oficina`, `codigo_agencia`/`nombre_agencia`,
`codigo_empresa`/`nombre_empresa`, `codigo_plaza`, `ciudad_codigo`/`ciudad_nombre`,
`motivo_viaje_codigo`/`motivo_viaje`, `codigo_grupo`/`nombre_grupo`,
`habitacion`, `tipo_habitacion`, `clase_habitacion`, `adultos`, `ninos`,
`valor_habitacion`, `valor_consumo`, `fecha_check_in`, `fecha_check_out`,
`num_habitacion`, `identificacion`, `tipo_documento`, `codigo_pais`, `folio`,
`ejecutivo_empresa`.

### Ejemplo

```bash
curl -X POST -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" \
  -d '{"hoteles":["casa_canabal"],"fecha_inicial":"2026-03-01","fecha_final":"2026-03-31"}' \
  https://api.innfinancial.cloud/api/v1/reportes/pms/auditoria-estadisticas
```

Cache 60s · Rate limit 30/min · Scope `reportes:pms:auditoria-estadisticas:read`.

---

## Endpoint 7 — Huéspedes por Segmentación y Empresas

**`POST /api/v1/reportes/pms/huespedes-segmentacion-empresas`**

Replica la pantalla **Huéspedes por Segmentación y Empresas** de Zeus PMS.
SP `dbo.Spestx_h`. Permite agrupar el negocio por empresa, agencia, segmento,
ciudad, oficina, total o usuario.

### Body

```json
{
  "hoteles": ["casa_canabal"],
  "fecha_inicial": "2026-03-01",
  "fecha_final":   "2026-03-31",
  "tipo_reporte":  "empresa",
  "excluir_planes_eventos": false,
  "codigo_filtro": "",
  "codigos_multiples": "",
  "house_use_comply": "",
  "resumido": false
}
```

| Campo | Tipo | Default | Descripción |
|---|---|---|---|
| `hoteles` | array | null=los 3 | hotel_key/name/nit |
| `fecha_inicial` | date | **requerido** | YYYY-MM-DD |
| `fecha_final` | date | **requerido** | YYYY-MM-DD |
| `tipo_reporte` | string | `empresa` | `empresa`/`agencia`/`segmento`/`ciudad`/`oficina`/`total`/`usuario` (o E/A/S/C/O/T/U) |
| `excluir_planes_eventos` | bool | `false` | Si true, excluye planes de eventos |
| `codigo_filtro` | string | `""` | Código específico de empresa/agencia para filtrar |
| `codigos_multiples` | string | `""` | Lista CSV: `"0001,0002,0003"` |
| `house_use_comply` | string | `""` | `""` todos / `"H"` House Use / `"C"` Comply / `"HC"` ambos |
| `resumido` | bool | `false` | Versión resumida |

### Estructura `row`

| Campo | Tipo | Descripción |
|---|---|---|
| `codigo` | string | Código del agrupador (empresa/agencia/etc) |
| `descripcion` | string | Nombre del agrupador |
| `noches` | int | Noches totales |
| `pax_total` | int | Total pasajeros |
| `valor_habitacion` | float | Revenue habitaciones (COP) |
| `valor_consumo` | float | Revenue consumos (COP) |
| `estadia_total` | int | Estadías totales |
| `estadia_adultos` | int | Estadías adultos |
| `estadia_ninos` | int | Estadías niños |
| `total_estadia` | int | Acumulado estadía |
| `nombre_segmento` | string | Segmento de mercado |
| `nombre_motivacion` | string | Motivación de viaje |
| `marca` | string | Marca asociada |

### Ejemplo

```bash
curl -X POST -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" \
  -d '{"fecha_inicial":"2026-03-01","fecha_final":"2026-03-31","tipo_reporte":"empresa"}' \
  https://api.innfinancial.cloud/api/v1/reportes/pms/huespedes-segmentacion-empresas
```

Cache 60s · Rate limit 30/min · Scope `reportes:pms:huespedes-segmentacion:read`.

---

## Endpoint 8 — Reservas por Fecha

**`POST /api/v1/reportes/pms/reservas-por-fecha`**

Replica la pantalla **Reservas por Fecha** de Zeus PMS. SP `dbo.spConsRes`.
Devuelve reservas con el detalle completo del Zeus (>120 campos por fila —
crudo del SP, sin reducir).

### Body

```json
{
  "hoteles": ["casa_canabal"],
  "fecha_inicial": "2026-04-01",
  "fecha_final":   "2026-04-30",
  "evaluar_por":   "entrada",
  "estado":        "todas",
  "complejo":      "",
  "filtro":        "",
  "orden":         "reserva",
  "via":           "",
  "medio":         "",
  "incluir_preferencias":      false,
  "incluir_quejas":            false,
  "incluir_objetos_olvidados": false,
  "resumen_multitarifa":       false,
  "agrupar_por":               "",
  "origen_complejo":           "L"
}
```

#### `evaluar_por` (cómo se interpreta el rango de fechas)

`entrada` (default · E) · `salida` (S) · `garantia` (G) · `elaboracion` (L) · `cancelacion` (C)

#### `estado` (filtra estado de la reserva)

| Valor | Código | Significado |
|---|---|---|
| `todas` | TD | Todas las reservas vigentes (default) |
| `confirmadas` | CF | Reservas confirmadas |
| `no_confirmadas` | NC | NO confirmadas |
| `lista_espera` | LE | En lista de espera |
| `canceladas` | CD | Canceladas |
| `por_cancelar` | PC | Por cancelar |
| `canceladas_corte` | CC | Canceladas por corte |
| `con_acompañantes` | AC | Huéspedes y acompañantes |
| `comisionables_sin_porcentaje` | SC | — |
| `garantizadas` | GR | — |
| `no_garantizadas` | NG | Sin depósito |
| `no_shows` | NS | No shows |
| `tarifas_diferentes` | DF | Tarifa diferente al plan |

#### `orden`

`reserva` (R · default) · `nombre` (N) · `fecha` (F) · `empresa` (E) · `agencia` (A) · `complejo` (C)

#### Otros params

| Campo | Tipo | Default | Descripción |
|---|---|---|---|
| `complejo` | string | `""` | Código del complejo |
| `filtro` | string | `""` | Filtro contextual según el estado |
| `via` / `medio` | string | `""` | Filtros vía/medio |
| `incluir_preferencias` | bool | `false` | Agrega `string_prefe` |
| `incluir_quejas` | bool | `false` | Agrega `string_quejas` |
| `incluir_objetos_olvidados` | bool | `false` | Agrega `string_objolvidados` |
| `resumen_multitarifa` | bool | `false` | Resúmenes multitarifa |
| `agrupar_por` | string | `""` | Categoría de agrupación |
| `origen_complejo` | string | `L` | `L`=línea reserva · `R`=reserva |

### Estructura `row`

El SP devuelve un dump completo de la reserva. Campos más usados:

| Campo | Descripción |
|---|---|
| `nreser_res` | Número de reserva |
| `linea_reh` | Línea de reserva (multi-room) |
| `fllega_reh` / `fsalid_reh` | Llegada / salida |
| `tiphab_tip` / `clahab_clh` | Tipo / clase habitación |
| `cantid_reh` | Habitaciones reservadas |
| `adulto_reh` / `ninos_reh` | Pax |
| `vlrp_reh` / `vlrus_reh` | Tarifa COP / USD |
| `estado_reh` | Estado actual |
| `fecha_elab` / `fecha_res` | Elaboración / fecha reserva |
| `nombre_res` | Nombre del huésped |
| `nombre_emp` / `codige_emp` | Empresa |
| `nombre_age` / `codiga_age` | Agencia |
| `nomofic` / `codofic` | Oficina |
| `noches` | Noches |
| `garantia` / `descri_garantia` | Garantía |
| `nacionalidad` / `nomnacionalidad` | Nacionalidad |
| `total_reservas` / `total_noches` / `total_personas` | Subtotales acumulados |

> **Nota**: el SP devuelve >120 columnas crudas (formato Zeus). El cliente
> puede filtrar las que necesita. Para resumen ejecutivo, considerar usar
> Endpoint 7 (huespedes-segmentacion) que ya viene agregado.

### Ejemplo

```bash
curl -X POST -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" \
  -d '{"fecha_inicial":"2026-04-01","fecha_final":"2026-04-30","evaluar_por":"entrada","estado":"todas"}' \
  https://api.innfinancial.cloud/api/v1/reportes/pms/reservas-por-fecha
```

Cache 60s · Rate limit 30/min · Scope `reportes:pms:reservas:read`.

---

## Verificación de salud

```bash
curl -s -o /dev/null -w "HTTP=%{http_code}\n" \
  https://api.innfinancial.cloud/health
```

## Errores comunes

| HTTP | Causa | Fix |
|---|---|---|
| 401 | API key inválida | Verificar header `X-API-Key` |
| 403 | Scope faltante | Verificar que el token tenga `rrhh:ingresos-retiros:read` |
| 422 | Body Pydantic inválido | `tipo_nomina_id` (si se envía) debe ser int; fechas en `YYYY-MM-DD` |
| `rows_count: 0` inesperado | Enviaste `tipo_nomina_id` de un tipo que ese hotel no usa | **Omite `tipo_nomina_id`** para unir todos los tipos (v1.8) |
| 429 | Rate-limit | Esperar 60s o reducir frecuencia |
| 500 | Error interno | Ver logs `docker logs extract-api` |

## Changelog

| Versión | Fecha | Cambios |
|---|---|---|
| 1.8 | 2026-07-09 | `/rrhh/ingresos-retiros`: `tipo_nomina_id` ahora **opcional** — omitido/`null`/`0` = une TODOS los tipos de nómina por hotel. Resuelve `rows_count:0` cuando el hotel usa otro tipo (Aloft=Mensual). **Contrato recomendado: omitir el campo.** Backward-compat: int concreto = comportamiento legacy |
| 1.7 | 2026-05-08 | **ROTACION**: token id=9 `ri_2a81e...` revocado, nuevo token id=11 `ri_60415...` con mismos 8 scopes (vence 2027-05-10) |
| 1.6 | 2026-05-08 | + 4 endpoints PMS documentados (`forecast`, `auditoria-estadisticas`, `huespedes-segmentacion-empresas`, `reservas-por-fecha`); 4 scopes ya activos en el token |
| 1.5 | 2026-05-07 | + `contrato` en `/rrhh/empleados-activos-detalle`; + `contrato`, `bu_codigo`, `bu_nombre` en `empleados[]` de `/nomina/reporte` (default `Local` si BU vacío) |
| 1.4 | 2026-04-24 | + `agrupar_bu` en `/rrhh/empleados-activos-detalle` con `unidades_negocio[]`; + `bu_codigo`/`bu_nombre` en cada empleado; + `agrupar.bu=true` en `/rrhh/ingresos-retiros` con `unidades_negocio[]` y `bu_codigo` en cada fila |
| 1.3 | 2026-04-23 | + Endpoint `POST /rrhh/ingresos-retiros` + scope `rrhh:ingresos-retiros:read` |
| 1.2 | 2026-04-22 | + Endpoint `POST /nomina/reporte` con `incluir_empleados`, + `GET /rrhh/empleados-activos-detalle`, rate-limit + cache 60s |
| 1.1 | 2026-04-21 | Fix `empleados-activos` SQL (Nm_Empleado JOIN Nm_Contrato) |
| 1.0 | 2026-04-20 | Versión inicial: scopes granulares, prefijo `ri_` para NominaApp |
