Files
plantillas-proyectos/backend/api/v1/modules/a76/customs_brokers/routes.py

415 lines
15 KiB
Python

import logging
import mimetypes
import os
from datetime import datetime
from typing import Any, Dict, Optional
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
from sqlalchemy.orm import Session
from core.config import settings
from core.database import get_core_db
from core.s3_keys import (
customs_broker_vu_certificate_key,
customs_broker_vu_cove_key,
customs_broker_vu_doda_certificate_key,
customs_broker_vu_doda_cove_key,
customs_broker_vu_doda_private_key_key,
customs_broker_vu_private_key_key,
)
from core.security import get_current_user, get_tenant_from_token, validate_access_to_resource
from core.storage_s3 import delete_object_if_exists, put_object_bytes
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from . import dto, services
from ..layouts_csv.customs_brokers.routes import router as imports_router
logger = logging.getLogger(__name__)
router = APIRouter()
MAX_VU_CER_KEY_BYTES = 5 * 1024 * 1024 # 5 MB
MAX_COVE_BYTES = 15 * 1024 * 1024 # 15 MB (xml/zip)
def _resolve_tenant_id_int(current_user: dict) -> int:
tid = get_tenant_from_token(current_user)
if tid is not None:
return int(tid)
raw = current_user.get("tenant_id")
if isinstance(raw, list) and raw:
raw = raw[0]
if raw is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID not found in user data",
)
try:
return int(raw)
except (TypeError, ValueError):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid tenant ID in token",
)
def _remove_stored_vu_path(ref: Optional[str]) -> None:
if not ref:
return
if ref.startswith("tenants/"):
delete_object_if_exists(ref)
elif os.path.isfile(ref):
try:
os.remove(ref)
except OSError:
pass
# CSV import (mismo flujo que a76.imports: upload → scan → commit)
router.include_router(imports_router, prefix="/customs-brokers/imports", tags=["customs_brokers / csv_import"])
customs_broker_crud = TenantCRUDRoutes(
service=services.CustomsBrokerService,
create_schema=dto.CustomsBrokerCreateDTO,
update_schema=dto.CustomsBrokerUpdateDTO,
response_schema=dto.CustomsBrokerResponseDTO,
prefix="/customs-brokers",
tags=[],
resource_name="Customs Broker",
id_name="broker_key",
id_type=str,
enable_list=True,
list_permissions=["customs_brokers.view"],
get_permissions=["customs_brokers.view"],
create_permissions=["customs_brokers.create"],
update_permissions=["customs_brokers.edit"],
delete_permissions=["customs_brokers.delete"],
)
router.include_router(customs_broker_crud.router)
@router.patch(
"/customs-brokers/{broker_key}",
response_model=dto.CustomsBrokerResponseDTO,
)
def update_customs_broker(
broker_key: str,
broker_data: dto.CustomsBrokerUpdateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Actualización parcial (PATCH).
"""
tenant_id = validate_access_to_resource(db, company_id, current_user, ["customs_brokers.edit"])
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
if not broker:
raise HTTPException(status_code=404, detail="Customs Broker not found")
updated_broker = services.CustomsBrokerService.update(
db=db,
broker_key=broker_key,
tenant_id=tenant_id,
broker_data=broker_data,
company_id=company_id
)
if not updated_broker:
raise HTTPException(status_code=400, detail="Error updating Customs Broker")
return updated_broker
@router.put(
"/customs-broker-vu/{broker_key}",
response_model=dto.CustomsBrokerVUResponseDTO,
)
def update_customs_broker_vu(
broker_key: str,
vu_data: dto.CustomsBrokerVUCreateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user, ["customs_brokers.edit"])
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
if not broker:
raise HTTPException(status_code=404, detail="Customs Broker not found")
updated_vu = services.CustomsBrokerVUService.update_vu(db, broker_key, vu_data, tenant_id, company_id)
if not updated_vu:
raise HTTPException(status_code=404, detail="Customs Broker VU not found")
return updated_vu
@router.put(
"/customs-broker-personnel/{broker_key}/{line}",
response_model=dto.CustomsBrokerPersonnelDTO,
)
def update_customs_broker_personnel(
broker_key: str,
line: int,
personnel_data: dto.CustomsBrokerPersonnelDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user, ["customs_brokers.edit"])
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
if not broker:
raise HTTPException(status_code=404, detail="Customs Broker not found")
updated_personnel = services.CustomsBrokerPersonnelService.update_personnel(
db, broker_key, line, personnel_data, tenant_id, company_id
)
if not updated_personnel:
raise HTTPException(
status_code=404, detail="Customs Broker Personnel not found"
)
return updated_personnel
@router.post(
"/customs-brokers/{broker_key}/vu/upload",
summary="Sube VU/DODA (CER, KEY, COVE) al bucket bajo tenants/.../customs_brokers/{id}/...",
)
async def upload_customs_broker_vu_file(
broker_key: str,
file_kind: str = Query(
...,
description=(
"certificate (.cer), key (.key), cove (xml/zip/txt/pdf/json), "
"doda_certificate (.cer), doda_key (.key), doda_cove (xml/zip/txt/pdf/json)"
),
),
company_id: int = Query(..., description="Company ID"),
file: UploadFile = File(...),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Persiste el archivo bajo la misma jerarquía que logos/avatares (tenant/company/...).
Guarda la clave S3 o ruta local en:
- VU: certificate_path, key_path, xml_files_path
- DODA: doda_certificate_path, doda_key_path, doda_xml_files_path
"""
validate_access_to_resource(db, company_id, current_user, ["customs_brokers.create"])
tenant_id = _resolve_tenant_id_int(current_user)
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
if not broker:
raise HTTPException(status_code=404, detail="Customs Broker not found")
fk = file_kind.lower().strip()
if fk not in (
"certificate",
"key",
"cove",
"doda_certificate",
"doda_key",
"doda_cove",
):
raise HTTPException(
status_code=400,
detail=(
"file_kind must be certificate, key, cove, "
"doda_certificate, doda_key, or doda_cove"
),
)
content = await file.read()
max_bytes = MAX_COVE_BYTES if fk in ("cove", "doda_cove") else MAX_VU_CER_KEY_BYTES
if len(content) > max_bytes:
raise HTTPException(
status_code=400,
detail=f"File too large (max {max_bytes // (1024 * 1024)} MB)",
)
file_ext = os.path.splitext(file.filename or "")[1].lower()
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
vu = services.CustomsBrokerVUService.ensure_vu_for_broker(db, broker)
broker_id = broker.id
field_name: str
stored: str
try:
if settings.use_s3_object_storage:
if fk == "certificate":
if file_ext != ".cer":
raise HTTPException(status_code=400, detail="certificate must be .cer")
key = customs_broker_vu_certificate_key(
tenant_id, company_id, broker_id, timestamp, file_ext
)
ct = "application/x-x509-ca-cert"
field_name = "certificate_path"
elif fk == "key":
if file_ext != ".key":
raise HTTPException(status_code=400, detail="key must be .key")
key = customs_broker_vu_private_key_key(
tenant_id, company_id, broker_id, timestamp, file_ext
)
ct = "application/pkcs8"
field_name = "key_path"
elif fk == "cove":
key = customs_broker_vu_cove_key(
tenant_id,
company_id,
broker_id,
timestamp,
file.filename or "cove.xml",
)
ct = (
file.content_type
or mimetypes.guess_type(file.filename or "")[0]
or "application/octet-stream"
)
field_name = "xml_files_path"
elif fk == "doda_certificate":
if file_ext != ".cer":
raise HTTPException(status_code=400, detail="doda_certificate must be .cer")
key = customs_broker_vu_doda_certificate_key(
tenant_id, company_id, broker_id, timestamp, file_ext
)
ct = "application/x-x509-ca-cert"
field_name = "doda_certificate_path"
elif fk == "doda_key":
if file_ext != ".key":
raise HTTPException(status_code=400, detail="doda_key must be .key")
key = customs_broker_vu_doda_private_key_key(
tenant_id, company_id, broker_id, timestamp, file_ext
)
ct = "application/pkcs8"
field_name = "doda_key_path"
else:
key = customs_broker_vu_doda_cove_key(
tenant_id,
company_id,
broker_id,
timestamp,
file.filename or "doda.xml",
)
ct = (
file.content_type
or mimetypes.guess_type(file.filename or "")[0]
or "application/octet-stream"
)
field_name = "doda_xml_files_path"
old = getattr(vu, field_name)
_remove_stored_vu_path(old)
put_object_bytes(key, content, content_type=ct)
logger.info(
"Customs broker VU upload kind=%s key=%s bytes=%s",
fk,
key,
len(content),
)
stored = key
else:
base = os.path.join(
"uploads", "customs_brokers", str(company_id), str(broker_id)
)
if fk == "certificate":
if file_ext != ".cer":
raise HTTPException(status_code=400, detail="certificate must be .cer")
key = customs_broker_vu_certificate_key(
tenant_id, company_id, broker_id, timestamp, file_ext
)
field_name = "certificate_path"
subdir = "certificates"
elif fk == "key":
if file_ext != ".key":
raise HTTPException(status_code=400, detail="key must be .key")
key = customs_broker_vu_private_key_key(
tenant_id, company_id, broker_id, timestamp, file_ext
)
field_name = "key_path"
subdir = "keys"
elif fk == "cove":
try:
key = customs_broker_vu_cove_key(
tenant_id,
company_id,
broker_id,
timestamp,
file.filename or "cove.xml",
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
field_name = "xml_files_path"
subdir = "cove"
elif fk == "doda_certificate":
if file_ext != ".cer":
raise HTTPException(status_code=400, detail="doda_certificate must be .cer")
key = customs_broker_vu_doda_certificate_key(
tenant_id, company_id, broker_id, timestamp, file_ext
)
field_name = "doda_certificate_path"
subdir = "doda/certificates"
elif fk == "doda_key":
if file_ext != ".key":
raise HTTPException(status_code=400, detail="doda_key must be .key")
key = customs_broker_vu_doda_private_key_key(
tenant_id, company_id, broker_id, timestamp, file_ext
)
field_name = "doda_key_path"
subdir = "doda/keys"
else:
try:
key = customs_broker_vu_doda_cove_key(
tenant_id,
company_id,
broker_id,
timestamp,
file.filename or "doda.xml",
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
field_name = "doda_xml_files_path"
subdir = "doda/cove"
fname = key.rsplit("/", 1)[-1]
dest_dir = os.path.join(base, subdir)
os.makedirs(dest_dir, exist_ok=True)
path = os.path.join(dest_dir, fname)
old = getattr(vu, field_name)
_remove_stored_vu_path(old)
with open(path, "wb") as f:
f.write(content)
logger.info(
"Customs broker VU upload kind=%s path=%s bytes=%s",
fk,
path,
len(content),
)
stored = path
setattr(vu, field_name, stored)
db.add(vu)
db.commit()
db.refresh(vu)
except HTTPException:
db.rollback()
raise
except ValueError as e:
db.rollback()
raise HTTPException(status_code=400, detail=str(e)) from e
except Exception as e:
db.rollback()
raise HTTPException(
status_code=500, detail=f"Error saving file: {str(e)}"
) from e
return {
"message": "File uploaded successfully",
"file_kind": fk,
"field": field_name,
"path": stored,
"broker_key": broker_key,
"company_id": company_id,
}