Files
descarga_cpe_sovos/CLAUDE.md
T

7.1 KiB
Raw Blame History

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.

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:

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):
    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:
    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.