Se agregaron los moduloes de api_v2

This commit is contained in:
2025-10-03 23:16:47 -06:00
parent ac075bfeb7
commit 7149515606
60 changed files with 3714 additions and 252 deletions

View File

View File

@@ -0,0 +1,61 @@
from controllers.RESTController import APIRESTController
from controllers.SOAPController import VUCEMController
from typing import List, Dict, Any
class AcuseVUController(VUCEMController):
def __init__(self):
super().__init__()
def generate_acuse_template(self, **kwargs) -> str:
credencial = kwargs.get("credencial", {})
username = credencial.get("user")
password = credencial.get("password")
idEDocument = kwargs['edoc'].get("numero_edocument", "N/A")
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>{idEDocument}</idEdocument>
</oxml:consultaAcusesPeticion>
</soapenv:Body>
</soapenv:Envelope>
'''
return soap_template
class EDocumentController(APIRESTController):
def __init__(self):
super().__init__()
async def get_edocs(self, pedimento: str) -> List[Dict[str, Any]]:
"""
Método para obtener los documentos digitalizados de un pedimento.
Args:
pedimento: UUID del pedimento a consultar
"""
return await self._make_request_async('GET', f'customs/edocuments/?pedimento={pedimento}')
async def put_edocument(self, edocument_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Método para actualizar un documento digitalizado en la API.
Args:
edocument_id: UUID del documento a actualizar
data: Diccionario con los datos a actualizar
"""
return await self._make_request_async('PUT', f'customs/edocuments/{edocument_id}/', data=data)
acuse_rest_controller = EDocumentController()
acuse_vu_controller = AcuseVUController()

View File

@@ -1,42 +1,43 @@
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Depends
from fastapi.responses import JSONResponse
from typing import Dict, Any, List, Optional
import asyncio
import logging
import traceback
from api.api_v2.modules.authentication.services import get_current_user
from .schemas import AcuseSchema, AcuseMasivoSchema
from .services import *
from .tasks import process_acuse_request
router = APIRouter()
router = APIRouter(prefix="/acuses", tags=["Acuses"])
@router.post("/service/acuse/individual", response_model=Dict[str, Any])
@router.post("/services/acuse/", response_model=Dict[str, Any])
async def obtener_acuse(acuse_request: AcuseSchema):
"""
Endpoint para obtener el acuse de recibo de un documento específico.
"""
acuse_dict = acuse_request.model_dump()
# Ejecuta la tarea de Celery de forma asíncrona
task = process_acuse_request.delay(acuse_dict)
# Puedes devolver el ID de la tarea para consultar el estado después
return {"task_id": task.id, "status": "submitted"}
pass
@router.post("/service/acuse", response_model=Dict[str, Any])
@router.post("/services/all/acuse/pedimento/", response_model=Dict[str, Any])
async def obtener_acuses(acuse_request: AcuseMasivoSchema):
"""
Endpoint para obtener acuses de recibo de documentos asociados a un pedimento.
"""
pass
# Para cada edoc en la lista, dispara una tarea Celery
task_ids = []
acuse_request_dict = acuse_request.model_dump()
for edoc in acuse_request_dict.get('edocs', []):
acuse_dict = {
"edoc": edoc,
"pedimento": acuse_request_dict.get('pedimento'),
"credencial": acuse_request_dict.get('credencial')
}
task = process_acuse_request.delay(acuse_dict)
task_ids.append(task.id)
@router.post("/service/acuse_cove", response_model=Dict[str, Any])
async def obtener_acuses_cove(acuse_request: AcuseMasivoSchema):
"""
Endpoint para obtener acuses de recibo de COVEs asociados a un pedimento.
"""
pass
@router.post("/service/acuse_cove/individual", response_model=Dict[str, Any])
async def obtener_acuse_cove(acuse_request: AcuseSchema):
"""
Endpoint para obtener el acuse de recibo de un COVE específico.
"""
pass
return {"task_ids": task_ids, "status": "submitted", "total": len(task_ids)}

View File

@@ -1,22 +1,20 @@
from fastapi import FastAPI
from pydantic import BaseModel
from uuid import UUID
from pydantic import BaseModel, Field, field_validator
from schemas.CredencialSchema import CredencialBaseSchema
from api.api_v2.modules.pedimentos.schemas import PedimentoBaseSchema
# Aplica para Acuse, Acuse Cove y Edocuments
class AcuseBaseSchema(BaseModel):
id: int = Field(..., description="ID único del eDocument")
numero_edocument: str =Field(..., description="Número del eDocument")
class AcuseSchema(BaseModel):
pedimento: str
organizacion: str
numero_documento: str
vu_user: str
password: str
edoc: AcuseBaseSchema
pedimento: PedimentoBaseSchema
credencial: CredencialBaseSchema
class AcuseMasivoSchema(BaseModel):
pedimento: str
organizacion: str
numeros_documentos: list[str]
vu_user: str
password: str
edocs: list[AcuseBaseSchema]
pedimento: PedimentoBaseSchema
credencial: CredencialBaseSchema

View File

@@ -0,0 +1,188 @@
from http.client import HTTPException
import base64
import re
from .controllers import acuse_vu_controller, acuse_rest_controller
from utils.helpers import soap_error
import xml.etree.ElementTree as ET
soap_headers = {
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': 'http://www.ventanillaunica.gob.mx/ventanilla/ConsultaAcusesService/consultarAcuseEdocument',# AcuseCove
'Accept-Encoding': 'gzip,deflate',
}
async def obtener_acuse(**kwargs):
soap_xml = acuse_vu_controller.generate_acuse_template(**kwargs)
response = await acuse_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)):
acuse_base64 = _extract_acuse_data(response.text)
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'):
import logging
logger = logging.getLogger("app.api")
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 acuse_rest_controller.post_document(
binary_content=pdf_bytes,
organizacion=organizacion,
pedimento=pedimento_id,
file_name=_file_name,
document_type=4
)
if rest_response is None:
raise Exception("No se pudo enviar el acuse a la API interna.")
if rest_response.get("id") is None:
raise Exception("La respuesta de la API interna no contiene un ID válido.")
acuse_update_response = await change_edocument_status(
edoc=kwargs.get('edoc'),
status=True,
pedimento=pedimento
)
return {
"document_response": rest_response,
"file_name": _file_name,
"pedimento": pedimento_num,
"acuse_update_response": acuse_update_response
}
async def change_edocument_status(edoc: dict, status: bool, pedimento: dict):
data = {
"id": edoc.get("id"),
"edocument_descargado": status,
"numero_edocument": edoc.get("numero_edocument"),
"pedimento": pedimento.get("id"),
"organizacion": pedimento.get("organizacion"),
}
response = await acuse_rest_controller.put_edocument(edocument_id=edoc.get("id"), data=data)
return response
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 &#xd;, &#xa;, 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) -> dict:
try:
# Primero, extraer la parte XML del contenido multipart
xml_start = soap_response_text.find('<?xml')
if xml_start == -1:
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]
# Parsear el XML
root = ET.fromstring(xml_content.strip())
# Buscar el elemento acuseDocumento con namespaces
namespaces = {
'S': 'http://schemas.xmlsoap.org/soap/envelope/',
'ns3': 'http://www.ventanillaunica.gob.mx/ws/consulta/acuses/'
}
# Buscar el elemento acuseDocumento
acuse_elemento = root.find('.//ns3:responseConsultaAcuses/acuseDocumento', namespaces)
if acuse_elemento is None:
# Intentar sin namespace
acuse_elemento = root.find('.//acuseDocumento')
if acuse_elemento is not None and acuse_elemento.text:
return acuse_elemento.text.strip()
else:
return None
except ET.ParseError as e:
return None
except Exception as e:
return None
def _get_file_name(**kwargs) -> dict:
pedimento = kwargs.get('pedimento', {})
pedimento_app = pedimento.get('pedimento_app', 'N/A')
idEdocument = kwargs['edoc'].get('numero_edocument', 'N/A')
_file_name = f"vu_AC_{pedimento_app}_{idEdocument}.pdf"
return _file_name

View File

@@ -0,0 +1,27 @@
from celery import Celery
from celery_app import celery_app
import asyncio
import logging
from typing import Dict, Any
from contextlib import asynccontextmanager
from .services import obtener_acuse
from api.api_v2.modules.tasks.tasks import run_async_task
@celery_app.task
def process_acuse_request(acuse_request: Dict[str, Any]) -> Dict[str, Any]:
"""
Tarea de Celery para procesar la solicitud de acuse.
Args:
acuse_request: Diccionario con los datos de la solicitud de acuse.
Returns:
Diccionario con la respuesta del acuse.
"""
loop = asyncio.get_event_loop()
acuse_response = loop.run_until_complete(obtener_acuse(**acuse_request))
return {"status": "processed", "data": acuse_response}