Files
plantillas-proyectos/backend/api/v1/modules/a76/expediente_archivos/service.py
2026-04-17 15:59:47 -06:00

360 lines
13 KiB
Python

from __future__ import annotations
import base64
import logging
from datetime import datetime
from typing import List, Optional
from cryptography.hazmat.primitives import padding as crypto_padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from sqlalchemy.orm import Session
from core.celery_app import celery_app
from core.config import settings
from core.exceptions import ErrorCollector, ValidationException
from core.storage_s3 import get_object_bytes, object_exists
from api.v1.modules.a76.customs_brokers import models as cb_models
from api.v1.modules.a76.general_catalogs.company.models import Company
from .dto import (
DigitalizacionErrorDetail,
DigitalizacionResult,
DigitalizacionTaskDetailResponse,
ExpedienteArchivoCreateDTO,
ExpedienteArchivoListResponse,
ExpedienteArchivoResponseDTO,
ExpedienteArchivoUpdateDTO,
)
from .models import ExpedienteArchivo
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# CRUD service
# ---------------------------------------------------------------------------
class ExpedienteArchivoService:
@staticmethod
def list(
db: Session,
company_id: int,
tenant_id: int,
page: int = 1,
page_size: int = 50,
search: Optional[str] = None,
) -> ExpedienteArchivoListResponse:
query = (
db.query(ExpedienteArchivo)
.filter(
ExpedienteArchivo.company_id == company_id,
ExpedienteArchivo.tenant_id == tenant_id,
ExpedienteArchivo.deleted_at.is_(None),
)
)
if search:
like = f"%{search}%"
query = query.filter(
ExpedienteArchivo.e_document.ilike(like)
| ExpedienteArchivo.tipo_documento.ilike(like)
)
total = query.count()
items = query.order_by(ExpedienteArchivo.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
return ExpedienteArchivoListResponse(
items=[ExpedienteArchivoResponseDTO.model_validate(r) for r in items],
total=total,
page=page,
page_size=page_size,
)
@staticmethod
def get(db: Session, record_id: int, company_id: int, tenant_id: int) -> Optional[ExpedienteArchivo]:
return (
db.query(ExpedienteArchivo)
.filter(
ExpedienteArchivo.id == record_id,
ExpedienteArchivo.company_id == company_id,
ExpedienteArchivo.tenant_id == tenant_id,
ExpedienteArchivo.deleted_at.is_(None),
)
.first()
)
@staticmethod
def create(db: Session, dto: ExpedienteArchivoCreateDTO, company_id: int, tenant_id: int) -> ExpedienteArchivo:
record = ExpedienteArchivo(
company_id=company_id,
tenant_id=tenant_id,
**dto.model_dump(exclude_none=False),
)
db.add(record)
db.commit()
db.refresh(record)
return record
@staticmethod
def update(
db: Session,
record: ExpedienteArchivo,
dto: ExpedienteArchivoUpdateDTO,
) -> ExpedienteArchivo:
for field, value in dto.model_dump(exclude_unset=True).items():
setattr(record, field, value)
db.commit()
db.refresh(record)
return record
@staticmethod
def delete(db: Session, record: ExpedienteArchivo) -> None:
from datetime import datetime
record.deleted_at = datetime.utcnow()
db.commit()
@staticmethod
def get_task_status(task_id: str) -> DigitalizacionTaskDetailResponse:
result = celery_app.AsyncResult(task_id)
state = result.state or "PENDING"
info = result.info or {}
if state == "SUCCESS":
raw = result.result or {}
return DigitalizacionTaskDetailResponse(
task_id=task_id,
state="SUCCESS",
status="success",
result=DigitalizacionResult(**{k: raw.get(k) for k in DigitalizacionResult.model_fields}),
progress=100,
total_steps=4,
)
if state == "FAILURE":
err = info if not isinstance(info, dict) else None
error_detail_raw = info.get("error_detail") if isinstance(info, dict) else None
return DigitalizacionTaskDetailResponse(
task_id=task_id,
state="FAILURE",
status="failed",
error=str(err or info.get("error", "")),
error_type=info.get("error_type") if isinstance(info, dict) else None,
error_detail=DigitalizacionErrorDetail(**(error_detail_raw or {})) if error_detail_raw else None,
)
# PROGRESS / PENDING / STARTED
if isinstance(info, dict):
return DigitalizacionTaskDetailResponse(
task_id=task_id,
state=state,
status="processing",
current_step=info.get("current_step") or info.get("status"),
progress=info.get("progress") or info.get("current"),
total_steps=info.get("total_steps") or 4,
)
return DigitalizacionTaskDetailResponse(task_id=task_id, state=state, status="pending")
# ---------------------------------------------------------------------------
# VU config builder (mirror of factura_cove/service.py _build_configuracion_vu)
# ---------------------------------------------------------------------------
def _encrypt_fiel(raw_fiel: str) -> str:
"""AES-256-CBC + PKCS7 + base64 — mismo esquema que factura_cove."""
normalized = (raw_fiel or "").strip()
if not normalized:
return ""
key_bytes = (settings.COVE_FIEL_HASH_KEY or "").encode("utf-8")
iv_bytes = (settings.COVE_FIEL_HASH_IV or "").encode("utf-8")
if not key_bytes or not iv_bytes:
return ""
key32 = key_bytes[:32].ljust(32, b"\0")
iv16 = iv_bytes[:16].ljust(16, b"\0")
padder = crypto_padding.PKCS7(algorithms.AES.block_size).padder()
padded = padder.update(normalized.encode("utf-8")) + padder.finalize()
cipher = Cipher(algorithms.AES(key32), modes.CBC(iv16))
enc = cipher.encryptor()
encrypted = enc.update(padded) + enc.finalize()
return base64.b64encode(encrypted).decode("ascii")
def build_configuracion_vu(
db: Session,
company_id: int,
tenant_id: int,
agente_aduanal_key: Optional[str],
errors: ErrorCollector,
) -> Optional[dict]:
"""
Construye el dict de configuracion_vu para el servicio externo de digitalización.
Prioridad: CustomsBrokerVU (agente) → company.ventanilla_unica → company_fiel_certificate.
"""
vu: Optional[cb_models.CustomsBrokerVU] = None
if agente_aduanal_key:
broker = (
db.query(cb_models.CustomsBroker)
.filter(
cb_models.CustomsBroker.license == agente_aduanal_key,
cb_models.CustomsBroker.company_id == company_id,
cb_models.CustomsBroker.tenant_id == tenant_id,
cb_models.CustomsBroker.deleted_at.is_(None),
)
.first()
)
if broker:
vu = broker.vu
else:
broker = (
db.query(cb_models.CustomsBroker)
.filter(
cb_models.CustomsBroker.company_id == company_id,
cb_models.CustomsBroker.tenant_id == tenant_id,
cb_models.CustomsBroker.deleted_at.is_(None),
)
.first()
)
if broker:
vu = broker.vu
company = db.get(Company, company_id)
company_vu = company.ventanilla_unica if company else None
company_fiel_certificate = None
if company:
for cert in (company.digital_certificates or []):
if (cert.certificate_type or "").strip().lower() == "fiel":
company_fiel_certificate = cert
break
if not vu and not company_vu and not company_fiel_certificate:
errors.add_error(
field="vu",
message="La empresa no tiene configuración VU ni certificado FIEL.",
solution=["Configura los datos VU del agente aduanal o de la empresa y sube certificado (.cer) y llave (.key)."],
code="MISSING_VU_CONFIGURATION",
)
return None
effective_ws_user = (
(
vu.web_service_user
or vu.doda_web_service_user
or getattr(company_vu, "webservice_user", None)
or ""
).strip()
if (vu or company_vu)
else ""
)
clave_fiel_value = ""
if vu and getattr(vu, "fiel_access_key", None):
clave_fiel_value = _encrypt_fiel(vu.fiel_access_key or "")
elif company_fiel_certificate:
secret = (
getattr(company_fiel_certificate, "access_key", None)
or getattr(company_fiel_certificate, "password", None)
or ""
)
clave_fiel_value = _encrypt_fiel(str(secret))
if not effective_ws_user:
errors.add_error(
field="vu",
message="Faltan credenciales de web service en VU.",
solution=["Captura usuario y clave de web service en la pestaña VU del agente aduanal."],
code="MISSING_VU_CREDENTIALS",
)
if not clave_fiel_value:
errors.add_error(
field="vu.clave_fiel",
message="La clave FIEL no está configurada en VU ni en la empresa.",
solution=["Captura la clave FIEL en la configuración VU del agente aduanal o en los certificados de la empresa."],
code="MISSING_FIEL_PASSWORD",
)
certificate_path = (
(getattr(vu, "certificate_path", None) or "").strip() if vu else ""
) or (
(getattr(company_fiel_certificate, "cer_file_path", None) or "").strip()
if company_fiel_certificate
else ""
)
key_path = (
(getattr(vu, "key_path", None) or "").strip() if vu else ""
) or (
(getattr(company_fiel_certificate, "key_file_path", None) or "").strip()
if company_fiel_certificate
else ""
)
if not (certificate_path and key_path):
errors.add_error(
field="vu",
message="No hay rutas de certificado o llave en la configuración VU.",
solution=["Sube el certificado (.cer) y la llave (.key) en la configuración VU del agente aduanal."],
code="MISSING_VU_CERT_KEY",
)
return None
cer_b64 = None
key_b64 = None
try:
if not object_exists(certificate_path):
errors.add_error(
field="vu.certificate_path",
message="El certificado VU no existe en el almacenamiento.",
solution=["Vuelve a subir el certificado en la configuración VU."],
code="VU_CERT_NOT_FOUND",
)
else:
cer_b64 = base64.b64encode(get_object_bytes(certificate_path)).decode("ascii")
if not object_exists(key_path):
errors.add_error(
field="vu.key_path",
message="La llave VU no existe en el almacenamiento.",
solution=["Vuelve a subir la llave en la configuración VU."],
code="VU_KEY_NOT_FOUND",
)
else:
key_b64 = base64.b64encode(get_object_bytes(key_path)).decode("ascii")
except Exception:
logger.exception("Error leyendo certificados VU desde almacenamiento")
errors.add_error(
field="vu",
message="Error leyendo certificados VU.",
solution=["Verifica la configuración de MinIO/S3."],
code="VU_STORAGE_ERROR",
)
if errors.has_errors():
return None
rfc_usuario_vu = (
(getattr(vu, "query_tax_id", None) or "").strip() if vu else ""
) or (
(getattr(company_vu, "query_rfc", None) or "").strip() if company_vu else ""
)
hardcoded_ws_key = (
"RZGd+CB4R6PfSrstOyN8Is9FXL9AK9NPFisyGGaEWa0vVHoVOl8v2SBcHBoGbt3T/4uHTGcsFQO3b7EonWVfugQjBooywbz74K+jM68j8/Y="
)
clave_webservice = (
(getattr(vu, "web_service_access_key", None) or "").strip()
or (getattr(company_vu, "webservice_password", None) or "").strip()
or hardcoded_ws_key
)
email = (getattr(vu, "vu_email", None) or "").strip() if vu else ""
return {
"rfc_usuario_vu": rfc_usuario_vu,
"clave_webservice": clave_webservice,
"archivo_cer_base64": cer_b64 or "",
"archivo_key_base64": key_b64 or "",
"clave_fiel": clave_fiel_value,
"email": email,
}