diff --git a/backend/api/v1/modules/a76/factura_cove/service.py b/backend/api/v1/modules/a76/factura_cove/service.py
index 21e4199b..19df3840 100644
--- a/backend/api/v1/modules/a76/factura_cove/service.py
+++ b/backend/api/v1/modules/a76/factura_cove/service.py
@@ -1,12 +1,14 @@
from __future__ import annotations
import base64
+import hashlib
from dataclasses import dataclass
from decimal import Decimal
from typing import List, Tuple
from sqlalchemy.orm import Session
+from core.config import settings
from core.database import CoreSessionLocal
from core.exceptions import ValidationException, ErrorCollector
from core.storage_s3 import get_object_bytes, object_exists
@@ -27,6 +29,15 @@ from .schemas import (
)
+# Clave de encriptado/sal para hash de FIEL.
+# TODO: mover a configuración (p. ej. variable de entorno) cuando se habilite el flujo real.
+FIEL_ENCRYPTION_KEY = "6a7f92d3c8d1e5b3b0ac23ff1926a7c9"
+
+# Flag para, en el futuro, activar el uso real de la FIEL hasheada.
+# Mientras sea False, se seguirá usando la clave FIEL hardcodeada de pruebas.
+USE_HASHED_FIEL_FOR_COVE = True
+
+
@dataclass
class InvoiceContext:
invoice: InvoiceHeader
@@ -78,6 +89,19 @@ class FacturaCoveDomainService:
return InvoiceContext(invoice=invoice, broker=broker, vu=vu)
+ def _hash_fiel(self, raw_fiel: str) -> str:
+ """
+ Calcula un hash determinista (base64) de la clave FIEL usando una
+ clave de encriptado fija. Este valor es el que se enviará como
+ `clave_fiel` al API externo cuando se active USE_HASHED_FIEL_FOR_COVE.
+ """
+ if not raw_fiel:
+ return ""
+
+ data = f"{FIEL_ENCRYPTION_KEY}:{raw_fiel}".encode("utf-8")
+ digest = hashlib.sha256(data).digest()
+ return base64.b64encode(digest).decode("ascii")
+
def _build_configuracion_vu(self, ctx: InvoiceContext, errors: ErrorCollector) -> ConfiguracionVU | None:
"""
Construye la sección configuracion_vu usando CustomsBrokerVU + S3.
@@ -96,14 +120,21 @@ class FacturaCoveDomainService:
)
# Determinar clave FIEL efectiva.
- # Mientras se define el flujo real de administración de FIEL para VU,
- # usamos la misma clave de ejemplo que se usó en el JSON que funciona.
- hardcoded_fiel_key = "amH8Ax3EJoUBMuQSu4TAzQ=="
- clave_fiel_value = hardcoded_fiel_key
+ # Orden de prioridad:
+ # 1) Si USE_HASHED_FIEL_FOR_COVE=True y existe vu.fiel_access_key, se hashea.
+ # 2) En caso contrario, se usa la variable de entorno COVE_FIEL_PASSWORD.
+ clave_fiel_value = ""
+
+ if USE_HASHED_FIEL_FOR_COVE and vu and getattr(vu, "fiel_access_key", None):
+ # Usar la clave capturada en VU, hasheada con hashlib + FIEL_ENCRYPTION_KEY
+ clave_fiel_value = self._hash_fiel(vu.fiel_access_key or "")
+
+ if not clave_fiel_value:
+ # Fallback: usar la contraseña FIEL configurada vía entorno
+ clave_fiel_value = (settings.COVE_FIEL_PASSWORD or "").strip()
# Validación básica de credenciales VU: para COVE necesitamos al menos
- # un usuario de web service (VU o DODA); la clave FIEL se inyecta desde
- # la configuración de ejemplo anterior.
+ # un usuario de web service (VU o DODA) y una clave FIEL no vacía.
if not effective_ws_user:
errors.add_error(
field="vu",
@@ -115,6 +146,17 @@ class FacturaCoveDomainService:
code="MISSING_VU_CREDENTIALS",
)
+ if not clave_fiel_value:
+ errors.add_error(
+ field="vu.clave_fiel",
+ message="La clave FIEL para COVE no está configurada. Define COVE_FIEL_PASSWORD o captura la FIEL en VU.",
+ solution=[
+ "Configura la variable de entorno COVE_FIEL_PASSWORD con la contraseña FIEL de VUCEM, "
+ "o captura la clave FIEL en la configuración VU del agente aduanal."
+ ],
+ code="MISSING_FIEL_PASSWORD",
+ )
+
if not (vu.certificate_path and vu.key_path):
errors.add_error(
field="vu",
@@ -412,7 +454,8 @@ class FacturaCoveDomainService:
# Saltar partidas sin cantidad válida
continue
- cantidad = Decimal(str(qty_model.quantity))
+ # Cantidad: normalizar a EXACTAMENTE 2 decimales (ej. 12.23)
+ cantidad = Decimal(str(qty_model.quantity)).quantize(Decimal("0.01"))
# Descripción genérica: priorizar descripción de parte / inglés / español
descripcion = ""
@@ -475,21 +518,24 @@ class FacturaCoveDomainService:
or Decimal("0")
)
- valor_total = Decimal(str(base_total or 0))
+ # Valor total: normalizar a EXACTAMENTE 2 decimales
+ valor_total = Decimal(str(base_total or 0)).quantize(Decimal("0.01"))
if cantidad > 0:
- valor_unitario = (valor_total / cantidad).quantize(
- Decimal("0.000001")
- )
+ # Valor unitario también a EXACTAMENTE 2 decimales
+ valor_unitario = (valor_total / cantidad).quantize(Decimal("0.01"))
else:
valor_unitario = Decimal("0")
- # Valor en dólares: si ya existe, lo usamos; si no, convertimos suponiendo que los totales ya están en USD
+ # Valor en dólares: si ya existe, lo usamos; si no, asumimos que los totales ya están en USD.
if fin_model.value_total_usd:
valor_dolares = Decimal(str(fin_model.value_total_usd))
elif tipo_moneda == "USD":
valor_dolares = valor_total
else:
valor_dolares = Decimal("0")
+
+ # Normalizar valor en dólares a EXACTAMENTE 2 decimales
+ valor_dolares = valor_dolares.quantize(Decimal("0.01"))
else:
errors.add_error(
field="mercancias",
@@ -556,9 +602,18 @@ class FacturaCoveDomainService:
raise ValidationException("No se puede generar COVE desde la factura", errors=errors.get_errors())
# Campos genéricos que se pueden poblar de forma segura
- raw_tipo_operacion = (ctx.invoice.operation_type or "").strip()
- # Normalizar tipo_operacion a MAYÚSCULAS (ej. IMP, EXP) con longitud acotada
- tipo_operacion = raw_tipo_operacion.upper()[:10]
+ raw_tipo_operacion = (ctx.invoice.operation_type or "").strip().lower()
+ # Mapear tipo_operacion al código esperado por el API de COVE
+ # Ejemplos:
+ # - IMP / importación -> "TOCE.IMP"
+ # - EXP / exportación -> "TOCE.EXP"
+ if raw_tipo_operacion in {"imp", "import", "importacion", "importación"}:
+ tipo_operacion = "TOCE.IMP"
+ elif raw_tipo_operacion in {"exp", "export", "exportacion", "exportación"}:
+ tipo_operacion = "TOCE.EXP"
+ else:
+ # Fallback seguro: usar valor por defecto de importación
+ tipo_operacion = "TOCE.IMP"
numero_factura = (ctx.invoice.invoice_number or "").strip()[:50]
fecha_expedicion = ctx.invoice.invoice_date or ctx.invoice.emission_date or ctx.invoice.capture_date
@@ -598,8 +653,11 @@ class FacturaCoveDomainService:
return FacturaCoveRequest(
configuracion_vu=configuracion_vu,
+ # El RFC de consulta NO debe ser igual al RFC del que registra el comprobante.
+ # Usamos como RFC de consulta el RFC del agente aduanal (customs broker),
+ # y dejamos que configuracion_vu.rfc_usuario_vu represente al contribuyente.
rfc_consulta=(
- (ctx.vu.query_tax_id or "").strip().upper() if ctx.vu and ctx.vu.query_tax_id else ""
+ (ctx.broker.tax_id or "").strip().upper() if ctx.broker and ctx.broker.tax_id else ""
),
tipo_figura=tipo_figura,
numero_factura=numero_factura,
diff --git a/backend/core/config.py b/backend/core/config.py
index 84a450c4..e52af02d 100644
--- a/backend/core/config.py
+++ b/backend/core/config.py
@@ -56,6 +56,8 @@ class Settings(BaseSettings):
SITAR_API_URL: str = "api.sitar.aduanasoft.com:880"
# Endpoint base para el servicio externo de Factura COVE
COVE_API_URL: str = ""
+ # Clave FIEL (contraseña) para COVE/VUCEM. Debe configurarse vía entorno en entornos reales.
+ COVE_FIEL_PASSWORD: str = ""
SITAR_API_USER: str = ""
SITAR_API_PASSWORD: str = ""
diff --git a/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte
index 6618906f..76485457 100644
--- a/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte
+++ b/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte
@@ -195,9 +195,13 @@
COVE: {lastResult.cove_number}
{/if}
- {#if taskId}
+ {#if lastResult?.external_task_id}
- Task ID: {taskId}
+ Task ID COVE: {lastResult.external_task_id}
+
+ {:else if taskId}
+
+ Task interno: {taskId}
{/if}