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