Files
plantillas-proyectos/backend/api/v1/modules/a76/expediente_archivos/tasks.py
2026-04-17 15:59:47 -06:00

287 lines
11 KiB
Python
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.

from __future__ import annotations
import logging
import time
from typing import Any, Dict
from celery import Task
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.exceptions import ErrorCollector, ValidationException
from .models import ExpedienteArchivo
from .service import ExpedienteArchivoService, build_configuracion_vu
from .external_service import ExpedienteExternalService
logger = logging.getLogger(__name__)
TOTAL_STEPS = 4
def _progress(task: Task, current: int, status: str) -> None:
task.update_state(
state="PROGRESS",
meta={"current": current, "status": status, "current_step": status, "progress": current, "total_steps": TOTAL_STEPS},
)
def _poll_external(
task: Task,
external: ExpedienteExternalService,
external_task_id: str,
timeout_seconds: int = 300,
) -> Dict[str, Any]:
"""
Hace polling al API externo hasta obtener un estado final o agotar el timeout.
Retorna el payload final tal como lo devuelve el API externo.
"""
start = time.time()
last_payload: Dict[str, Any] = {}
while True:
elapsed = time.time() - start
if elapsed > timeout_seconds:
logger.error("Timeout en polling externo de digitalización: task_id=%s", external_task_id)
raise TimeoutError(f"Timeout consultando estado de digitalización (task_id={external_task_id}).")
try:
status_payload = external.get_status(external_task_id) or {}
except Exception as exc:
logger.exception("Error consultando estado externo de digitalización")
raise
last_payload = status_payload
state = str(status_payload.get("state") or "").upper()
progress_info = status_payload.get("progress") or {}
try:
percent = float(progress_info.get("progress", 0.0))
except (TypeError, ValueError):
percent = 0.0
current_step = (
progress_info.get("current_step")
or "Consultando estado en Ventanilla Única..."
)
_progress(task, int(percent), str(current_step))
if state in {"PENDING", "STARTED", "PROGRESS"} or not state:
time.sleep(5)
continue
return last_payload
@celery_app.task(bind=True, name="expediente_archivos_digitalizar")
def digitalizar_task(
self: Task,
expediente_id: int,
request_data: Dict[str, Any],
company_id: int,
tenant_id: int,
) -> dict:
"""
Celery task: digitaliza un ExpedienteArchivo en Ventanilla Única.
Pasos:
1. Cargar el registro y construir configuracion_vu (server-side).
2. Construir y enviar el payload al API externo.
3. Hacer polling del estado del task externo.
4. Persistir resultado (e_document, num_operacion, acuse_pdf_path) en DB.
"""
db = CoreSessionLocal()
try:
# ------------------------------------------------------------------ #
# Paso 1 cargar registro y construir configuracion_vu #
# ------------------------------------------------------------------ #
_progress(self, 5, "Construyendo configuración VU...")
record: ExpedienteArchivo | None = db.get(ExpedienteArchivo, expediente_id)
if not record:
raise ValidationException(
"Expediente no encontrado",
errors=[{"field": "expediente_id", "message": f"No existe expediente {expediente_id}"}],
)
errors = ErrorCollector()
agente_key = request_data.get("agente_aduanal") or record.agente_aduanal
config_vu = build_configuracion_vu(db, company_id, tenant_id, agente_key, errors)
if errors.has_errors():
error_list = errors._errors # type: ignore[attr-defined]
first = error_list[0] if error_list else {}
self.update_state(
state="FAILURE",
meta={
"error": first.get("message", "Error de configuración VU"),
"error_type": "VALIDATION_ERROR",
"error_detail": {
"codigo": first.get("code", "VALIDATION_ERROR"),
"descripcion": first.get("message", ""),
"paso": "Construcción de configuración VU",
"sugerencias": first.get("solution") or [],
},
},
)
return {}
# Actualizar status en DB
record.status = "processing"
record.task_id = self.request.id
db.commit()
# ------------------------------------------------------------------ #
# Paso 2 construir payload y enviar al API externo #
# ------------------------------------------------------------------ #
_progress(self, 30, "Enviando documento a Ventanilla Única...")
payload = {
"rfc_consulta": request_data.get("rfc_consulta") or record.rfc_consulta or "",
"clave_documento": request_data.get("clave_documento") or record.tipo_documento or "",
"nombre_archivo": request_data.get("nombre_archivo") or record.nombre_archivo or "",
"archivo_base64": request_data.get("archivo_base64") or "",
"configuracion_vu": config_vu,
}
external = ExpedienteExternalService()
response = external.digitalizar_archivo_json(payload)
# Chequear si el API externo devolvió un error inmediato
resp_state = str(response.get("state") or response.get("status") or "").upper()
if resp_state in {"ERROR", "FAILURE", "FAILED"}:
error_msg = response.get("message") or response.get("error") or "Error en API externo"
record.status = "failed"
db.commit()
self.update_state(
state="FAILURE",
meta={
"error": error_msg,
"error_type": "EXTERNAL_API_ERROR",
"error_detail": {
"codigo": "EXTERNAL_API_ERROR",
"descripcion": error_msg,
"paso": "Envío a Ventanilla Única",
"sugerencias": ["Verifica las credenciales VU y vuelve a intentarlo."],
},
},
)
return {}
# Extraer task_id externo si el API lo devolvió de inmediato en PENDING/PROCESSING
external_task_id = response.get("task_id") or response.get("id")
# ------------------------------------------------------------------ #
# Paso 3 polling del estado externo #
# ------------------------------------------------------------------ #
if external_task_id:
_progress(self, 50, "Esperando respuesta de Ventanilla Única...")
record.external_task_id = str(external_task_id)
db.commit()
try:
final_response = _poll_external(self, external, str(external_task_id))
except TimeoutError as exc:
record.status = "failed"
db.commit()
self.update_state(
state="FAILURE",
meta={
"error": str(exc),
"error_type": "TIMEOUT",
"error_detail": {
"codigo": "TIMEOUT",
"descripcion": str(exc),
"paso": "Polling Ventanilla Única",
"sugerencias": ["Vuelve a intentarlo o consulta el estado manualmente."],
},
},
)
return {}
else:
# El API devolvió resultado directo
final_response = response
# ------------------------------------------------------------------ #
# Paso 4 persistir resultado #
# ------------------------------------------------------------------ #
_progress(self, 90, "Guardando resultado...")
result_payload = final_response.get("result") or final_response
e_doc = result_payload.get("e_document") or result_payload.get("eDocument")
num_op = result_payload.get("numero_operacion") or result_payload.get("numeroOperacion")
acuse_b64 = result_payload.get("acuese_digitalizacion_pdf_base64") or result_payload.get("acuse_pdf_base64")
record.status = "success"
if e_doc:
record.e_document = str(e_doc)
if num_op:
record.num_operacion = str(num_op)
# Guardamos el acuse en-línea (no en S3 por ahora) o en una ruta
# Los acuses se almacenan en el campo acuse_pdf_path como indicación
if acuse_b64:
record.acuse_pdf_path = "inline"
db.commit()
return {
"status": "success",
"message": "Digitalización completada exitosamente.",
"e_document": e_doc,
"numero_operacion": num_op,
"acuese_digitalizacion_pdf_base64": acuse_b64,
"nombre_archivo": payload["nombre_archivo"],
"timestamp": result_payload.get("timestamp"),
"request_id": result_payload.get("request_id"),
"response_code": result_payload.get("response_code"),
}
except ValidationException as exc:
if db:
try:
record = db.get(ExpedienteArchivo, expediente_id) # type: ignore
if record:
record.status = "failed"
db.commit()
except Exception:
pass
first_error = (exc.errors or [{}])[0]
self.update_state(
state="FAILURE",
meta={
"error": first_error.get("message", str(exc)),
"error_type": "VALIDATION_ERROR",
"error_detail": {
"codigo": first_error.get("code", "VALIDATION_ERROR"),
"descripcion": first_error.get("message", ""),
"paso": "Validación",
"sugerencias": first_error.get("solution") or [],
},
},
)
return {}
except Exception as exc:
logger.exception("Error inesperado en digitalizar_task expediente_id=%s", expediente_id)
if db:
try:
record = db.get(ExpedienteArchivo, expediente_id) # type: ignore
if record:
record.status = "failed"
db.commit()
except Exception:
pass
self.update_state(
state="FAILURE",
meta={
"error": str(exc),
"error_type": type(exc).__name__,
"error_detail": {
"codigo": "UNEXPECTED_ERROR",
"descripcion": str(exc),
"paso": "Proceso de digitalización",
"sugerencias": ["Contacta al soporte técnico."],
},
},
)
return {}
finally:
db.close()