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, resolve_tenant_id_required, 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, db=None, company_id: int = None, ): """Delega a resolve_tenant_id_required. Hub admin resuelve tenant desde la empresa.""" return resolve_tenant_id_required(current_user, db=db, company_id=company_id) 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, db=db, company_id=company_id) 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, }