145 lines
7.1 KiB
Markdown
145 lines
7.1 KiB
Markdown
# 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`.
|