193 lines
7.2 KiB
Python
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
|
|
|