Files
descarga_cpe_sovos/CLAUDE.md
T

145 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 de archivos
```
main.py # entrada delgada: try/except alrededor de ejecutar()
core.py # pipeline funcional: describir_servicio/obtener_datos/procesar_y_descargar/ejecutar
logger_manager.py # reutilizable (logging niveles + árbol; TimedRotatingFileHandler, rotación diaria, 30 días de retención)
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
.env # SMB_* + SMB_AUTO_MOUNT + LOG_LEVEL
Dockerfile # imagen (Playwright+Firefox ya incluidos, CA Fortinet, rsync)
docker-compose.yml # servicio `descarga-cpe`, monta config/downloads/logs/SMB
crontab-host # cron del HOST (macOS o Linux) que dispara `docker compose run --rm`
certs/fortinet-root.crt # CA de inspección SSL de la red (FG100FTK22018356)
```
## Arquitectura: Docker
Todo corre **solo en Docker** — no hay venv nativa ni LaunchAgent en el proyecto.
El contenedor es una **pasada única**: arranca, procesa, y se destruye (`--rm`); nunca
queda corriendo en loop. No agregar un `while`/scheduler dentro de la app — si el
contenedor falla, la siguiente corrida de cron simplemente reintenta.
```mermaid
flowchart LR
subgraph HOST["Host (macOS o Linux)"]
CRON["cron\n(crontab-host)"]
FILES[("config.json · downloads/ · logs/\nSMB ya montado en el host")]
end
subgraph CONTAINER["Contenedor Docker (una pasada, --rm)"]
APP["main.py → core.ejecutar()"]
end
CRON -- "docker compose run --rm" --> APP
FILES -. "volumes" .- APP
APP -- "hdbcli" --> HANA[("SAP HANA")]
APP -- "Playwright + Firefox" --> SOVOS[("Sovos / SUNAT")]
APP -- "rsync" --> SMB[("Carpeta SMB / NAS")]
```
- El contenedor fija `TZ=America/Lima`, así que los timestamps del log y cualquier
lógica sensible a fecha/hora quedan en horario de Perú, sin importar la zona
horaria del host.
- La imagen base (`mcr.microsoft.com/playwright/python:v1.62.0-jammy`) ya trae
Chromium y Firefox instalados, en la misma versión que `playwright==1.62.0` de
`requirements.txt`. Si se sube la versión de Playwright, hay que subir también
el tag de la imagen en el `Dockerfile` (si no, Firefox no va a coincidir).
- `certs/fortinet-root.crt` se instala en el contenedor porque la red de la oficina
tiene un Fortinet haciendo inspección SSL; sin esa CA, `pip` y las requests HTTPS
fallarían por certificado no confiable.
- `main.py` envuelve `ejecutar()` en un `try/except` que loguea cualquier excepción
no controlada (`exc_info=True`) antes de salir con código 1, para que los crashes
también queden en `logs/descarga-cpe.log` (ya rotado) en vez de perderse en el
mail local de cron. Por eso no hace falta redirigir el stdout de cron a un
segundo archivo de log — sería un mecanismo de rotación redundante.
## Arquitectura: Cron (programación)
El **host** (no el contenedor) decide cuándo correr. `crontab-host` dispara
`docker compose run --rm descarga-cpe` en estas franjas, lunes a viernes:
| Franja | Frecuencia |
|---------------|------------|
| 06:00–13:00 | cada 20 min |
| 13:00–14:00 | cada 30 min |
| 14:00–21:00 | cada 20 min |
| 21:00–23:00 | cada 1 hora (21:00 y 22:00) |
Fuera de esas franjas y en fines de semana no corre nada; retoma a las 06:00 del
siguiente día hábil.
Flujo de una corrida individual:
```mermaid
flowchart TD
A["Cron: horario programado"] --> B["docker compose run --rm descarga-cpe"]
B --> C["Contenedor arranca → python main.py"]
C --> D["core.ejecutar()"]
D --> E{"¿Hay empresas activas?\n(config.json, locked=false)"}
E -->|No| Z["Log: sin empresas activas, termina"]
E -->|Sí| F["Por cada empresa activa"]
F --> G["obtener_datos(empresa)\nHANA: SELECT pendientes"]
G --> H{"¿Hay documentos\npendientes?"}
H -->|No| F
H -->|Sí| I["descargar_documentos_pendientes()\nPlaywright + Firefox → Sovos"]
I --> J["Marca U_PGX_PDF / U_PGX_XML\nen HANA (hana_manager)"]
J --> F
F -->|Todas procesadas| K["transferir_a_smb()\nrsync → carpeta SMB/NAS"]
K --> L["Contenedor se destruye (--rm)"]
C -.->|"excepción no controlada"| M["main.py: try/except\nloguea y sale con código 1"]
```
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.
### Configurar el cron del host (`crontab-host`)
1. Editar `crontab-host` y ajustar:
- `PROY`: ruta absoluta del proyecto en esa máquina.
- `SMB_HOST_MOUNT`: ruta donde esa máquina ya tiene montada la carpeta SMB/NAS
(macOS: normalmente algo bajo `/Volumes/...` o `/Users/<usuario>/...` si se
montó a mano; Linux: p.ej. `/mnt/filesap`).
- `PATH`: cron corre con un `PATH` mínimo que no incluye `/usr/local/bin`, donde
suele vivir el binario `docker` de Docker Desktop — sin esto, cada corrida
falla con `docker: command not found`. Confirmar la ruta real con
`which docker` en una terminal normal y reflejarla en esta línea.
2. Instalar el crontab (reemplaza todo el crontab del usuario actual):
```bash
crontab crontab-host
```
Para revisar qué quedó cargado: `crontab -l`. Para editarlo en caliente:
`crontab -e` (abre el crontab activo en el editor por defecto, no el archivo
del repo).
3. **macOS**: dar Full Disk Access a `/usr/sbin/cron` en Ajustes del Sistema →
Privacidad y Seguridad → Acceso Total al Disco. Sin esto, cron no puede leer/
escribir dentro de `~/Documents` (o donde esté el proyecto) y las corridas
fallan en silencio.
4. Verificar que corre: revisar `logs/descarga-cpe.log` después del próximo
horario programado, o forzar una corrida manual con la variable ya seteada:
```bash
cd $PROY && SMB_HOST_MOUNT=/ruta/real docker compose run --rm descarga-cpe
```
## Reglas de código
1. Empresas en `config.json` con `{ nombre, schema, locked, smb_subfolder }`.
`locked: true` = no ejecutar. Descargas en `downloads/<nombre>/`.
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()`).
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 hana_manager.py descargador_sovos.py smb_transfer.py logger_manager.py`
y correr `docker compose run --rm descarga-cpe`.