277 lines
11 KiB
Python
277 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 ValidationException
|
|
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceComplianceMx
|
|
|
|
from .service import FacturaCoveDomainService
|
|
from .schemas import GenerateCoveResult
|
|
from .external_service import CoveExternalService, CoveExternalResult
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _progress(task: Task, current: int, status: str) -> None:
|
|
task.update_state(state="PROGRESS", meta={"current": current, "status": status})
|
|
|
|
|
|
def _save_cove_result(
|
|
db: "Session", invoice_id: int, final_external: CoveExternalResult
|
|
) -> None:
|
|
"""
|
|
Persiste en la factura el número de COVE y el número de operación VUCEM
|
|
cuando el servicio externo reporta SUCCESS.
|
|
"""
|
|
if final_external.status != "success":
|
|
return
|
|
|
|
if not final_external.cove_number and not final_external.vucem_operation_num:
|
|
return
|
|
|
|
invoice = db.get(InvoiceHeader, invoice_id)
|
|
if not invoice:
|
|
logger.error("No se encontró la factura %s para guardar COVE", invoice_id)
|
|
return
|
|
|
|
compliance = invoice.compliance_mx
|
|
if not compliance:
|
|
# Creamos un registro mínimo de compliance ligado a la factura.
|
|
compliance = InvoiceComplianceMx(
|
|
invoice_id=invoice.id,
|
|
tenant_id=invoice.tenant_id,
|
|
company_id=invoice.company_id,
|
|
)
|
|
db.add(compliance)
|
|
|
|
# Idempotencia básica: solo sobrescribir si está vacío o coincide.
|
|
if final_external.cove_number:
|
|
current_cove = compliance.edocument or ""
|
|
new_cove = final_external.cove_number or ""
|
|
if not current_cove or current_cove == new_cove:
|
|
compliance.edocument = new_cove
|
|
|
|
if final_external.vucem_operation_num:
|
|
current_op = compliance.vucem_operation_num or ""
|
|
new_op = final_external.vucem_operation_num or ""
|
|
if not current_op or current_op == new_op:
|
|
compliance.vucem_operation_num = new_op
|
|
|
|
db.commit()
|
|
|
|
|
|
def _poll_external_status(
|
|
task: Task, external: CoveExternalService, external_task_id: str, timeout_seconds: int = 300
|
|
) -> CoveExternalResult:
|
|
"""
|
|
Realiza polling al endpoint externo de status de COVE hasta obtener un estado final
|
|
o agotar el timeout.
|
|
"""
|
|
start = time.time()
|
|
last_payload: Dict[str, Any] = {}
|
|
|
|
while True:
|
|
if time.time() - start > timeout_seconds:
|
|
logger.error("Timeout consultando estado de COVE para external_task_id=%s", external_task_id)
|
|
return CoveExternalResult(
|
|
status="error",
|
|
message="Timeout consultando estado de COVE en Ventanilla Única.",
|
|
cove_number=None,
|
|
vucem_operation_num=None,
|
|
raw_response={"last_status": last_payload, "external_task_id": external_task_id},
|
|
)
|
|
|
|
try:
|
|
status_payload = external.get_status(external_task_id)
|
|
except Exception as exc: # pragma: no cover - errores HTTP inesperados
|
|
logger.exception("Error consultando estado externo de COVE")
|
|
return CoveExternalResult(
|
|
status="error",
|
|
message=f"Error consultando estado de COVE en Ventanilla Única: {exc}",
|
|
cove_number=None,
|
|
vucem_operation_num=None,
|
|
raw_response={"last_status": last_payload, "external_task_id": external_task_id},
|
|
)
|
|
|
|
last_payload = status_payload or {}
|
|
state = str(last_payload.get("state") or "").upper()
|
|
progress = last_payload.get("progress") or {}
|
|
# En el ejemplo: progress.progress (float 0-100), progress.current_step (texto), numero_operacion
|
|
try:
|
|
percent = float(progress.get("progress", 0.0))
|
|
except (TypeError, ValueError):
|
|
percent = 0.0
|
|
current_step = progress.get("current_step") or "Consultando estado de COVE en Ventanilla Única..."
|
|
numero_operacion = progress.get("numero_operacion") or last_payload.get("numero_operacion")
|
|
if numero_operacion:
|
|
current_step = f"{current_step} (Operación: {numero_operacion})"
|
|
|
|
# Actualizar progreso para que el frontend lo vea en el diálogo
|
|
_progress(task, int(percent), str(current_step))
|
|
|
|
# Estados intermedios: seguimos pollendo
|
|
if state in {"PENDING", "STARTED", "PROGRESS"} or not state:
|
|
time.sleep(5)
|
|
continue
|
|
|
|
# Estado final: SUCCESS / FAILURE u otros
|
|
result_payload = last_payload.get("result") or {}
|
|
error_text = last_payload.get("error")
|
|
|
|
if state in {"SUCCESS", "COMPLETED"}:
|
|
cove_number = result_payload.get("cove_number") or result_payload.get("cove")
|
|
vucem_operation_num = result_payload.get("vucem_operation_num") or result_payload.get(
|
|
"numero_operacion"
|
|
)
|
|
message = result_payload.get("message") or last_payload.get("message") or "COVE generado correctamente."
|
|
|
|
return CoveExternalResult(
|
|
status="success",
|
|
message=message,
|
|
cove_number=cove_number,
|
|
vucem_operation_num=vucem_operation_num,
|
|
raw_response={**last_payload, "external_task_id": external_task_id},
|
|
)
|
|
|
|
# Cualquier otro estado lo tratamos como error
|
|
message = error_text or result_payload.get("message") or last_payload.get("message") or state
|
|
|
|
return CoveExternalResult(
|
|
status="error",
|
|
message=str(message),
|
|
cove_number=None,
|
|
vucem_operation_num=None,
|
|
raw_response={**last_payload, "external_task_id": external_task_id},
|
|
)
|
|
|
|
|
|
@celery_app.task(bind=True, name="factura_cove_generate")
|
|
def factura_cove_generate(
|
|
self: Task,
|
|
invoice_id: int,
|
|
tenant_id: int,
|
|
company_id: int,
|
|
recipient_email: str | None = None,
|
|
) -> dict:
|
|
"""
|
|
Tarea Celery para preparar (y en el futuro generar) un COVE a partir de una factura.
|
|
|
|
Actualmente:
|
|
- Valida prerrequisitos de factura y configuración VU.
|
|
- Construye el payload FacturaCoveRequest (sin llamar aún al webservice externo).
|
|
- Devuelve un resultado estándar indicando éxito o errores de validación.
|
|
|
|
En el futuro se puede extender para:
|
|
- Invocar al servicio externo de COVE.
|
|
- Persistir número de COVE / operación VUCEM en la factura.
|
|
"""
|
|
db = CoreSessionLocal()
|
|
|
|
try:
|
|
_progress(self, 5, "Validando factura para COVE...")
|
|
service = FacturaCoveDomainService(db)
|
|
|
|
# Esta llamada valida todo y construye el payload; si algo falla, lanza ValidationException
|
|
request_payload = service.build_factura_cove_request(
|
|
invoice_id=invoice_id,
|
|
tenant_id=tenant_id,
|
|
company_id=company_id,
|
|
recipient_email=recipient_email,
|
|
)
|
|
|
|
_progress(self, 80, "Enviando solicitud al servicio COVE...")
|
|
|
|
# Integración externa que encola la generación de COVE en Ventanilla Única
|
|
external = CoveExternalService()
|
|
external_result = external.generate_cove(request_payload)
|
|
|
|
# Si el servicio externo devolvió un error inmediato (por ejemplo 422),
|
|
# devolvemos ese resultado tal cual sin hacer polling adicional.
|
|
if external_result.status in {"error", "validation_error"}:
|
|
result = GenerateCoveResult(
|
|
status=external_result.status,
|
|
message=external_result.message,
|
|
invoice_id=invoice_id,
|
|
cove_number=external_result.cove_number,
|
|
vucem_operation_num=external_result.vucem_operation_num,
|
|
external_task_id=(
|
|
external_result.raw_response.get("task_id") if external_result.raw_response else None
|
|
),
|
|
external_response=external_result.raw_response,
|
|
errors=None,
|
|
)
|
|
return result.model_dump()
|
|
|
|
external_task_id = (
|
|
external_result.raw_response.get("task_id") if external_result.raw_response else None
|
|
)
|
|
|
|
# Si la factura quedó encolada en VU y tenemos un task_id externo, hacemos polling
|
|
# al endpoint de status para acompañar el progreso completo hasta obtener COVE.
|
|
if external_task_id and external_result.status in {"external_queued", "queued", "success"}:
|
|
_progress(
|
|
self,
|
|
85,
|
|
"Factura enviada a Ventanilla Única, consultando estado de COVE...",
|
|
)
|
|
final_external = _poll_external_status(self, external, external_task_id)
|
|
else:
|
|
# Fallback: usamos el resultado tal cual devolvió el endpoint de generación
|
|
final_external = external_result
|
|
|
|
# Intentar persistir COVE / número de operación en la factura cuando sea éxito.
|
|
try:
|
|
_save_cove_result(db, invoice_id, final_external)
|
|
except Exception:
|
|
# No fallamos la tarea por errores de persistencia; solo los registramos.
|
|
logger.exception("Error guardando COVE en la factura %s", invoice_id)
|
|
|
|
_progress(self, 100, "Proceso de COVE finalizado.")
|
|
|
|
result = GenerateCoveResult(
|
|
status=final_external.status,
|
|
message=final_external.message,
|
|
invoice_id=invoice_id,
|
|
cove_number=final_external.cove_number,
|
|
vucem_operation_num=final_external.vucem_operation_num,
|
|
external_task_id=(
|
|
final_external.raw_response.get("external_task_id")
|
|
or final_external.raw_response.get("task_id")
|
|
if final_external.raw_response
|
|
else None
|
|
),
|
|
external_response=final_external.raw_response,
|
|
errors=None,
|
|
)
|
|
return result.model_dump()
|
|
|
|
except ValidationException as exc:
|
|
db.rollback()
|
|
logger.info("Validation error in factura_cove_generate: %s", exc.message)
|
|
result = GenerateCoveResult(
|
|
status="validation_error",
|
|
message=exc.message,
|
|
invoice_id=invoice_id,
|
|
errors=exc.errors,
|
|
)
|
|
return result.model_dump()
|
|
except Exception as exc: # pragma: no cover - errores inesperados de runtime
|
|
db.rollback()
|
|
logger.exception("Unexpected error in factura_cove_generate")
|
|
result = GenerateCoveResult(
|
|
status="error",
|
|
message=str(exc),
|
|
invoice_id=invoice_id,
|
|
errors=None,
|
|
)
|
|
return result.model_dump()
|
|
finally:
|
|
db.close()
|
|
|