# MVE Incrementables Parser Microservicio para extraer y parsear la sección de **Incrementables** de documentos PDF tipo "CARTA INS". Construido con **FastAPI** y **Python 3.12**. ## 🚀 Características - ✅ **Autenticación JWT** con bcrypt - ✅ **Extracción de texto** con PyMuPDF y pdfplumber (fallback) - ✅ **OCR con Tesseract** para PDFs escaneados (detección automática) - ✅ **Parsing robusto** de incrementables (fletes, seguros, almacenaje, regalías) - ✅ **Validación de archivos** (tamaño, tipo MIME, PDF cifrado) - ✅ **Logging estructurado** con correlation ID - ✅ **Rate limiting** en login - ✅ **Procesamiento asíncrono** con Celery y Redis - ✅ **Cola de tareas** para procesamiento en background - ✅ **Tests con pytest** - ✅ **Docker & Docker Compose** ## 📋 Requisitos - Python 3.12+ - Docker & Docker Compose (opcional) - **Tesseract OCR** (para PDFs escaneados) - [Ver guía de instalación](OCR_SETUP.md) - **Poppler** (para conversión PDF a imagen) - Requerido solo para OCR ### 🤖 OCR para PDFs Escaneados El sistema puede leer **PDFs escaneados** (imágenes) usando Tesseract OCR. La detección es automática: - Si el PDF tiene texto seleccionable → Extracción normal (rápido) - Si el PDF está escaneado → OCR automático (más lento) **Con Docker (Recomendado):** - ✅ Tesseract ya incluido en la imagen - ✅ Sin instalación adicional - ✅ Ver [DOCKER_OCR.md](DOCKER_OCR.md) para guía completa **Sin Docker (Instalación local):** ```powershell # Windows - Ejecutar script de instalación .\install_ocr.ps1 # O instalar manualmente - Ver OCR_SETUP.md para guía completa ``` **Verificar instalación local:** ```bash python test_ocr.py ``` Ver [OCR_SETUP.md](OCR_SETUP.md) para instrucciones detalladas de instalación local. ## 🛠️ Instalación ### Opción 1: Con Docker (Recomendado) 1. **Clonar el repositorio** ```bash cd mve-incrementables-parser ``` 2. **Configurar variables de entorno** ```bash cp .env.example .env ``` Edita `.env` y configura: ```env AUTH_USERNAME=admin AUTH_PASSWORD_HASH=$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYqwMzYXhHO JWT_SECRET=your-secret-key-change-this-in-production JWT_EXPIRES_MINUTES=60 MAX_FILE_MB=10 LOG_LEVEL=INFO # OCR Settings (ya configurado por defecto) OCR_ENABLED=true OCR_LANGUAGE=spa OCR_DPI=300 OCR_TIMEOUT=300 ``` **Generar hash de contraseña:** ```bash python -c "from passlib.hash import bcrypt; print(bcrypt.hash('tu_contraseña'))" ``` 3. **Levantar el servicio** ```bash docker-compose up --build ``` El servicio estará disponible en `http://localhost:9876` **Servicios incluidos:** - API FastAPI: puerto 9876 - Redis: puerto 16379 - Celery Worker: procesamiento en background - **Tesseract OCR**: Incluido en el contenedor (sin instalación adicional) ### Opción 2: Sin Docker 1. **Instalar dependencias del sistema** ```bash # macOS brew install mupdf-tools tesseract tesseract-lang # Ubuntu/Debian sudo apt-get install libmupdf-dev mupdf-tools tesseract-ocr tesseract-ocr-spa poppler-utils # Windows - Ver OCR_SETUP.md para instrucciones detalladas # Descargar e instalar: # - Tesseract: https://github.com/UB-Mannheim/tesseract/wiki # - Poppler: https://github.com/oschwartz10612/poppler-windows/releases ``` 2. **Crear entorno virtual** ```bash python3.12 -m venv venv source venv/bin/activate # En Windows: venv\Scripts\activate ``` 3. **Instalar dependencias Python** ```bash pip install -r requirements.txt ``` 4. **Configurar .env** (ver Opción 1, paso 2) 5. **Ejecutar el servicio** ```bash uvicorn app.main:app --host 0.0.0.0 --port 9876 --reload ``` ## 📚 Uso ### 1. Health Check ```bash curl http://localhost:9876/health ``` **Respuesta:** ```json { "status": "ok", "service": "mve-incrementables-parser", "version": "1.0.0" } ``` ### 2. Autenticación (Login) ```bash curl -X POST http://localhost:9876/auth/login \ -H "Content-Type: application/json" \ -d '{ "username": "admin", "password": "tu_contraseña" }' ``` **Respuesta:** ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "bearer", "expires_in": 3600 } ``` ⚠️ **Nota:** Guarda el `access_token` para usarlo en las siguientes peticiones. ### 3. Parsear PDF (Síncrono) ```bash curl -X POST http://localhost:9876/v1/incrementables/parse \ -H "Authorization: Bearer " \ -F "file=@CARTA_INS_228718.pdf" \ -F "document_ref=INS-228718" ``` **Con Correlation ID personalizado:** ```bash curl -X POST http://localhost:9876/v1/incrementables/parse \ -H "Authorization: Bearer " \ -H "X-Correlation-Id: custom-id-12345" \ -F "file=@CARTA_INS_228718.pdf" ``` **Respuesta exitosa (200):** ```json { "correlation_id": "550e8400-e29b-41d4-a716-446655440000", "document": { "filename": "CARTA INS 228718.pdf", "pages": 1, "sha256": "a3d5f..." }, "incrementables": { "currency": "USD", "fletes": 1591.20, "seguros": null, "almacenaje_consolidacion": 0.00, "regalias": null }, "extraction": { "method": "text", "anchors_found": ["AJUSTE DE INCREMENTABLES EN:"], "warnings": [] } } ``` ### 4. Parsear PDF (Asíncrono con Cola) Para archivos grandes o cuando no quieres esperar, usa el endpoint asíncrono: ```bash curl -X POST http://localhost:9876/v1/incrementables/parse/async \ -H "Authorization: Bearer " \ -F "file=@CARTA_INS_228718.pdf" \ -F "document_ref=INS-228718" ``` **Respuesta (202):** ```json { "task_id": "a8f2e9c1-5b3d-4e7f-9a1c-2d3e4f5a6b7c", "correlation_id": "550e8400-e29b-41d4-a716-446655440000", "status": "queued", "message": "Task queued for processing. Use /v1/incrementables/status/{task_id} to check progress." } ``` ### 5. Consultar Estado de Tarea ```bash curl -X GET http://localhost:9876/v1/incrementables/status/a8f2e9c1-5b3d-4e7f-9a1c-2d3e4f5a6b7c \ -H "Authorization: Bearer " ``` **Respuesta (en proceso):** ```json { "task_id": "a8f2e9c1-5b3d-4e7f-9a1c-2d3e4f5a6b7c", "status": "started", "result": null, "error": null } ``` **Respuesta (completada):** ```json { "task_id": "a8f2e9c1-5b3d-4e7f-9a1c-2d3e4f5a6b7c", "status": "completed", "result": { "status": "completed", "task_id": "a8f2e9c1-5b3d-4e7f-9a1c-2d3e4f5a6b7c", "document": { "filename": "CARTA INS 228718.pdf", "pages": 1, "sha256": "a3d5f...", "document_ref": "INS-228718" }, "incrementables": { "currency": "USD", "fletes": 1591.20, "seguros": null, "almacenaje_consolidacion": 0.00, "regalias": null }, "extraction": { "method": "text", "anchors_found": ["AJUSTE DE INCREMENTABLES EN:"], "warnings": [] } }, "error": null } ``` **Errores comunes:** - **400**: Archivo no válido, no es PDF, excede tamaño máximo - **401**: Token ausente, inválido o expirado - **422**: No se encontró sección de incrementables o parsing falló - **500**: Error interno del servidor ## 🔄 Procesamiento Asíncrono El servicio soporta dos modos de procesamiento: ### Modo Síncrono (`/parse`) - Respuesta inmediata - Ideal para PDFs pequeños - Timeout en request HTTP ### Modo Asíncrono (`/parse/async`) - Respuesta inmediata con task_id - Procesamiento en background con Celery - Ideal para PDFs grandes o lotes - Sin timeout (límite: 5 minutos por tarea) - Consulta estado con `/status/{task_id}` ### Arquitectura ``` Cliente → FastAPI → Redis (Cola) → Celery Worker → Procesa PDF → Redis (Resultado) ↓ Cliente ← FastAPI ← Redis (Consulta) ←──────────────────────────────── ``` ## 🧪 Tests ```bash # Ejecutar todos los tests pytest # Con cobertura pytest --cov=app --cov-report=html # Test específico pytest tests/test_parser.py -v # Ver logs detallados pytest -v -s ``` ## 🏗️ Estructura del Proyecto ``` mve-incrementables-parser/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI app principal │ ├── schemas.py # Modelos Pydantic │ ├── core/ │ │ ├── config.py # Configuración (ENV) │ │ ├── security.py # JWT, bcrypt, rate limiting │ │ └── celery_app.py # Configuración de Celery │ ├── api/ │ │ ├── auth.py # Endpoint de login │ │ └── v1/ │ │ └── incrementables.py # Endpoints de parsing (sync/async) │ ├── tasks/ │ │ └── parse_tasks.py # Tareas de Celery │ └── services/ │ ├── pdf_text.py # Extracción de texto (PyMuPDF/pdfplumber) │ └── parser.py # Parsing de incrementables ├── tests/ │ ├── conftest.py # Fixtures de pytest │ ├── test_parser.py # Tests del parser │ └── test_api.py # Tests de endpoints ├── .env.example # Ejemplo de variables de entorno ├── .gitignore ├── Dockerfile ├── docker-compose.yml # API + Redis + Celery Worker ├── pytest.ini ├── requirements.txt └── README.md ``` │ │ └── v1/ │ │ └── incrementables.py # Endpoint de parsing │ └── services/ │ ├── pdf_text.py # Extracción de texto (PyMuPDF/pdfplumber) │ └── parser.py # Parsing de incrementables ├── tests/ │ ├── conftest.py # Fixtures de pytest │ ├── test_parser.py # Tests del parser │ └── test_api.py # Tests de endpoints ├── .env.example # Ejemplo de variables de entorno ├── .gitignore ├── Dockerfile ├── docker-compose.yml ├── pytest.ini ├── requirements.txt └── README.md ``` ## 🔒 Seguridad ### Autenticación JWT - **Login** con username/password → devuelve JWT - **Protección** de endpoints `/v1/*` con Bearer token - **Expiración** configurable (default: 60 minutos) - **Rate limiting** básico en login (5 intentos / 5 minutos por IP) ### Contraseñas - **Nunca** se loguean contraseñas en texto plano - Uso de **bcrypt** para hashing - Respuestas **uniformes** (401) en error de autenticación ### Archivos - Validación de **tipo MIME** - Validación de **extensión** `.pdf` - **Tamaño máximo** configurable (default: 10 MB) - Rechazo de **PDFs cifrados** - Cálculo de **SHA256** para integridad ## ⚙️ Configuración (Variables de Entorno) | Variable | Descripción | Default | |----------|-------------|---------| | `AUTH_USERNAME` | Usuario para login | `admin` | | `AUTH_PASSWORD_HASH` | Hash bcrypt de contraseña | *requerido* | | `JWT_SECRET` | Secret para firmar JWT | *requerido* | | `JWT_EXPIRES_MINUTES` | Duración del token (minutos) | `60` | | `MAX_FILE_MB` | Tamaño máximo de PDF (MB) | `10` | | `LOG_LEVEL` | Nivel de logging | `INFO` | | `REDIS_URL` | URL de conexión a Redis | `redis://redis:6379/0` | | `CELERY_BROKER_URL` | URL del broker de Celery | `redis://redis:6379/0` | | `CELERY_RESULT_BACKEND` | URL del backend de resultados | `redis://redis:6379/0` | | `SERVICE_NAME` | Nombre del servicio | `mve-incrementables-parser` | | `SERVICE_VERSION` | Versión del servicio | `1.0.0` | ## 📖 API Documentation Una vez levantado el servicio, accede a la documentación interactiva: - **Swagger UI**: http://localhost:9876/docs - **ReDoc**: http://localhost:9876/redoc ## 🔍 Logging El servicio utiliza **logging estructurado en JSON**: ```json { "time": "2026-03-02T10:30:45", "level": "INFO", "name": "app.api.v1.incrementables", "message": "Successfully parsed incrementables", "correlation_id": "550e8400-e29b-41d4-a716-446655440000", "currency": "USD", "fletes": 1591.20 } ``` ### Correlation ID - **Automático**: Se genera UUID si no se proporciona - **Manual**: Enviar header `X-Correlation-Id` - **Propagación**: Aparece en logs y respuesta - **Utilidad**: Tracking de requests en sistemas distribuidos ## 🐛 Troubleshooting ### Error: "PyMuPDF extraction failed" **Causa**: PDF corrupto o cifrado **Solución**: El servicio intentará con pdfplumber automáticamente. Si ambos fallan: - Verificar que el PDF no esté cifrado - Intentar con otro PDF - Revisar logs para detalles ### Error: "Incrementables section not found" **Causa**: El PDF no contiene la sección esperada o el formato es diferente **Solución**: - Verificar que el PDF tenga el texto "AJUSTE DE INCREMENTABLES EN:" - Revisar el formato del documento - Si es un formato nuevo, actualizar los patrones en `app/services/parser.py` ### Error: "Rate limit exceeded" **Causa**: Demasiados intentos de login desde la misma IP **Solución**: Esperar 5 minutos o reiniciar el servicio ## 🚀 Despliegue en Producción ### Recomendaciones 1. **Cambiar credenciales**: Generar nueva contraseña y JWT_SECRET 2. **HTTPS**: Usar reverse proxy (nginx, traefik) 3. **Límites**: Ajustar `MAX_FILE_MB` según necesidad 4. **Logging**: Integrar con sistema de logs centralizado 5. **Monitoring**: Configurar health checks y alertas 6. **Recursos**: Ajustar memoria/CPU del contenedor ### Ejemplo con nginx ```nginx server { listen 443 ssl; server_name api.mve.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; location / { proxy_pass http://localhost:9876; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Aumentar límites para PDFs grandes client_max_body_size 20M; } } ``` ## 📝 Ejemplos de Formato Esperado El servicio espera PDFs con formato similar a: ``` CARTA INSTRUCTIVO DE EMBARQUE No. 228718 AJUSTE DE INCREMENTABLES EN: Fletes: $1,591.20 USD Seguros: USD Almacenaje/Consolidación: $0.00 USD Regalías: USD Observaciones... ``` ### Campos Reconocidos - **Fletes**: Requerido, con o sin signo `$`, con comas - **Seguros**: Opcional, puede estar vacío (solo "USD" → `null`) - **Almacenaje/Consolidación**: Requerido, acepta variaciones de escritura - **Regalías/Regalias**: Opcional, con o sin acento ## 🤝 Contribuir 1. Fork el proyecto 2. Crear rama feature (`git checkout -b feature/amazing-feature`) 3. Commit cambios (`git commit -m 'Add amazing feature'`) 4. Push a la rama (`git push origin feature/amazing-feature`) 5. Abrir Pull Request ## 📄 Licencia Este proyecto es parte del sistema MVE y es de uso interno. ## 📧 Contacto Para soporte o dudas, contactar al equipo de desarrollo MVE. --- **Versión:** 1.0.0 **Última actualización:** Marzo 2026