checkpoint

This commit is contained in:
2026-04-07 08:15:02 -06:00
parent cc3942bbd7
commit df4024436b
3 changed files with 82 additions and 18 deletions

View File

@@ -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,

View File

@@ -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 = ""

View File

@@ -195,9 +195,13 @@
COVE: {lastResult.cove_number}
</span>
{/if}
{#if taskId}
{#if lastResult?.external_task_id}
<span class="mt-1 text-[11px] text-muted-foreground break-all">
Task ID: {taskId}
Task ID COVE: {lastResult.external_task_id}
</span>
{:else if taskId}
<span class="mt-1 text-[11px] text-muted-foreground break-all">
Task interno: {taskId}
</span>
{/if}