Files
microservice/api/api_v2/modules/edocs/services.py

359 lines
14 KiB
Python

import base64
import re
import logging
import os
from typing import Any, Dict, Optional
import xml.etree.ElementTree as ET
from fastapi import HTTPException
from utils.helpers import soap_error
from .controllers import edocs_rest_controller, edocs_vu_controller
from ..common import create_service_response, create_error_response
# Logger para el módulo
logger = logging.getLogger("app.api")
# --- FUNCIONES AUXILIARES ---
def _get_file_name(**kwargs) -> str:
pedimento = kwargs.get('pedimento', {})
pedimento_app = pedimento.get('pedimento_app', 'N/A')
idEDocument = kwargs['edoc'].get('numero_edocument', 'N/A')
return f"vu_ED_{pedimento_app}_{idEDocument}.pdf"
# --- FUNCIONES DE SERVICIO ---
async def obtener_edoc(**kwargs):
credencial = kwargs.get('credencial', {})
usuario = credencial.get('user', '')
password = credencial.get('password', '')
doc = kwargs.get('edoc', {})
numero_documento = kwargs['edoc'].get('numero_edocument', '')
soap_headers = {
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': 'http://tempuri.org/IServicioEdocument/GetDocumento'
}
soap_xml = edocs_vu_controller.generate_edocument_template(username=usuario, password=password, idEDocument=numero_documento)
# Enviar documento a EFC
try:
pedimento_efc = kwargs.get('pedimento', {})
pedimento_app = pedimento_efc.get('pedimento_app', 'N/A')
organizacion_efc = pedimento_efc.get('organizacion', None)
pedimento_id_efc = pedimento_efc.get("id", None)
file_name_request = f"VU_ED_{pedimento_app}_{numero_documento}_REQUEST.xml"
document_response = await edocs_rest_controller.post_or_update_document(
soap_response=soap_xml,
organizacion=organizacion_efc,
pedimento=pedimento_id_efc,
file_name=file_name_request,
document_type=21,
identifier=numero_documento,
)
except Exception as e:
logger.error(f"Error al enviar documento request: {e}")
response = await edocs_vu_controller.make_request_async(
"Ventanilla-HA/ServicioEdocument/ServicioEdocument.svc",
data=soap_xml,
headers=soap_headers
)
# Validar respuesta del servicio SOAP
if response is None:
logger.error("No se obtuvo respuesta del servicio SOAP")
raise HTTPException(
status_code=500,
detail=create_error_response(
message="Error al contactar el servicio SOAP",
errors=["No se obtuvo respuesta del servicio"],
metadata={
"edoc_number": numero_documento,
"username": usuario
}
)
)
if response.status_code != 200:
logger.error(f"Error en la solicitud SOAP: {response.status_code}")
raise HTTPException(
status_code=response.status_code,
detail=create_error_response(
message="Error en la solicitud SOAP",
errors=[f"Código de estado: {response.status_code}"],
data={"soap_response": response.text[:500]},
metadata={
"status_code": response.status_code,
"edoc_number": numero_documento
}
)
)
if soap_error(response):
logger.error("Respuesta SOAP contiene error de VUCEM")
_pedimento_efc = kwargs.get('pedimento', {})
_file_name_error = f"VU_ED_{_pedimento_efc.get('pedimento_app', 'N/A')}_{numero_documento}_RESPONSE_ERROR.xml"
logger.info(f"Guardando RESPONSE_ERROR doc_type=22: file={_file_name_error}, organizacion={_pedimento_efc.get('organizacion')}, pedimento={_pedimento_efc.get('id')}")
_doc_result = await edocs_rest_controller.post_or_update_document(
soap_response=response.text,
organizacion=_pedimento_efc.get('organizacion'),
pedimento=_pedimento_efc.get('id'),
file_name=_file_name_error,
document_type=22,
identifier=numero_documento,
)
if _doc_result is None:
logger.error("post_or_update_document retornó None para RESPONSE_ERROR doc_type=22 — archivo físico sin registro en BD")
else:
logger.info(f"RESPONSE_ERROR registrado en BD: id={_doc_result.get('id')}, document_type={_doc_result.get('document_type')}")
raise HTTPException(
status_code=500,
detail=create_error_response(
message="Error en la respuesta del servicio SOAP",
errors=["La respuesta contiene un error de VUCEM"],
data={"soap_response": response.text[:500]},
metadata={"edoc_number": numero_documento}
)
)
try:
edoc_base64 = extract_pdf_bytes_from_xml_content(response.text)
except Exception as ve:
logger.error(f"Error extrayendo contenido del XML: {ve}")
_pedimento_efc = kwargs.get('pedimento', {})
_file_name_error = f"VU_ED_{_pedimento_efc.get('pedimento_app', 'N/A')}_{numero_documento}_RESPONSE_ERROR.xml"
logger.info(f"Guardando RESPONSE_ERROR doc_type=22 (parse error): file={_file_name_error}")
_doc_result = await edocs_rest_controller.post_or_update_document(
soap_response=response.text,
organizacion=_pedimento_efc.get('organizacion'),
pedimento=_pedimento_efc.get('id'),
file_name=_file_name_error,
document_type=22,
identifier=numero_documento,
)
if _doc_result is None:
logger.error("post_document retornó None para RESPONSE_ERROR doc_type=22 (parse error)")
else:
logger.info(f"RESPONSE_ERROR registrado en BD: id={_doc_result.get('id')}, document_type={_doc_result.get('document_type')}")
raise HTTPException(
status_code=500,
detail=create_error_response(
message="Error al procesar la respuesta SOAP",
errors=[str(ve)],
metadata={"edoc_number": numero_documento}
)
)
if edoc_base64 is None:
logger.error("No se pudo extraer el documento de la respuesta SOAP")
raise HTTPException(
status_code=500,
detail=create_error_response(
message="Error al extraer el documento",
errors=["No se pudo encontrar el documento en la respuesta SOAP"],
metadata={"edoc_number": numero_documento}
)
)
pdf_bytes = edoc_base64['pdf_bytes']
if not pdf_bytes:
logger.error("No se pudo decodificar el documento PDF")
raise HTTPException(
status_code=500,
detail=create_error_response(
message="Error al decodificar el documento",
errors=["El contenido del documento está vacío o es inválido"],
metadata={
"edoc_number": numero_documento,
"has_cadena_original": bool(edoc_base64.get('cadena_original')),
"has_sello_digital": bool(edoc_base64.get('sello_digital'))
}
)
)
# Validar formato PDF
if not pdf_bytes.startswith(b'%PDF'):
logger.error("El contenido decodificado no es un PDF válido")
raise HTTPException(
status_code=500,
detail=create_error_response(
message="El documento recibido no es un PDF válido",
errors=["El contenido no tiene el formato PDF esperado"],
metadata={
"edoc_number": numero_documento,
"content_start": str(pdf_bytes[:20])
}
)
)
pedimento = kwargs.get('pedimento', {})
numero_documento = kwargs['edoc'].get('numero_edocument', '')
_file_name = _get_file_name(**kwargs)
organizacion = pedimento.get("organizacion", None)
pedimento_id = pedimento.get("id", None)
# No guardaremos el archivo localmente por seguridad
logger.debug(f"Procesando documento {numero_documento} para pedimento {pedimento_id}")
rest_response = await edocs_rest_controller.post_or_update_document(
binary_content=pdf_bytes,
organizacion=organizacion,
pedimento=pedimento_id,
file_name=_file_name,
document_type=5,
identifier=numero_documento,
)
print(f"rest_response >>>> {rest_response}")
if rest_response is None:
logger.error("Error al enviar el documento a la API interna")
raise HTTPException(
status_code=500,
detail=create_error_response(
message="Error al guardar el documento en el sistema",
errors=["No se pudo enviar el documento a la API interna"],
metadata={
"file_name": _file_name,
"edoc_number": numero_documento
}
)
)
if rest_response.get("id") is None:
logger.error("Respuesta de API interna sin ID válido")
raise HTTPException(
status_code=500,
detail=create_error_response(
message="Error al procesar la respuesta del sistema",
errors=["La respuesta de la API no contiene un ID válido"],
data={"api_response": rest_response}
)
)
logger.info("Documento enviado, actualizando status de Edoc")
try:
edoc_status_response = await change_edocument_status(
edoc=doc,
status=True,
pedimento=pedimento
)
except Exception as e:
logger.warning(f"Error al actualizar estado del documento: {e}")
# No fallamos aquí porque el documento ya se guardó exitosamente
logger.info(f"E-document {numero_documento} procesado exitosamente")
return create_service_response(
message=f"E-document {numero_documento} procesado exitosamente",
data={
"document_response": rest_response,
"file_name": _file_name,
"numero_documento": numero_documento,
"edoc_update_response": edoc_status_response if edoc_status_response else None
},
metadata={
"document_type": 5,
"pedimento_app": pedimento.get('pedimento_app'),
"organizacion": organizacion,
"content_type": "application/pdf",
"has_cadena_original": bool(edoc_base64.get('cadena_original')),
"has_sello_digital": bool(edoc_base64.get('sello_digital'))
}
)
async def change_edocument_status(edoc: dict, status: bool, pedimento: dict):
# estaba acualizando mal el status de descarga, actualizaba otro campo que no le correspondia
data = {
"id": edoc.get("id"),
"edocument_descargado": status,
"edocument_estado": "descargado" if status else "pendiente",
"numero_edocument": edoc.get("numero_edocument"),
"pedimento": pedimento.get("id"),
"organizacion": pedimento.get("organizacion"),
}
response = await edocs_rest_controller.put_edocument(edocument_id=edoc.get("id"), data=data)
# Nunca reportar éxito si el estatus no quedó persistido (T2026-05-027)
if response is None:
logger.error(f"No se pudo actualizar el estatus del EDocument {edoc.get('numero_edocument')} en la API")
raise Exception(f"Fallo al actualizar el estatus del EDocument {edoc.get('numero_edocument')}")
return response
async def marcar_error_edocument(edoc: dict, pedimento: dict, mensaje: str, definitivo: bool = False):
"""
Reporta un fallo de descarga al registro de negocio (T2026-05-027).
- definitivo=False (fallo transitorio): solo registra ultimo_error; el registro
permanece 'pendiente' y el tope de intentos automáticos del backend gobierna
la transición a 'error'.
- definitivo=True (fallo permanente): transiciona de inmediato a 'error';
queda fuera del ciclo automático, solo reproceso manual o reset.
"""
data = {
"id": edoc.get("id"),
"ultimo_error": (mensaje or "Error de descarga en VUCEM")[:2000],
"numero_edocument": edoc.get("numero_edocument"),
"pedimento": pedimento.get("id"),
"organizacion": pedimento.get("organizacion"),
}
if definitivo:
data["edocument_estado"] = "error"
response = await edocs_rest_controller.put_edocument(edocument_id=edoc.get("id"), data=data)
if response is None:
logger.error(f"No se pudo registrar el error del EDocument {edoc.get('numero_edocument')} en la API")
return response
NS = {
's': 'http://schemas.xmlsoap.org/soap/envelope/',
't': 'http://tempuri.org/',
'i': 'http://www.w3.org/2001/XMLSchema-instance'
}
# mejorar la extraccion de seccion File
def extract_pdf_bytes_from_xml_content(xml_content: str):
root = ET.fromstring(xml_content)
errores = root.find('.//t:Errores', NS)
tiene_error = root.find('.//t:TieneError', NS)
if tiene_error is not None and tiene_error.text == 'true':
err_msg = errores.text if errores is not None else 'Error desconocido'
raise Exception(f"VUCEM informa error: {err_msg}")
file_elem = root.find('.//t:File', NS)
if file_elem is None or file_elem.get('{http://www.w3.org/2001/XMLSchema-instance}nil') == 'true' or not file_elem.text:
raise ValueError("No se encontró el tag <File> con contenido válido.")
base64_data = file_elem.text.strip().replace('\n', '').replace('\r', '')
pdf_bytes = base64.b64decode(base64_data)
# Extraer CadenaOriginal y SelloDigital con namespaces
cadena_original = None
sello_digital = None
cadena_elem = root.find('.//t:CadenaOriginal', NS)
if cadena_elem is not None and cadena_elem.get('{http://www.w3.org/2001/XMLSchema-instance}nil') != 'true':
cadena_original = cadena_elem.text.strip() if cadena_elem.text else None
sello_elem = root.find('.//t:SelloDigital', NS)
if sello_elem is not None and sello_elem.get('{http://www.w3.org/2001/XMLSchema-instance}nil') != 'true':
sello_digital = sello_elem.text.strip() if sello_elem.text else None
return {
"pdf_bytes": pdf_bytes,
"cadena_original": cadena_original,
"sello_digital": sello_digital
}