From df4024436be8711d8f24d1ed5c66140923307eae Mon Sep 17 00:00:00 2001 From: hreyes Date: Tue, 7 Apr 2026 08:15:02 -0600 Subject: [PATCH] checkpoint --- .../v1/modules/a76/factura_cove/service.py | 90 +++++++++++++++---- backend/core/config.py | 2 + .../invoices/pdf-progress-dialog.svelte | 8 +- 3 files changed, 82 insertions(+), 18 deletions(-) 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}