# CLAUDE.md — Convenciones del proyecto Servicio Python de **descarga de comprobantes electrónicos (CPE) desde Sovos/SUNAT**. Sigue la estructura de la plantilla `~/Desarrollo/Python/plantilla-servicio/`. ## Estructura ``` main.py # entrada delgada: while loop dirigido por scheduler.json, llama ejecutar() core.py # pipeline funcional: describir_servicio/obtener_datos/procesar_y_descargar/ejecutar scheduler.py # reutilizable (franja activa / próximo inicio a partir de scheduler.json) logger_manager.py # reutilizable (logging niveles + árbol; TimedRotatingFileHandler 30 días) hana_manager.py # reutilizable (HANA multi-schema, obtener_pendientes, marcar) descargador_sovos.py # específico: descarga PDF/XML en Sovos con Playwright (Firefox) smb_transfer.py # reutilizable (detectar/montar SMB + rsync por empresa) config.json # hana, rucs, consulta.dias, empresas(locked), files scheduler.json # HORARIOS ([hora_ini,min_ini,hora_fin,min_fin,modo]) + DIAS_ACTIVOS .env # SMB_* + SMB_AUTO_MOUNT + LOG_LEVEL Dockerfile # imagen (Playwright+Firefox ya incluidos, CA Fortinet, rsync) docker-compose.yml # servicio `descarga-cpe` persistente (restart: unless-stopped) certs/fortinet-root.crt # CA de inspección SSL de la red (FG100FTK22018356) ``` ## Reglas 1. Empresas en `config.json` con `{ nombre, schema, locked, smb_subfolder }`. `locked: true` = no ejecutar. Descargas en `downloads//`. 2. Credenciales en `.env`, nunca en git. 3. Logging con `logger_manager.get_logger(...)` y niveles info/warning/error (sin `print`). 4. `core.py` es funcional (sin clases): cada servicio reescribe `describir_servicio()`, `obtener_datos(empresa)` y `procesar_y_descargar(datos, dir_empresa)`; `main.py` siempre delgado (solo llama a `ejecutar()`; no reescribe lógica de negocio — su única lógica propia es el loop/scheduler de la regla 8). 5. Después de descargar PDF/XML se marca en HANA (`U_PGX_PDF`/`U_PGX_XML`) por schema. 6. Transferencia con `smb_transfer.transferir_a_smb(...)`; si el SMB no está montado conserva los archivos (no los borra). 7. Verificar: `python -m py_compile main.py core.py scheduler.py hana_manager.py descargador_sovos.py smb_transfer.py logger_manager.py` y correr `docker compose up -d descarga-cpe` (revisar con `docker compose logs -f`). 8. **Solo Docker, servicio persistente** (no hay venv nativa ni LaunchAgent/cron del HOST en el proyecto): `docker-compose.yml` usa `restart: unless-stopped` y `main.py` mantiene un `while` continuo — en cada ciclo llama a `ejecutar()` (pipeline de `core.py`) y luego duerme según el modo de la franja horaria activa. `scheduler.py` lee `scheduler.json` (releído en cada ciclo, así se pueden editar horarios sin reiniciar el contenedor) y responde: - si el día/hora actual cae dentro de una franja de `HORARIOS`, su `modo` (`"rapido"` → dormir 10 min, `"full"` → dormir 1 hora; mapeo fijo en `INTERVALOS_POR_MODO` dentro de `scheduler.py`, no en el JSON); - si no, el inicio de la próxima franja activa (respetando `DIAS_ACTIVOS`), y `main.py` duerme exactamente hasta ese instante. El sueño se hace en pasos de 1s (`_dormir_interrumpible`) y `main.py` registra handlers de `SIGINT`/`SIGTERM` para poder frenar el loop y salir limpio ante `docker compose stop/down`, en vez de que Docker tenga que matarlo por timeout. Una excepción no controlada en un ciclo se loguea (`exc_info=True`, va a `logs/descarga-cpe.log` con su rotación de 30 días) y el loop reintenta tras 60s — ya no hay cron externo que reintente por nosotros. Como el contenedor ya no es efímero, `docker-compose.yml` limita el driver `json-file` de Docker (`max-size: 10m`, `max-file: 5`) para que el log propio del contenedor (stdout) no crezca sin límite; esto es independiente de la rotación de `logs/descarga-cpe.log`, que maneja `logger_manager.py`. 9. `scheduler.json` define `HORARIOS` (lista de `[hora_ini, min_ini, hora_fin, min_fin, modo]`, `modo` es `"rapido"` o `"full"`) y `DIAS_ACTIVOS` (0=lunes … 6=domingo, según `datetime.weekday()`). Fuera de esas franjas y días no se ejecuta nada. 10. Bloqueos de red (FortiGuard Web Filter, p.ej. categoría "Finance and Banking" en `public-einvoicing.sovos.com`) no son bug de la app: se resuelven en el FortiGate, no reintentando ni parcheando el código.