Files
plantillas-proyectos/backend/api/v1/modules/a76/factura_cove/external_service.py
2026-04-13 15:57:40 -06:00

193 lines
7.2 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict
import logging
import httpx
from core.config import settings
from .schemas import FacturaCoveRequest
logger = logging.getLogger(__name__)
@dataclass
class CoveExternalResult:
"""
Resultado simplificado de la llamada al servicio externo de COVE.
Por ahora usamos un stub que simula una respuesta exitosa y devuelve
un número de COVE ficticio para poder probar el flujo end-to-end
(task_id + cove_number) sin depender del ambiente externo.
"""
status: str
message: str | None = None
cove_number: str | None = None
vucem_operation_num: str | None = None
raw_response: Dict[str, Any] | None = None
class CoveExternalService:
"""
Cliente del API externo de COVE.
NOTA IMPORTANTE:
----------------
Esta implementación es, por ahora, un stub que:
- No realiza la llamada HTTP real.
- Genera un número de COVE ficticio basado en los datos de la factura.
Cuando se tenga disponible la URL y contrato exacto del servicio COVE,
este stub se puede reemplazar por una implementación con httpx/requests
que:
- Serialice el FacturaCoveRequest al JSON requerido.
- Realice la petición HTTP.
- Mapee la respuesta real a CoveExternalResult.
"""
def __init__(self) -> None:
"""
Inicializa el cliente usando COVE_API_URL si está definido; de lo contrario,
usa por defecto el endpoint público documentado en:
https://api.vu.aduanasoft.com/docs#/Factura%20COVE/generar_factura_cove_endpoint_api_v1_factura_cove_generar_factura_cove_post
"""
self.base_url = (settings.COVE_API_URL or "").strip() or "https://api.vu.aduanasoft.com"
def generate_cove(self, payload: FacturaCoveRequest) -> CoveExternalResult:
"""
Llama al endpoint externo /api/v1/factura-cove/generar-factura-cove
con el FacturaCoveRequest completo y retorna el resultado tal como
lo reporta el servicio remoto.
"""
url = f"{self.base_url.rstrip('/')}/api/v1/factura-cove/generar-factura-cove"
json_payload = payload.model_dump(mode="json")
configuracion_vu = json_payload.get("configuracion_vu") or {}
logger.info(
"Sending COVE payload: invoice=%s rfc_vu=%s clave_fiel_len=%s clave_ws_len=%s cer_len=%s key_len=%s",
json_payload.get("numero_factura"),
configuracion_vu.get("rfc_usuario_vu"),
len(configuracion_vu.get("clave_fiel") or ""),
len(configuracion_vu.get("clave_webservice") or ""),
len(configuracion_vu.get("archivo_cer_base64") or ""),
len(configuracion_vu.get("archivo_key_base64") or ""),
)
# NOTA: verify=False desactiva la validación de certificado SSL.
# Esto es útil en entornos de desarrollo o cuando el entorno no confía
# en el certificado del endpoint externo. En producción, idealmente
# se debería habilitar la verificación SSL.
with httpx.Client(timeout=30.0, verify=False) as client:
resp = client.post(url, json=json_payload)
# Intentar parsear JSON siempre, incluso en errores 4xx/5xx
try:
data: Dict[str, Any] = resp.json()
except Exception:
data = {}
# Si el servicio externo respondió con error (por ejemplo 422 Validation Error),
# devolvemos un resultado de error rico en información para que la UI pueda
# mostrar el detalle completo.
if resp.status_code >= 400:
# Intentar construir un mensaje amigable
message = None
if isinstance(data, dict):
message = data.get("message")
if not message and "detail" in data:
# FastAPI ValidationError-style: detail: [{loc, msg, type}, ...]
try:
parts = [str(d.get("msg")) for d in data["detail"] if isinstance(d, dict)]
message = "; ".join([p for p in parts if p])
except Exception:
pass
if not message:
message = resp.text or f"HTTP {resp.status_code}"
status = "validation_error" if resp.status_code == 422 else "error"
return CoveExternalResult(
status=status,
message=message,
cove_number=None,
vucem_operation_num=None,
raw_response={
"status_code": resp.status_code,
"body": data,
},
)
# 2xx: según la especificación del servicio externo, al menos devuelve:
# { "task_id": "...", "status": "...", "message": "..." }
raw_status = str(data.get("status") or "queued")
message = data.get("message")
external_task_id = data.get("task_id")
# Caso especial: algunos ambientes de VU regresan status="error" pero un mensaje
# tipo "Factura COVE iniciada para: ... Use el task_id para consultar el estado."
# que en realidad indica que la factura fue aceptada y quedó encolada en VU.
# En ese caso NO lo tratamos como error de negocio, sino como "en cola".
normalized_status = raw_status.lower()
if (
normalized_status == "error"
and isinstance(message, str)
and "Factura COVE iniciada para" in message
):
status = "external_queued"
else:
status = raw_status
# El número de COVE normalmente se obtendrá vía /status/{task_id}; por ahora
# lo dejamos en None y exponemos la respuesta completa para inspección en UI.
return CoveExternalResult(
status=status,
message=message,
cove_number=None,
vucem_operation_num=None,
raw_response={
"task_id": external_task_id,
"status": status,
"message": message,
"raw": data,
},
)
def get_status(self, task_id: str) -> Dict[str, Any]:
"""
Consulta el endpoint externo /api/v1/factura-cove/status/{task_id}
y devuelve el JSON de progreso/resultado tal cual lo envía el servicio.
Ejemplo de respuesta esperada (simplificada):
{
"task_id": "...",
"state": "PROGRESS" | "SUCCESS" | "FAILURE",
"result": null | {...},
"error": null | "...",
"progress": {
"current_step": "Consultando respuesta COVE con número de operación",
"progress": 10.5,
"total_steps": 12,
"task_id": "...",
"numero_operacion": "306658625"
}
}
"""
url = f"{self.base_url.rstrip('/')}/api/v1/factura-cove/status/{task_id}"
# Igual que en generate_cove, desactivamos verify solo para entornos de dev.
with httpx.Client(timeout=30.0, verify=False) as client:
resp = client.get(url)
try:
data: Dict[str, Any] = resp.json()
except Exception:
data = {}
# Adjuntar metadatos mínimos de respuesta HTTP
data.setdefault("status_code", resp.status_code)
return data