checkpoint
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user