Se agregaron los moduloes de api_v2
This commit is contained in:
0
api/api_v2/modules/coves/__init__.py
Normal file
0
api/api_v2/modules/coves/__init__.py
Normal file
103
api/api_v2/modules/coves/controllers.py
Normal file
103
api/api_v2/modules/coves/controllers.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from typing import Any, Dict
|
||||
from controllers.RESTController import APIRESTController
|
||||
from controllers.SOAPController import VUCEMController
|
||||
|
||||
class CovesController(APIRESTController):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
async def get_cer(self, id: str) -> bytes:
|
||||
"""
|
||||
Método para obtener un certificado específico desde la API (como binario).
|
||||
Args:
|
||||
id: UUID del certificado a consultar
|
||||
Returns:
|
||||
bytes: Contenido binario del certificado
|
||||
"""
|
||||
return await self._make_request_async('GET', f'vucem/vucem/{id}/download_cer/', return_bytes=True)
|
||||
|
||||
async def get_key(self, id: str) -> bytes:
|
||||
"""
|
||||
Método para obtener una llave específica desde la API (como binario).
|
||||
Args:
|
||||
id: UUID de la llave a consultar
|
||||
Returns:
|
||||
bytes: Contenido binario de la llave
|
||||
"""
|
||||
return await self._make_request_async('GET', f'vucem/vucem/{id}/download_key/', return_bytes=True)
|
||||
|
||||
async def put_cove_data(self, cove_id, data) -> Dict[str, Any]:
|
||||
return await self._make_request_async('PUT', f'customs/coves/{cove_id}/', data=data)
|
||||
|
||||
|
||||
class CovesVUController(VUCEMController):
|
||||
def __init__(self):
|
||||
super().__init__() # Implementación específica para Coves VU
|
||||
|
||||
def generate_cove_template(self, username: str, password: str, certificado: str, firma: str, cove: str) -> str:
|
||||
"""
|
||||
Genera el template SOAP para consultar un COVE específico
|
||||
|
||||
Args:
|
||||
username: Usuario de VUCEM
|
||||
password: Contraseña de VUCEM
|
||||
certificado: certificado base 64
|
||||
firma: firma a base de cadena original base 64
|
||||
cove: COVE
|
||||
|
||||
Returns:
|
||||
str: Template SOAP XML completo
|
||||
"""
|
||||
soap_template = f'''
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
|
||||
xmlns:con="http://www.ventanillaunica.gob.mx/ConsultarEdocument/"
|
||||
xmlns:oxml="http://www.ventanillaunica.gob.mx/cove/ws/oxml/">
|
||||
<soapenv:Header>
|
||||
<wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
|
||||
<wsse:UsernameToken>
|
||||
<wsse:Username>{username}</wsse:Username>
|
||||
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">{password}</wsse:Password>
|
||||
</wsse:UsernameToken>
|
||||
</wsse:Security>
|
||||
</soapenv:Header>
|
||||
<soapenv:Body>
|
||||
<con:ConsultarEdocumentRequest>
|
||||
<con:request>
|
||||
<con:firmaElectronica>
|
||||
<oxml:certificado>{certificado}</oxml:certificado>
|
||||
<oxml:cadenaOriginal>|{username}|{cove}|</oxml:cadenaOriginal>
|
||||
<oxml:firma>{firma}</oxml:firma>
|
||||
</con:firmaElectronica>
|
||||
<con:criterioBusqueda>
|
||||
<con:eDocument>{cove}</con:eDocument>
|
||||
</con:criterioBusqueda>
|
||||
</con:request>
|
||||
</con:ConsultarEdocumentRequest>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
'''
|
||||
return soap_template
|
||||
|
||||
def generate_acuse_template(self, username: str, password: str, cove: str) -> str:
|
||||
soap_template = f'''
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:oxml="http://www.ventanillaunica.gob.mx/consulta/acuses/oxml">
|
||||
<soapenv:Header>
|
||||
<wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
|
||||
<wsse:UsernameToken>
|
||||
<wsse:Username>{username}</wsse:Username>
|
||||
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">{password}</wsse:Password>
|
||||
</wsse:UsernameToken>
|
||||
</wsse:Security>
|
||||
</soapenv:Header>
|
||||
<soapenv:Body>
|
||||
<oxml:consultaAcusesPeticion>
|
||||
<idEdocument>{cove}</idEdocument>
|
||||
</oxml:consultaAcusesPeticion>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
'''
|
||||
return soap_template
|
||||
|
||||
coves_rest_controller = CovesController()
|
||||
coves_vu_controller = CovesVUController()
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from .schema import CoveBaseSchema
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
router = APIRouter()
|
||||
# Aquí puedes definir tus endpoints relacionados con COVES usando el esquema CoveBaseSchema
|
||||
|
||||
@router.post("/cove/", response_model=CoveBaseSchema)
|
||||
async def create_cove(cove: CoveBaseSchema):
|
||||
# Lógica para crear un COVE
|
||||
return cove
|
||||
58
api/api_v2/modules/coves/routers.py
Normal file
58
api/api_v2/modules/coves/routers.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from .schemas import CoveListSchema, CoveRequestSchema
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from .tasks import process_cove_request, process_acuse_cove_request
|
||||
|
||||
router = APIRouter()
|
||||
# Aquí puedes definir tus endpoints relacionados con COVES usando el esquema CoveBaseSchema
|
||||
|
||||
|
||||
|
||||
@router.post("/services/cove/", response_model=dict)
|
||||
async def get_cove(cove: CoveRequestSchema):
|
||||
# Lógica para obtener un COVE
|
||||
task = process_cove_request.delay(cove.model_dump())
|
||||
return {"task_id": task.id, "status": "submitted"}
|
||||
|
||||
|
||||
@router.post("/services/all/coves", response_model=dict)
|
||||
async def get_coves(coves_request: CoveListSchema):
|
||||
# Lógica para obtener un COVE
|
||||
task_ids = []
|
||||
coves_dict = coves_request.model_dump()
|
||||
for cove in coves_dict.get('coves', []):
|
||||
cove_dict = {
|
||||
"cove": cove,
|
||||
"pedimento": coves_dict.get('pedimento'),
|
||||
"credencial": coves_dict.get('credencial')
|
||||
}
|
||||
task = process_cove_request.delay(cove_dict)
|
||||
task_ids.append(task.id)
|
||||
|
||||
|
||||
return {"task_id": task.id, "coves_tasks_ids": task_ids, "status": "submitted"}
|
||||
|
||||
|
||||
@router.post("/services/acuse/cove/", response_model=dict)
|
||||
async def get_acuse_cove(cove: CoveRequestSchema):
|
||||
# Lógica para obtener un COVE
|
||||
task = process_acuse_cove_request.delay(cove.model_dump())
|
||||
return {"task_id": task.id, "status": "submitted"}
|
||||
|
||||
@router.post("/services/all/acuse/cove/")
|
||||
async def get_acuses_cove(coves_request: CoveListSchema):
|
||||
# Lógica para obtener un COVE
|
||||
task_ids = []
|
||||
coves_dict = coves_request.model_dump()
|
||||
for cove in coves_dict.get('coves', []):
|
||||
acuse_dict = {
|
||||
"cove": cove,
|
||||
"pedimento": coves_dict.get('pedimento'),
|
||||
"credencial": coves_dict.get('credencial')
|
||||
}
|
||||
task = process_acuse_cove_request.delay(acuse_dict)
|
||||
task_ids.append(task.id)
|
||||
|
||||
return {"task_ids": task_ids, "status": "submitted", "total": len(task_ids)}
|
||||
@@ -1,12 +0,0 @@
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
from api.api_v2.modules.pedimentos.schema import PedimentoBaseSchema
|
||||
from schemas.CredencialSchema import CredencialBaseSchema
|
||||
|
||||
class CoveBaseSchema(BaseModel):
|
||||
cove: str = Field(..., description="ID del COVE asociado")
|
||||
pedimento: PedimentoBaseSchema
|
||||
credenciales: CredencialBaseSchema
|
||||
21
api/api_v2/modules/coves/schemas.py
Normal file
21
api/api_v2/modules/coves/schemas.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
from api.api_v2.modules.pedimentos.schemas import PedimentoBaseSchema
|
||||
from schemas.CredencialSchema import CredencialBaseSchema
|
||||
|
||||
class CoveBaseSchema(BaseModel):
|
||||
id: int = Field(..., description="ID único del COVE")
|
||||
cove: str = Field(..., description="Numero del COVE")
|
||||
|
||||
class CoveRequestSchema(BaseModel):
|
||||
cove: CoveBaseSchema
|
||||
pedimento: PedimentoBaseSchema
|
||||
credencial: CredencialBaseSchema
|
||||
|
||||
class CoveListSchema(BaseModel):
|
||||
coves: list[CoveBaseSchema] = Field(..., description="Lista de COVEs")
|
||||
pedimento: PedimentoBaseSchema
|
||||
credencial: CredencialBaseSchema
|
||||
@@ -1,105 +0,0 @@
|
||||
import base64
|
||||
from http.client import HTTPException
|
||||
import os
|
||||
from controllers.RESTController import rest_controller
|
||||
from controllers.SOAPController import soap_controller
|
||||
from cryptography.hazmat.primitives import serialization, hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.serialization import load_der_private_key
|
||||
import tempfile
|
||||
|
||||
|
||||
def sign_chain_original(key_path: str, password: str, cadena_original: str) -> str:
|
||||
with open(key_path, 'rb') as key_file:
|
||||
private_key = load_der_private_key(
|
||||
key_file.read(),
|
||||
password=password.encode() if password else None
|
||||
)
|
||||
|
||||
signature = private_key.sign(
|
||||
cadena_original.encode(),
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA256()
|
||||
)
|
||||
|
||||
return base64.b64encode(signature).decode()
|
||||
|
||||
async def fetch_sign_and_cer(cadena_original: str, username: str, credenciales: dict, **kwargs):
|
||||
cer = await rest_controller.get_cer(credenciales['id'])
|
||||
if cer is None:
|
||||
raise HTTPException(status_code=500, detail="No se pudo obtener el certificado para firmar el COVE")
|
||||
certificado = base64.b64encode(cer).decode('utf-8')
|
||||
|
||||
# Obtener la key como binario y guardarla en un archivo temporal
|
||||
import tempfile
|
||||
key_bytes = await rest_controller.get_key(credenciales['id'])
|
||||
if key_bytes is None:
|
||||
raise HTTPException(status_code=500, detail="No se pudo obtener la llave privada para firmar el COVE")
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tmp_key_file:
|
||||
tmp_key_file.write(key_bytes)
|
||||
tmp_key_path = tmp_key_file.name
|
||||
|
||||
# Usar la ruta temporal para firmar
|
||||
firma = sign_chain_original(tmp_key_path, credenciales['efirma'], cadena_original)
|
||||
return firma, certificado, tmp_key_path
|
||||
|
||||
async def consume_ws_get_cove(**kwargs):
|
||||
|
||||
# valdiar kwargs
|
||||
|
||||
# Cadena original que vas a firmar
|
||||
|
||||
try:
|
||||
cadena_original = f"|{username}|{cove['numero_cove']}|"
|
||||
firma, certificado, tmp_key_path = await fetch_sign_and_cer(cadena_original, username, credenciales, **kwargs)
|
||||
os.remove(tmp_key_path) # Eliminar el archivo temporal después de usarlo
|
||||
|
||||
soap_xml = soap_controller.generate_cove_template(
|
||||
username=username,
|
||||
password=credenciales['password'],
|
||||
certificado=certificado,
|
||||
firma=firma,
|
||||
cove=cove,
|
||||
)
|
||||
|
||||
soap_headers = {
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'SOAPAction': '',
|
||||
#'Accept-Encoding': 'gzip,deflate',
|
||||
}
|
||||
|
||||
soap_response = await soap_controller.make_request_async(
|
||||
"ventanilla/ConsultarEdocumentService?wsdl",
|
||||
data=soap_xml,
|
||||
headers=soap_headers
|
||||
)
|
||||
|
||||
if (soap_response) and (not soap_error(soap_response)):
|
||||
remesas = 1 if response_service['pedimento'].get('remesas', 0) else 0
|
||||
patente = response_service['pedimento'].get('patente', 'N/A')
|
||||
aduana = response_service['pedimento'].get('aduana', 'N/A')
|
||||
no_partidas = response_service['pedimento'].get('numero_partidas', 0)
|
||||
tipo_operacion = response_service['pedimento'].get('tipo_operacion', 'N/A')
|
||||
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
|
||||
_file_name = f"vu_COVE_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}_{cove['numero_cove']}.xml"
|
||||
|
||||
document_response = await rest_controller.post_document(
|
||||
soap_response=soap_response,
|
||||
organizacion=response_service['organizacion'],
|
||||
pedimento=response_service['pedimento']['id'],
|
||||
file_name=_file_name,
|
||||
document_type=8,
|
||||
)
|
||||
|
||||
return {
|
||||
"servicio": response_service,
|
||||
"documento": document_response
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
|
||||
except HTTPException:
|
||||
# Re-lanzar HTTPExceptions sin modificar
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
raise HTTPException(status_code=500, detail=f"Error interno al procesar acuse cove: {str(e)}")
|
||||
469
api/api_v2/modules/coves/services.py
Normal file
469
api/api_v2/modules/coves/services.py
Normal file
@@ -0,0 +1,469 @@
|
||||
import base64
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from fastapi import HTTPException
|
||||
from controllers.RESTController import rest_controller
|
||||
from controllers.SOAPController import soap_controller
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.serialization import load_der_private_key
|
||||
import tempfile
|
||||
|
||||
from utils.helpers import soap_error
|
||||
from .controllers import coves_vu_controller, coves_rest_controller
|
||||
|
||||
# Logger para el módulo
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Logica de negocio para consumir el servicio SOAP de VUCEM y procesar la respuesta
|
||||
async def consume_ws_get_cove(**kwargs):
|
||||
"""
|
||||
Consume el servicio SOAP para obtener un COVE y procesar la respuesta.
|
||||
|
||||
Args:
|
||||
**kwargs: Debe contener 'credencial', 'pedimento' y 'cove'
|
||||
|
||||
Returns:
|
||||
Dict serializable con 'documento' y 'cove_put_response'
|
||||
|
||||
Raises:
|
||||
Exception: Si hay errores en el procesamiento
|
||||
"""
|
||||
try:
|
||||
logger.info("Iniciando procesamiento de COVE")
|
||||
|
||||
credenciales = kwargs.get('credencial')
|
||||
username = credenciales.get('user')
|
||||
pedimento_app = kwargs.get('pedimento', {}).get('pedimento_app', 'N/A')
|
||||
cove = kwargs['cove'].get('cove', None)
|
||||
|
||||
if not credenciales or not username or not cove:
|
||||
raise Exception("Credenciales o COVE no proporcionados correctamente")
|
||||
|
||||
logger.info(f"Procesando COVE: {cove} para usuario: {username}")
|
||||
|
||||
# Generar cadena original y obtener firma/certificado
|
||||
cadena_original = f"|{credenciales.get('user')}|{cove}|"
|
||||
firma, certificado, tmp_key_path = await fetch_sign_and_cer(cadena_original, username, credenciales)
|
||||
|
||||
# Limpiar archivo temporal inmediatamente
|
||||
try:
|
||||
os.remove(tmp_key_path)
|
||||
logger.debug("Archivo temporal de llave eliminado")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error al eliminar archivo temporal: {e}")
|
||||
|
||||
# Generar template SOAP
|
||||
soap_xml = coves_vu_controller.generate_cove_template(
|
||||
username=username,
|
||||
password=credenciales['password'],
|
||||
certificado=certificado,
|
||||
firma=firma,
|
||||
cove=cove,
|
||||
)
|
||||
|
||||
soap_headers = {
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'SOAPAction': '',
|
||||
}
|
||||
|
||||
logger.info("Enviando petición SOAP a VUCEM")
|
||||
soap_response = await coves_vu_controller.make_request_async(
|
||||
"ventanilla/ConsultarEdocumentService?wsdl",
|
||||
data=soap_xml,
|
||||
headers=soap_headers
|
||||
)
|
||||
|
||||
if not soap_response:
|
||||
raise Exception("No se recibió respuesta del servicio SOAP")
|
||||
|
||||
if soap_error(soap_response):
|
||||
raise Exception("Error en la respuesta del servicio SOAP")
|
||||
|
||||
logger.info("Respuesta SOAP exitosa, enviando documento")
|
||||
|
||||
# Enviar documento
|
||||
_file_name = f"vu_COVE_{pedimento_app}_{cove}.xml"
|
||||
document_response = await coves_rest_controller.post_document(
|
||||
soap_response=soap_response,
|
||||
organizacion=kwargs.get('pedimento').get('organizacion'),
|
||||
pedimento=kwargs.get('pedimento').get('id'),
|
||||
file_name=_file_name,
|
||||
document_type=8,
|
||||
)
|
||||
|
||||
logger.info("Documento enviado, actualizando status de COVE")
|
||||
|
||||
# Actualizar status del COVE
|
||||
cove_status_response = await change_cove_status(
|
||||
cove=kwargs.get('cove'),
|
||||
status=True,
|
||||
pedimento=kwargs.get('pedimento')
|
||||
)
|
||||
|
||||
logger.info(f"COVE {cove} procesado exitosamente")
|
||||
|
||||
# Asegurar que la respuesta sea serializable
|
||||
result = {
|
||||
"documento": document_response if document_response else None,
|
||||
"cove_update_response": cove_status_response if cove_status_response else None
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error procesando COVE: {str(e)}", exc_info=True)
|
||||
# Asegurar que no se retornen datos binarios en el error
|
||||
raise Exception(f"Error interno al procesar COVE: {str(e)}")
|
||||
|
||||
|
||||
async def consume_ws_get_acuse_cove(**kwargs):
|
||||
credenciales = kwargs.get('credencial')
|
||||
soap_headers = {
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'SOAPAction': 'http://www.ventanillaunica.gob.mx/ventanilla/ConsultaAcusesService/consultarAcuseCove',
|
||||
'Accept-Encoding': 'gzip,deflate',
|
||||
}
|
||||
|
||||
|
||||
soap_xml = coves_vu_controller.generate_acuse_template(
|
||||
username=credenciales.get('user'),
|
||||
password=credenciales.get('password'),
|
||||
cove=kwargs['cove'].get('cove', None),
|
||||
)
|
||||
|
||||
|
||||
response = await coves_vu_controller.make_request_async(
|
||||
"ventanilla-acuses-HA/ConsultaAcusesServiceWS?wsdl",
|
||||
data=soap_xml,
|
||||
headers=soap_headers
|
||||
)
|
||||
|
||||
|
||||
|
||||
if response is None:
|
||||
raise Exception("No se obtuvo respuesta del servicio SOAP.")
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Error en la solicitud SOAP: {response.status}")
|
||||
|
||||
if (response) and (not soap_error(response)):
|
||||
logger.debug(f"Respuesta SOAP recibida, extrayendo acuse...")
|
||||
acuse_base64 = _extract_acuse_data(response.text)
|
||||
|
||||
if acuse_base64 is None:
|
||||
logger.error("No se encontró elemento acuseDocumento en la respuesta")
|
||||
logger.debug(f"Contenido de respuesta (primeros 1000 chars): {response.text[:1000]}")
|
||||
else:
|
||||
logger.error("Error en respuesta SOAP o soap_error detectado")
|
||||
logger.debug(f"Contenido de respuesta con error: {response.text[:500] if response else 'No response'}")
|
||||
|
||||
if acuse_base64 is None:
|
||||
raise Exception("No se pudo extraer el acuse del documento de la respuesta SOAP.")
|
||||
|
||||
|
||||
pdf_bytes = _decode_acuse_base64_content(acuse_base64)
|
||||
|
||||
if not pdf_bytes:
|
||||
raise HTTPException(status_code=500, detail="No se pudo decodificar el documento del acuse")
|
||||
|
||||
# Validar que el PDF sea válido
|
||||
if not pdf_bytes.startswith(b'%PDF'):
|
||||
logger.warning("El contenido decodificado no parece ser un PDF válido")
|
||||
|
||||
|
||||
# Mejorar el nombre del archivo usando todos los datos relevantes
|
||||
pedimento = kwargs.get('pedimento', {})
|
||||
pedimento_num = pedimento.get('pedimento','')
|
||||
_file_name = _get_file_name(**kwargs)
|
||||
|
||||
# Validar que organización y pedimento no sean None
|
||||
organizacion = pedimento.get("organizacion", None)
|
||||
pedimento_id = pedimento.get("id", None)
|
||||
|
||||
rest_response = await coves_rest_controller.post_document(
|
||||
binary_content=pdf_bytes,
|
||||
organizacion=organizacion,
|
||||
pedimento=pedimento_id,
|
||||
file_name=_file_name,
|
||||
document_type=7
|
||||
)
|
||||
|
||||
acuse_status = await change_acuse_status(
|
||||
cove=kwargs.get('cove'),
|
||||
status=True,
|
||||
pedimento=kwargs.get('pedimento')
|
||||
)
|
||||
|
||||
return {
|
||||
"document_response": rest_response,
|
||||
"file_name": _file_name,
|
||||
"pedimento": pedimento_num,
|
||||
"acuse_update": acuse_status
|
||||
}
|
||||
|
||||
def _decode_acuse_base64_content(base64_content): # Testeado
|
||||
"""
|
||||
Decodifica el contenido Base64 del acuse y limpia caracteres especiales.
|
||||
|
||||
Args:
|
||||
base64_content (str): Contenido codificado en Base64
|
||||
|
||||
Returns:
|
||||
bytes: Contenido decodificado o None si hay error
|
||||
"""
|
||||
try:
|
||||
# Limpiar el contenido Base64 de manera exhaustiva
|
||||
cleaned_content = base64_content
|
||||
|
||||
# Remover entidades HTML/XML como 
, 
, etc.
|
||||
cleaned_content = re.sub(r'&#x[0-9a-fA-F]+;', '', cleaned_content)
|
||||
cleaned_content = re.sub(r'&#[0-9]+;', '', cleaned_content)
|
||||
|
||||
# Remover espacios en blanco, saltos de línea, etc.
|
||||
cleaned_content = re.sub(r'[\s\n\r\t]', '', cleaned_content)
|
||||
|
||||
# Remover caracteres no válidos para Base64
|
||||
cleaned_content = re.sub(r'[^A-Za-z0-9+/=]', '', cleaned_content)
|
||||
|
||||
|
||||
# Agregar padding si es necesario
|
||||
missing_padding = len(cleaned_content) % 4
|
||||
if missing_padding:
|
||||
cleaned_content += '=' * (4 - missing_padding)
|
||||
|
||||
# Decodificar Base64
|
||||
decoded_content = base64.b64decode(cleaned_content)
|
||||
|
||||
return decoded_content
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# Intentar con validación estricta deshabilitada
|
||||
try:
|
||||
decoded_content = base64.b64decode(cleaned_content, validate=False)
|
||||
return decoded_content
|
||||
except Exception as e2:
|
||||
return None
|
||||
|
||||
def _extract_acuse_data(soap_response_text: str) -> str:
|
||||
"""
|
||||
Extrae el contenido base64 del acuse desde la respuesta SOAP.
|
||||
|
||||
Args:
|
||||
soap_response_text: Texto completo de la respuesta SOAP
|
||||
|
||||
Returns:
|
||||
str: Contenido base64 del acuse o None si no se encuentra
|
||||
"""
|
||||
try:
|
||||
logger.debug("Iniciando extracción de datos del acuse")
|
||||
|
||||
# Primero, extraer la parte XML del contenido multipart
|
||||
xml_start = soap_response_text.find('<?xml')
|
||||
if xml_start == -1:
|
||||
logger.error("No se encontró inicio de XML en la respuesta")
|
||||
return None
|
||||
|
||||
# Extraer solo la parte XML
|
||||
xml_content = soap_response_text[xml_start:]
|
||||
|
||||
# Si hay más contenido multipart después, cortarlo
|
||||
boundary_end = xml_content.find('--uuid:')
|
||||
if boundary_end != -1:
|
||||
xml_content = xml_content[:boundary_end]
|
||||
|
||||
logger.debug("XML extraído, parseando contenido...")
|
||||
|
||||
# Parsear el XML
|
||||
root = ET.fromstring(xml_content.strip())
|
||||
|
||||
# Log de la estructura XML para debugging
|
||||
logger.debug(f"Elemento raíz: {root.tag}")
|
||||
logger.debug(f"Namespaces encontrados: {root.nsmap if hasattr(root, 'nsmap') else 'No disponible'}")
|
||||
|
||||
# Buscar el elemento acuseDocumento con diferentes estrategias
|
||||
namespaces = {
|
||||
'S': 'http://schemas.xmlsoap.org/soap/envelope/',
|
||||
'ns3': 'http://www.ventanillaunica.gob.mx/ws/consulta/acuses/',
|
||||
'soap': 'http://schemas.xmlsoap.org/soap/envelope/',
|
||||
'ns1': 'http://www.ventanillaunica.gob.mx/ws/consulta/acuses/'
|
||||
}
|
||||
|
||||
# Estrategia 1: Con namespace ns3
|
||||
acuse_elemento = root.find('.//ns3:responseConsultaAcuses/acuseDocumento', namespaces)
|
||||
|
||||
if acuse_elemento is None:
|
||||
# Estrategia 2: Con namespace ns1
|
||||
acuse_elemento = root.find('.//ns1:responseConsultaAcuses/acuseDocumento', namespaces)
|
||||
|
||||
if acuse_elemento is None:
|
||||
# Estrategia 3: Sin namespace específico
|
||||
acuse_elemento = root.find('.//acuseDocumento')
|
||||
|
||||
if acuse_elemento is None:
|
||||
# Estrategia 4: Buscar cualquier elemento que contenga "acuse" o "documento"
|
||||
for elem in root.iter():
|
||||
if 'acuse' in elem.tag.lower() and 'documento' in elem.tag.lower():
|
||||
acuse_elemento = elem
|
||||
break
|
||||
elif elem.tag.endswith('acuseDocumento'):
|
||||
acuse_elemento = elem
|
||||
break
|
||||
|
||||
if acuse_elemento is None:
|
||||
# Log de todos los elementos para debugging
|
||||
logger.error("No se encontró elemento acuseDocumento. Elementos disponibles:")
|
||||
for elem in root.iter():
|
||||
logger.debug(f" - {elem.tag}: {elem.text[:50] if elem.text else 'Sin contenido'}...")
|
||||
return None
|
||||
|
||||
if acuse_elemento is not None and acuse_elemento.text:
|
||||
logger.debug(f"Acuse encontrado, longitud: {len(acuse_elemento.text)} caracteres")
|
||||
return acuse_elemento.text.strip()
|
||||
else:
|
||||
logger.error("Elemento acuseDocumento encontrado pero sin contenido de texto")
|
||||
return None
|
||||
|
||||
except ET.ParseError as e:
|
||||
logger.error(f"Error parseando XML: {e}")
|
||||
logger.debug(f"Contenido XML problemático: {xml_content[:500] if 'xml_content' in locals() else 'No disponible'}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error general extrayendo acuse: {e}")
|
||||
return None
|
||||
|
||||
def _get_file_name(**kwargs) -> dict:
|
||||
pedimento = kwargs.get('pedimento', {})
|
||||
pedimento_app = pedimento.get('pedimento_app', 'N/A')
|
||||
cove = kwargs['cove'].get('cove', 'N/A')
|
||||
_file_name = f"vu_AC_COVE_{pedimento_app}_{cove}.pdf"
|
||||
return _file_name
|
||||
|
||||
|
||||
|
||||
async def change_cove_status(cove: dict, status: bool, pedimento: dict):
|
||||
data = {
|
||||
"id": cove.get("id"),
|
||||
"cove_descargado": status,
|
||||
"numero_cove": cove.get("cove"),
|
||||
"pedimento": pedimento.get("id"),
|
||||
"organizacion": pedimento.get("organizacion"),
|
||||
}
|
||||
|
||||
response = await coves_rest_controller.put_cove_data(cove_id=cove.get("id"), data=data)
|
||||
|
||||
return response
|
||||
|
||||
async def change_acuse_status(cove: dict, status: bool, pedimento: dict):
|
||||
data = {
|
||||
"id": cove.get("id"),
|
||||
"acuse_cove_descargado": status,
|
||||
"numero_cove": cove.get("cove"),
|
||||
"pedimento": pedimento.get("id"),
|
||||
"organizacion": pedimento.get("organizacion"),
|
||||
}
|
||||
|
||||
print(data)
|
||||
response = await coves_rest_controller.put_cove_data(cove_id=cove.get("id"), data=data)
|
||||
|
||||
return response
|
||||
|
||||
async def fetch_sign_and_cer(cadena_original: str, username: str, credenciales: dict):
|
||||
"""
|
||||
Obtiene certificado y llave, genera la firma para la cadena original.
|
||||
|
||||
Args:
|
||||
cadena_original: Cadena a firmar
|
||||
username: Usuario de VUCEM
|
||||
credenciales: Diccionario con credenciales
|
||||
|
||||
Returns:
|
||||
tuple: (firma_base64, certificado_base64, ruta_archivo_temporal)
|
||||
|
||||
Raises:
|
||||
Exception: Si no se pueden obtener los certificados o generar la firma
|
||||
"""
|
||||
try:
|
||||
logger.debug("Obteniendo certificado desde API")
|
||||
|
||||
# Obtener certificado como bytes
|
||||
cer = await coves_rest_controller.get_cer(credenciales['id'])
|
||||
if cer is None:
|
||||
raise Exception("No se pudo obtener el certificado para firmar el COVE")
|
||||
|
||||
# Convertir certificado a base64 string
|
||||
certificado = base64.b64encode(cer).decode('utf-8')
|
||||
logger.debug("Certificado obtenido y codificado exitosamente")
|
||||
|
||||
# Obtener llave privada como bytes
|
||||
logger.debug("Obteniendo llave privada desde API")
|
||||
key_bytes = await coves_rest_controller.get_key(credenciales['id'])
|
||||
if key_bytes is None:
|
||||
raise Exception("No se pudo obtener la llave privada para firmar el COVE")
|
||||
|
||||
# Crear archivo temporal para la llave (requerido por cryptography)
|
||||
with tempfile.NamedTemporaryFile(delete=False, mode='wb') as tmp_key_file:
|
||||
tmp_key_file.write(key_bytes)
|
||||
tmp_key_path = tmp_key_file.name
|
||||
|
||||
logger.debug(f"Llave privada guardada temporalmente en: {tmp_key_path}")
|
||||
|
||||
# Generar firma usando el archivo temporal
|
||||
firma = sign_chain_original(tmp_key_path, credenciales['efirma'], cadena_original)
|
||||
logger.debug("Firma generada exitosamente")
|
||||
|
||||
return firma, certificado, tmp_key_path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error obteniendo certificado/llave o generando firma: {e}")
|
||||
# Limpiar archivo temporal si existe
|
||||
if 'tmp_key_path' in locals() and os.path.exists(tmp_key_path):
|
||||
try:
|
||||
os.remove(tmp_key_path)
|
||||
except:
|
||||
pass
|
||||
raise Exception(f"Error en fetch_sign_and_cer: {str(e)}")
|
||||
|
||||
|
||||
def sign_chain_original(key_path: str, password: str, cadena_original: str) -> str:
|
||||
"""
|
||||
Firma una cadena original usando una llave privada.
|
||||
|
||||
Args:
|
||||
key_path: Ruta al archivo de la llave privada
|
||||
password: Password de la llave privada
|
||||
cadena_original: Cadena a firmar
|
||||
|
||||
Returns:
|
||||
str: Firma en base64
|
||||
|
||||
Raises:
|
||||
Exception: Si hay errores en el proceso de firma
|
||||
"""
|
||||
try:
|
||||
logger.debug(f"Firmando cadena original: {cadena_original}")
|
||||
|
||||
with open(key_path, 'rb') as key_file:
|
||||
private_key = load_der_private_key(
|
||||
key_file.read(),
|
||||
password=password.encode() if password else None
|
||||
)
|
||||
|
||||
signature = private_key.sign(
|
||||
cadena_original.encode('utf-8'),
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA256()
|
||||
)
|
||||
|
||||
firma_b64 = base64.b64encode(signature).decode('utf-8')
|
||||
logger.debug("Cadena firmada exitosamente")
|
||||
|
||||
return firma_b64
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error firmando cadena original: {e}")
|
||||
raise Exception(f"Error en sign_chain_original: {str(e)}")
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from celery import Celery
|
||||
from celery_app import celery_app
|
||||
from typing import Dict, Any
|
||||
|
||||
from .services import consume_ws_get_cove, consume_ws_get_acuse_cove
|
||||
from api.api_v2.modules.tasks.tasks import run_async_task
|
||||
|
||||
# Logger para el módulo
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def process_cove_request(self, cove_request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Tarea de Celery para procesar la solicitud de descarga de COVE.
|
||||
|
||||
Args:
|
||||
cove_request: Diccionario con los datos de la solicitud de COVE.
|
||||
|
||||
Returns:
|
||||
Diccionario con la respuesta del COVE procesado.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Iniciando procesamiento de COVE - Task ID: {self.request.id}")
|
||||
|
||||
# Actualizar progreso
|
||||
self.update_state(state='PROGRESS', meta={'current': 10, 'total': 100, 'status': 'Iniciando procesamiento de COVE'})
|
||||
|
||||
# Usar run_async_task para ejecutar la función asíncrona
|
||||
result = run_async_task(consume_ws_get_cove, **cove_request)
|
||||
|
||||
# Actualizar progreso
|
||||
self.update_state(state='SUCCESS', meta={'current': 100, 'total': 100, 'status': 'COVE procesado exitosamente'})
|
||||
|
||||
logger.info(f"COVE procesado exitosamente - Task ID: {self.request.id}")
|
||||
|
||||
# Asegurar que la respuesta sea serializable para Celery
|
||||
return {
|
||||
"status": "processed",
|
||||
"data": result,
|
||||
"task_id": self.request.id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error procesando COVE: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
# Actualizar estado con error
|
||||
self.update_state(
|
||||
state='FAILURE',
|
||||
meta={
|
||||
'current': 0,
|
||||
'total': 100,
|
||||
'status': error_msg,
|
||||
'error': str(e)
|
||||
}
|
||||
)
|
||||
|
||||
# Re-lanzar excepción para que Celery la registre
|
||||
raise e
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def process_acuse_cove_request(self, cove_request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Tarea de Celery para procesar la solicitud de acuse de COVE.
|
||||
|
||||
Args:
|
||||
cove_request: Diccionario con los datos de la solicitud de acuse.
|
||||
|
||||
Returns:
|
||||
Diccionario con la respuesta del acuse procesado.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Iniciando procesamiento de acuse de COVE - Task ID: {self.request.id}")
|
||||
|
||||
# Actualizar progreso
|
||||
self.update_state(state='PROGRESS', meta={'current': 10, 'total': 100, 'status': 'Iniciando procesamiento de acuse de COVE'})
|
||||
|
||||
# Usar run_async_task para ejecutar la función asíncrona
|
||||
result = run_async_task(consume_ws_get_acuse_cove, **cove_request)
|
||||
|
||||
# Actualizar progreso
|
||||
self.update_state(state='SUCCESS', meta={'current': 100, 'total': 100, 'status': 'Acuse de COVE procesado exitosamente'})
|
||||
|
||||
logger.info(f"Acuse de COVE procesado exitosamente - Task ID: {self.request.id}")
|
||||
|
||||
# Asegurar que la respuesta sea serializable para Celery
|
||||
return {
|
||||
"status": "processed",
|
||||
"data": result,
|
||||
"task_id": self.request.id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error procesando acuse de COVE: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
# Actualizar estado con error
|
||||
self.update_state(
|
||||
state='FAILURE',
|
||||
meta={
|
||||
'current': 0,
|
||||
'total': 100,
|
||||
'status': error_msg,
|
||||
'error': str(e)
|
||||
}
|
||||
)
|
||||
|
||||
# Re-lanzar excepción para que Celery la registre
|
||||
raise e
|
||||
|
||||
|
||||
Reference in New Issue
Block a user