# Instalacion del job de cierres en el servidor de la API

## Control del documento

| Campo | Valor |
|---|---|
| Proyecto | Dashboard Talento Humano - Faranda Hotels |
| Repositorio | `https://git.faranda.online/faranda/rrhh-report.git` |
| Rama de produccion | `main` |
| Base historica destino | `Base de Datos Historicos Talento Humano` |
| Servidor PostgreSQL | `172.19.0.160:5432` |
| Schema | `public` |
| Zona horaria funcional | `America/Bogota` |
| Fecha de documentacion | 2026-07-24 |
| Estado | Listo para instalacion controlada |

## 1. Objetivo

Instalar los cierres mensuales de Talento Humano en el servidor original donde
se encuentra la API de nomina, para que:

- la ejecucion no dependa de que la Mac este encendida;
- los cierres se escriban en PostgreSQL remoto `172.19.0.160`;
- el dashboard consulte PostgreSQL para los periodos historicos ya cerrados;
- las llaves de API y la clave de PostgreSQL no queden en Git ni en comandos
  visibles;
- exista un procedimiento verificable de instalacion, certificacion,
  operacion, actualizacion y recuperacion.

Este documento esta pensado para el administrador del servidor y para la
persona responsable de certificar los cierres.

## 2. Resultado esperado

La arquitectura final es:

```text
GET/POST NominaApp local o interno ---------+
                                             |
POST P&G Faranda ----------------------------+--> job cron en servidor API
                                                       |
                                                       v
                                        PostgreSQL 172.19.0.160
                                  Base de Datos Historicos Talento Humano
                                                       |
                                                       v
                                              Dashboard Talento Humano
```

El servidor de la API ejecuta los jobs. Los datos historicos no se almacenan
en la Mac y el dashboard no depende de la Mac para consultarlos.

## 3. Aclaracion sobre el consumo de API

La implementacion actual no es una integracion directa base-a-base.

Los jobs siguen consumiendo las APIs durante el cierre:

| Proceso | Fuente actual | Destino |
|---|---|---|
| Empleados del dia 1 | `GET /rrhh/empleados-activos-detalle` | `historico_empleados` |
| Ingresos y retiros | `POST /rrhh/ingresos-retiros` | `historico_ir` |
| Nomina | `POST /nomina/reporte` | `historico_nomina_pyg` |
| P&G | API `reportePyg/comparativo` | `historico_nomina_pyg` |

La mejora consiste en que esas llamadas quedan concentradas en las fechas de
cierre y se ejecutan desde un servidor permanente. Para meses historicos, el
dashboard consulta PostgreSQL.

Los datos del mes vivo pueden seguir consultando la API hasta que el periodo
quede almacenado como historico. Para eliminar absolutamente todas las
llamadas a API se requeriria otro desarrollo: acceso de solo lectura a las
tablas operacionales de la API, vistas acordadas con su propietario y
reemplazo de los clientes HTTP por consultas SQL. Ese cambio no forma parte
del job documentado aqui.

## 4. Logica funcional de los cierres

### 4.1 Dia 1 a las 5:00 AM

Comando:

```bash
python3 cierre_mes.py --empleados-activos-detalle
```

Comportamiento:

1. Calcula automaticamente el mes anterior.
2. Consulta la fotografia viva de empleados activos del dia 1.
3. Guarda esos empleados con fecha del primer dia del mes anterior.
4. Actualiza ingresos y retiros del mes anterior.

Ejemplo:

```text
Ejecucion: 1 de agosto de 2026
Fotografia consultada: empleados activos el 1 de agosto
Periodo historico asignado: julio de 2026
fecha almacenada: 2026-07-01
```

### 4.2 Dias 5 y 7 a las 5:00 AM

Comando:

```bash
python3 cierre_mes.py --solo-empleados-si-vacio
```

Comportamiento:

- si el cierre de empleados del dia 1 existe, lo conserva;
- si el periodo de empleados esta vacio, intenta llenarlo;
- ingresos y retiros se vuelven a consultar y se reemplazan mediante
  `DELETE + INSERT`.

Los dias 5 y 7 son recuperaciones controladas, no reemplazos normales de la
fotografia de empleados tomada el dia 1.

### 4.3 Dias 1, 5 y 7 a las 5:30 AM

Comando:

```bash
python3 cargar_historico_nomina_pyg.py --mes-anterior --fuente ambas
```

Comportamiento:

- carga NominaApp para el mes anterior;
- carga P&G para el mes anterior;
- actualiza `historico_nomina_pyg`;
- registra el estado por fuente, empresa o unidad de negocio en
  `historico_nomina_pyg_control`.

## 5. Requisitos del servidor

### 5.1 Sistema

- Linux con disponibilidad permanente.
- `git`.
- Python 3.10 o superior.
- soporte para entornos virtuales de Python.
- `cron` o `crond`.
- acceso administrativo inicial para crear el usuario tecnico.
- espacio para el repositorio y logs.

### 5.2 Red

| Origen | Destino | Puerto | Proposito |
|---|---|---:|---|
| Servidor API | `172.19.0.160` | 5432 | Escritura de historicos |
| Servidor API | Git Faranda | 443 | Descargar y actualizar codigo |
| Servidor API | API de nomina | Puerto interno o 443 | Cierres de empleados, IR y nomina |
| Servidor API | `app.faranda.online` | 443 | Cierre P&G |

PostgreSQL no debe quedar abierto a internet. La regla de firewall y
`pg_hba.conf` debe permitir exclusivamente la IP interna del servidor que
ejecutara los cierres.

### 5.3 Accesos

- credencial Git de despliegue con lectura del repositorio;
- usuario PostgreSQL dedicado para cierres;
- `NOMINA_API_KEY` rotada y vigente;
- `PYG_API_KEY` rotada y vigente;
- permiso para instalar el crontab del usuario tecnico.

## 6. Revision previa del servidor

Ejecutar sin modificar el sistema:

```bash
cat /etc/os-release
date
timedatectl status
python3 --version
git --version
crontab -l
nc -vz 172.19.0.160 5432
```

Registrar unicamente:

- distribucion y version del sistema;
- version de Python;
- zona horaria;
- resultado de conectividad al puerto 5432;
- existencia de otros cron jobs.

No copiar secretos ni respuestas de las APIs en el registro de instalacion.

Si `nc` no esta instalado, la conectividad tambien se certifica durante el
`--dry-run`.

## 7. Zona horaria

Los horarios funcionales estan definidos en `America/Bogota`.

El instalador actual usa la hora local del servidor. Antes de instalar se debe
confirmar:

```bash
date
timedatectl status
```

Si el servidor opera en hora Colombia, se usan los horarios incluidos en el
instalador:

```text
05:00 empleados e IR
05:30 nomina y P&G
```

Si el servidor opera en UTC y no se puede cambiar su zona horaria:

```text
10:00 UTC = 05:00 Colombia
10:30 UTC = 05:30 Colombia
```

No cambiar la zona horaria completa del servidor sin autorizacion del
responsable de la API, porque puede afectar otros procesos.

## 8. Usuario tecnico del sistema operativo

Crear un usuario exclusivo. No ejecutar los cierres como `root`.

```bash
sudo useradd --system --create-home \
  --home-dir /var/lib/rrhhjobs \
  --shell /bin/bash rrhhjobs

sudo mkdir -p /opt/rrhh-report
sudo chown rrhhjobs:rrhhjobs /opt/rrhh-report
```

Comprobar:

```bash
id rrhhjobs
ls -ld /opt/rrhh-report
```

## 9. Usuario PostgreSQL de cierres

Se recomienda el rol `th_cierre_writer`. La clave debe generarse en el
gestor de secretos o por el DBA y nunca almacenarse en esta documentacion.

La estructura de tablas debe haber sido creada previamente por el propietario
de la base. El usuario de ejecucion no necesita permiso `CREATE`. El comando
`cargar_historico_nomina_pyg.py --init-db` queda reservado para una
inicializacion administrada y no se usa en los cierres mensuales.

Como propietario o administrador de PostgreSQL:

```sql
CREATE ROLE th_cierre_writer LOGIN;

GRANT CONNECT
ON DATABASE "Base de Datos Historicos Talento Humano"
TO th_cierre_writer;
```

En `psql`, asignar la clave mediante el prompt seguro para no dejarla escrita
en el historial SQL:

```text
\password th_cierre_writer
```

Conectado a la base `Base de Datos Historicos Talento Humano`:

```sql
GRANT USAGE ON SCHEMA public TO th_cierre_writer;

GRANT SELECT
ON TABLE
    public.empresas,
    public.hotel_unificacion,
    public.socios
TO th_cierre_writer;

GRANT SELECT, INSERT, UPDATE, DELETE
ON TABLE
    public.historico_empleados,
    public.historico_ir,
    public.historico_nomina_pyg,
    public.historico_nomina_pyg_control
TO th_cierre_writer;

GRANT USAGE, SELECT
ON ALL SEQUENCES IN SCHEMA public
TO th_cierre_writer;
```

El dashboard debe utilizar un usuario diferente, por ejemplo
`th_dashboard_reader`, con `CONNECT`, `USAGE` y `SELECT`, sin permisos de
escritura.

## 10. Descarga del repositorio

Configurar primero una credencial Git de despliegue para `rrhhjobs`. Luego:

```bash
sudo -u rrhhjobs git clone \
  https://git.faranda.online/faranda/rrhh-report.git \
  /opt/rrhh-report

sudo -u rrhhjobs git -C /opt/rrhh-report checkout main
sudo -u rrhhjobs git -C /opt/rrhh-report status
sudo -u rrhhjobs git -C /opt/rrhh-report log -1 --oneline
```

El estado debe indicar la rama `main` sin modificaciones locales.

## 11. Entorno virtual y dependencias

```bash
sudo -u rrhhjobs python3 -m venv /opt/rrhh-report/.venv

sudo -u rrhhjobs \
  /opt/rrhh-report/.venv/bin/python -m pip install --upgrade pip

sudo -u rrhhjobs \
  /opt/rrhh-report/.venv/bin/pip install \
  psycopg2-binary pandas openpyxl
```

Verificar:

```bash
sudo -u rrhhjobs \
  /opt/rrhh-report/.venv/bin/python -c \
  "import psycopg2, pandas, openpyxl; print('dependencias OK')"
```

## 12. Archivo privado de configuracion

Crear el archivo:

```bash
sudo -u rrhhjobs touch /opt/rrhh-report/.env
sudo chmod 600 /opt/rrhh-report/.env
sudo -u rrhhjobs nano /opt/rrhh-report/.env
```

Plantilla:

```dotenv
DATABASE_URL=postgresql://th_cierre_writer:[PASSWORD_URL_ENCODED]@172.19.0.160:5432/Base%20de%20Datos%20Historicos%20Talento%20Humano
PG_SCHEMA=public

NOMINA_API_BASE=http://127.0.0.1:[PUERTO_API]/api/v1
NOMINA_API_KEY=[SECRETO_NOMINA]

PYG_API_URL=https://app.faranda.online/rm/reportePyg/comparativo/
PYG_API_KEY=[SECRETO_PYG]

APP_VERIFY_SSL=true
APP_DEBUG_ERRORS=false
```

Reglas:

- reemplazar todos los valores entre corchetes;
- codificar caracteres especiales de usuario, password y nombre de base en la
  URL;
- usar `NOMINA_API_BASE` por loopback solamente si la API esta publicada en el
  host;
- si la API esta en un contenedor sin puerto publicado, usar una direccion
  interna alcanzable desde el host o el dominio HTTPS;
- si PostgreSQL soporta TLS, agregar `?sslmode=require` a `DATABASE_URL`;
- mantener `APP_VERIFY_SSL=true`;
- no reutilizar el usuario superadministrador de PostgreSQL;
- no guardar el `.env` en Git.

Verificar propietario y permisos:

```bash
sudo chown rrhhjobs:rrhhjobs /opt/rrhh-report/.env
sudo chmod 600 /opt/rrhh-report/.env
sudo stat -c '%U %G %a %n' /opt/rrhh-report/.env
```

Resultado esperado:

```text
rrhhjobs rrhhjobs 600 /opt/rrhh-report/.env
```

## 13. Prueba de conectividad a la API sin mostrar datos

La prueba siguiente consulta el endpoint y solo muestra el codigo HTTP y el
tipo de contenido. No imprime empleados ni la llave.

```bash
cd /opt/rrhh-report

sudo -u rrhhjobs .venv/bin/python - <<'PY'
import urllib.request
from app_config import API_BASE, NOMINA_API_KEY, ssl_context

url = API_BASE.rstrip("/") + "/rrhh/empleados-activos-detalle"
request = urllib.request.Request(
    url,
    headers={"X-API-Key": NOMINA_API_KEY},
    method="GET",
)
with urllib.request.urlopen(
    request,
    timeout=60,
    context=ssl_context(),
) as response:
    print("HTTP", response.status)
    print("Content-Type", response.headers.get("Content-Type"))
PY
```

Resultado esperado: HTTP `200`.

No guardar ni imprimir el cuerpo de la respuesta durante la certificacion.

## 14. Prueba integral sin escritura

Ejecutar:

```bash
sudo -u rrhhjobs env \
  APP_DIR=/opt/rrhh-report \
  PYTHON=/opt/rrhh-report/.venv/bin/python \
  ENV_FILE=/opt/rrhh-report/.env \
  LOG_DIR=/opt/rrhh-report/logs/cloud \
  /opt/rrhh-report/deploy/install-cloud-cron.sh --dry-run
```

La prueba valida:

- existencia y permisos del `.env`;
- variables obligatorias;
- compilacion de los scripts;
- conexion a la base remota;
- base y schema activos;
- conteos agregados;
- cierre de empleados e IR en modo test;
- Nomina y P&G en modo dry-run.

La salida debe indicar:

```text
database=Base de Datos Historicos Talento Humano
schema=public
Dry-run complete
```

Esta prueba no escribe registros historicos.

## 15. Instalacion del cron

Solo despues de aprobar el `--dry-run`:

```bash
sudo -u rrhhjobs env \
  APP_DIR=/opt/rrhh-report \
  PYTHON=/opt/rrhh-report/.venv/bin/python \
  ENV_FILE=/opt/rrhh-report/.env \
  LOG_DIR=/opt/rrhh-report/logs/cloud \
  /opt/rrhh-report/deploy/install-cloud-cron.sh --install
```

El instalador administra unicamente el bloque:

```text
# BEGIN BIA_TALENTO_HUMANO_CLOUD_CRON
...
# END BIA_TALENTO_HUMANO_CLOUD_CRON
```

Los demas cron jobs del usuario se conservan.

Verificar:

```bash
sudo -u rrhhjobs crontab -l
```

Programacion esperada en un servidor con hora Colombia:

```cron
0 5 1 * * cierre de empleados e IR con fotografia del dia 1
0 5 5,7 * * recuperacion de empleados si esta vacio y refresco de IR
30 5 1,5,7 * * carga de Nomina y P&G
```

## 16. Corte desde la Mac al servidor

No deben existir dos schedulers escribiendo simultaneamente.

Procedimiento recomendado:

1. Aprobar el `--dry-run` en el servidor.
2. Confirmar el cron del usuario `rrhhjobs`.
3. Antes de las 5:00 AM de la primera fecha real, pausar temporalmente los
   LaunchAgents locales de cierres.
4. Mantener los archivos `.plist` como respaldo, sin borrarlos.
5. Supervisar la ejecucion del servidor.
6. Si el servidor falla, ejecutar la recuperacion manual desde un unico
   origen.
7. Despues de certificar la primera ejecucion, dejar los LaunchAgents locales
   desactivados permanentemente.

LaunchAgents de cierre que se deben retirar de la programacion:

```text
com.bia.cierre-mes-dia1
com.bia.cierre-mes-dia5
com.bia.cierre-mes-dia7
com.bia.nomina-pyg-dia1
com.bia.nomina-pyg-dia5
com.bia.nomina-pyg-dia7
```

No desactivar:

```text
com.bia.dashboard-talento
com.bia.dashboard-talento-dev
```

## 17. Primera ejecucion real

No ejecutar `--empleados-activos-detalle` a mitad de mes para probar. La
fotografia corresponde al dia en que se llama la API, pero el script la
etiqueta como el mes anterior. Una prueba real fuera del dia 1 puede producir
un cierre conceptualmente incorrecto.

La primera ejecucion real debe ocurrir en una fecha de cierre aprobada.

Ejemplo para el cierre de julio de 2026:

```text
1 de agosto de 2026, 05:00: fotografia de empleados + IR
1 de agosto de 2026, 05:30: Nomina + P&G
5 de agosto de 2026: recuperacion/refresco
7 de agosto de 2026: ultima recuperacion/refresco
```

## 18. Logs

Logs administrados por cron:

```text
/opt/rrhh-report/logs/cloud/cierre_mes_dia1.log
/opt/rrhh-report/logs/cloud/cierre_mes_dia5_7.log
/opt/rrhh-report/logs/cloud/nomina_pyg.log
```

Logs detallados adicionales:

```text
/opt/rrhh-report/logs/cierres/
```

Revision:

```bash
tail -n 100 /opt/rrhh-report/logs/cloud/cierre_mes_dia1.log
tail -n 100 /opt/rrhh-report/logs/cloud/cierre_mes_dia5_7.log
tail -n 100 /opt/rrhh-report/logs/cloud/nomina_pyg.log
```

No enviar logs completos a canales publicos. Antes de compartir un fragmento,
revisar que no contenga cedulas, nombres, salarios, passwords, tokens o
respuestas crudas.

## 19. Certificacion en PostgreSQL

Ejecutar con un usuario autorizado, mostrando solo agregados.

### 19.1 Empleados

```sql
SELECT
    fecha,
    COUNT(*) AS empleados
FROM public.historico_empleados
GROUP BY fecha
ORDER BY fecha DESC
LIMIT 3;
```

### 19.2 Ingresos y retiros

```sql
SELECT
    periodo,
    COUNT(*) AS movimientos
FROM public.historico_ir
GROUP BY periodo
ORDER BY periodo DESC
LIMIT 3;
```

### 19.3 Nomina y P&G

```sql
SELECT
    fuente,
    periodo,
    estado,
    COUNT(*) AS ejecuciones,
    SUM(filas) AS filas
FROM public.historico_nomina_pyg_control
GROUP BY fuente, periodo, estado
ORDER BY periodo DESC, fuente, estado;
```

No se considera completa la certificacion si existen controles con
`estado = 'error'` para el periodo que se esta cerrando.

### 19.4 Endpoint del dashboard

Esta validacion se ejecuta en la Mac o en el host donde este corriendo el
dashboard, no necesariamente en el servidor de la API:

```bash
curl -fsS http://127.0.0.1:8080/proxy/historico/max-periodo
```

Debe mostrar como maximos los periodos cargados en PostgreSQL.

## 20. Criterios de aceptacion

El despliegue queda certificado cuando:

- el job corre bajo `rrhhjobs`, no bajo `root`;
- `.env` tiene permisos `600`;
- no hay secretos en Git ni en el crontab;
- PostgreSQL solo acepta la IP interna autorizada;
- el `--dry-run` termina correctamente;
- el cron muestra el bloque administrado;
- la zona horaria fue validada;
- la Mac no ejecuta cierres simultaneos;
- el dia 1 crea el periodo esperado de empleados;
- dias 5 y 7 no reemplazan empleados existentes;
- IR queda actualizado;
- Nomina y P&G no presentan controles en error;
- `/proxy/historico/max-periodo` refleja el nuevo cierre;
- las vistas principales del dashboard funcionan con la base remota.

## 21. Actualizacion futura del job

Antes de actualizar:

```bash
sudo -u rrhhjobs git -C /opt/rrhh-report status
sudo -u rrhhjobs git -C /opt/rrhh-report log -1 --oneline
```

No continuar si existen modificaciones locales sin explicar.
Realizar la actualizacion fuera de las ventanas de cierre de los dias 1, 5 y
7.

Actualizar:

```bash
sudo -u rrhhjobs git -C /opt/rrhh-report pull --ff-only origin main

sudo -u rrhhjobs \
  /opt/rrhh-report/.venv/bin/pip install \
  psycopg2-binary pandas openpyxl
```

Repetir:

```bash
sudo -u rrhhjobs env \
  APP_DIR=/opt/rrhh-report \
  PYTHON=/opt/rrhh-report/.venv/bin/python \
  /opt/rrhh-report/deploy/install-cloud-cron.sh --dry-run
```

Si la programacion cambio, volver a ejecutar `--install`.

## 22. Recuperacion ante fallas

### Falla de conexion a PostgreSQL

1. No cambiar credenciales en el codigo.
2. Comprobar `172.19.0.160:5432`.
3. Revisar firewall, VPN o ruta interna.
4. Revisar permisos del usuario `th_cierre_writer`.
5. Repetir `--dry-run`.

### API de nomina no responde

1. Comprobar el servicio local de la API.
2. Validar `NOMINA_API_BASE`.
3. Confirmar que la llave esta vigente.
4. No imprimir la respuesta completa.
5. Usar los dias 5 y 7 como recuperacion.

### P&G falla parcialmente

1. Revisar `historico_nomina_pyg_control`.
2. Identificar fuente, periodo y unidad con `estado = 'error'`.
3. Corregir conectividad o credenciales.
4. Ejecutar de nuevo la carga del mes anterior en una ventana aprobada.
5. Certificar que no quedan controles en error.

### Se ejecutaron la Mac y el servidor

1. Detener nuevos schedulers duplicados.
2. No borrar datos inmediatamente.
3. Comparar conteos del periodo.
4. Revisar tiempos y logs de ambos ejecutores.
5. Ejecutar una unica recarga controlada si se requiere reconstruir el
   periodo.

## 23. Respaldo y reversa

Antes de la primera escritura productiva:

- confirmar que existe respaldo de
  `Base de Datos Historicos Talento Humano`;
- confirmar retencion y responsable del respaldo;
- conservar temporalmente los LaunchAgents como fallback desactivado;
- registrar el commit instalado en el servidor.

Para detener el cron sin borrar codigo ni datos:

```bash
sudo -u rrhhjobs crontab -l
sudo -u rrhhjobs crontab -e
```

Retirar solamente el bloque comprendido entre:

```text
# BEGIN BIA_TALENTO_HUMANO_CLOUD_CRON
# END BIA_TALENTO_HUMANO_CLOUD_CRON
```

No usar `DROP`, `TRUNCATE` ni restauraciones sin respaldo y aprobacion
explicita.

## 24. Evidencia minima de instalacion

La evidencia debe incluir:

- hostname o identificador interno del servidor;
- fecha y zona horaria;
- commit instalado;
- salida resumida del `--dry-run`;
- bloque cron sin secretos;
- conteos y periodos certificados;
- confirmacion de LaunchAgents locales pausados;
- responsable que aprobo la ejecucion.

La evidencia nunca debe incluir:

- `.env`;
- passwords;
- API keys;
- cedulas;
- nombres de empleados;
- salarios;
- cuerpos completos de las respuestas API.
