- Updated SealService to support tenant and company filtering with pagination and enhanced CRUD methods. - Refactored TrailerService to include tenant and company support, added filtering capabilities, and improved CRUD methods. - Introduced TenantCRUDRoutes for Trailer and Transporter routes to streamline API endpoint creation and management. - Enhanced TransporterService with tenant and company filtering, pagination, and improved CRUD operations. - Added Customs Broker module with DTOs, models, services, and routes for managing customs broker data. - Implemented CRUD operations for Customs Broker, including personnel and VU management. - Improved data validation and descriptions in DTOs for better API documentation.
367 lines
15 KiB
Python
367 lines
15 KiB
Python
from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar, Union
|
|
|
|
from core.database import get_core_db
|
|
from core.security import get_current_user, validate_access_to_resource
|
|
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
|
|
# Type variables for generic types
|
|
ModelType = TypeVar("ModelType")
|
|
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
|
|
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
|
|
ResponseSchemaType = TypeVar("ResponseSchemaType", bound=BaseModel)
|
|
ServiceType = TypeVar("ServiceType")
|
|
|
|
|
|
class TenantCRUDRoutes(
|
|
Generic[CreateSchemaType, UpdateSchemaType, ResponseSchemaType, ServiceType]
|
|
):
|
|
"""
|
|
Generic CRUD routes factory for tenant-scoped resources
|
|
|
|
Supports both parent resources (with list/pagination) and child resources (nested under parent).
|
|
|
|
Usage examples:
|
|
|
|
1. Parent resource with list (e.g., /pedimentos):
|
|
router = TenantCRUDRoutes(
|
|
service=PedimentosService,
|
|
create_schema=PedimentosCreate,
|
|
update_schema=PedimentosUpdate,
|
|
response_schema=PedimentosResponse,
|
|
prefix="/pedimentos",
|
|
tags=["Pedimentos"],
|
|
resource_name="Pedimento",
|
|
id_name="pedimento_id",
|
|
enable_list=True,
|
|
).router
|
|
|
|
2. Child resource (e.g., /pedimentos/{pedimento_id}/config-additional):
|
|
router = TenantCRUDRoutes(
|
|
service=PedimentoConfigAdditionalService,
|
|
create_schema=PedimentoConfigAdditionalCreate,
|
|
update_schema=PedimentoConfigAdditionalUpdate,
|
|
response_schema=PedimentoConfigAdditionalResponse,
|
|
prefix="/{pedimento_id}/config-additional",
|
|
tags=["Pedimento Config Additional"],
|
|
resource_name="Config additional",
|
|
parent_id_name="pedimento_id",
|
|
enable_list=False,
|
|
).router
|
|
|
|
3. Parent resource with string ID (e.g., /vehicles with vehicle_key):
|
|
router = TenantCRUDRoutes(
|
|
service=VehicleService,
|
|
create_schema=VehicleCreate,
|
|
update_schema=VehicleUpdate,
|
|
response_schema=VehicleResponse,
|
|
prefix="/vehicles",
|
|
tags=["Vehicles"],
|
|
resource_name="Vehicle",
|
|
id_name="vehicle_key",
|
|
id_type=str, # Specify string type for vehicle_key
|
|
enable_list=True,
|
|
).router
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
service: Type[ServiceType],
|
|
create_schema: Type[CreateSchemaType],
|
|
update_schema: Type[UpdateSchemaType],
|
|
response_schema: Type[ResponseSchemaType],
|
|
prefix: str,
|
|
tags: list[str],
|
|
resource_name: str = "Resource",
|
|
id_name: Optional[str] = None, # For parent resources (e.g., "pedimento_id")
|
|
id_type: Type = int, # Type of the ID (int, str, etc.)
|
|
parent_id_name: Optional[
|
|
str
|
|
] = None, # For child resources (e.g., "pedimento_id")
|
|
db_dependency: Callable = get_core_db,
|
|
auth_dependency: Callable = get_current_user,
|
|
validate_parent_match: bool = True, # Validate parent_id matches in create
|
|
enable_list: bool = False, # Enable GET list endpoint with pagination
|
|
enable_filters: bool = False, # Enable custom filters in list endpoint
|
|
default_page_size: int = 50,
|
|
max_page_size: int = 100,
|
|
):
|
|
self.service = service
|
|
self.create_schema = create_schema
|
|
self.update_schema = update_schema
|
|
self.response_schema = response_schema
|
|
self.resource_name = resource_name
|
|
self.id_name = id_name or parent_id_name or "id"
|
|
self.id_type = id_type
|
|
self.parent_id_name = parent_id_name
|
|
self.db_dependency = db_dependency
|
|
self.auth_dependency = auth_dependency
|
|
self.validate_parent_match = validate_parent_match
|
|
self.enable_list = enable_list
|
|
self.enable_filters = enable_filters
|
|
self.default_page_size = default_page_size
|
|
self.max_page_size = max_page_size
|
|
|
|
self.router = APIRouter(prefix=prefix, tags=tags)
|
|
self._register_routes()
|
|
|
|
def _register_routes(self):
|
|
"""Register all CRUD routes"""
|
|
|
|
# LIST route (optional, for parent resources)
|
|
if self.enable_list:
|
|
if self.enable_filters:
|
|
|
|
@self.router.get("/", response_model=Dict[str, Any])
|
|
async def list_resources(
|
|
company_id: int = Query(..., description="Company ID"),
|
|
page: int = Query(1, ge=1, description="Page number"),
|
|
page_size: int = Query(
|
|
self.default_page_size,
|
|
ge=1,
|
|
le=self.max_page_size,
|
|
description="Page size",
|
|
),
|
|
status: Optional[str] = Query(None, description="Filter by status"),
|
|
db: Session = Depends(self.db_dependency),
|
|
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
|
):
|
|
f"""List all {self.resource_name}s with pagination"""
|
|
tenant_id = validate_access_to_resource(
|
|
db, company_id, current_user
|
|
)
|
|
|
|
skip = (page - 1) * page_size
|
|
filters = {}
|
|
if status:
|
|
filters["status"] = status
|
|
|
|
items, total = self.service.get_all(
|
|
db, tenant_id, company_id, skip, page_size, filters
|
|
)
|
|
|
|
return {
|
|
"items": [
|
|
self.response_schema.model_validate(item) for item in items
|
|
],
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": page_size,
|
|
}
|
|
|
|
else:
|
|
|
|
@self.router.get("/", response_model=Dict[str, Any])
|
|
async def list_resources(
|
|
company_id: int = Query(..., description="Company ID"),
|
|
page: int = Query(1, ge=1, description="Page number"),
|
|
page_size: int = Query(
|
|
self.default_page_size,
|
|
ge=1,
|
|
le=self.max_page_size,
|
|
description="Page size",
|
|
),
|
|
db: Session = Depends(self.db_dependency),
|
|
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
|
):
|
|
f"""List all {self.resource_name}s with pagination"""
|
|
tenant_id = validate_access_to_resource(
|
|
db, company_id, current_user
|
|
)
|
|
|
|
skip = (page - 1) * page_size
|
|
|
|
items, total = self.service.get_all(
|
|
db, tenant_id, company_id, skip, page_size, None
|
|
)
|
|
|
|
return {
|
|
"items": [
|
|
self.response_schema.model_validate(item) for item in items
|
|
],
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": page_size,
|
|
}
|
|
|
|
# GET single resource route
|
|
# For parent resources: GET /{id}
|
|
# For child resources: GET / (parent_id comes from path)
|
|
if self.parent_id_name:
|
|
# Child resource - single GET without ID in path
|
|
@self.router.get("/", response_model=self.response_schema)
|
|
async def get_resource(
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(self.db_dependency),
|
|
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
|
**path_params,
|
|
):
|
|
f"""Get {self.resource_name} by {self.parent_id_name}"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
parent_id = path_params.get(self.parent_id_name)
|
|
|
|
# Try method with 4 params (pedimento_id, tenant_id, company_id)
|
|
if hasattr(self.service, "get_by_pedimento_id"):
|
|
resource = self.service.get_by_pedimento_id(
|
|
db, parent_id, tenant_id, company_id
|
|
)
|
|
# Fallback to method with 3 params
|
|
elif hasattr(self.service, "get_by_id"):
|
|
resource = self.service.get_by_id(
|
|
db, parent_id, tenant_id, company_id
|
|
)
|
|
else:
|
|
resource = self.service.get(db, parent_id, tenant_id, company_id)
|
|
|
|
if not resource:
|
|
raise HTTPException(
|
|
status_code=404, detail=f"{self.resource_name} not found"
|
|
)
|
|
return resource
|
|
|
|
else:
|
|
# Parent resource - GET by ID in path
|
|
@self.router.get(
|
|
f"/{{{self.id_name}}}", response_model=self.response_schema
|
|
)
|
|
async def get_resource_by_id(
|
|
resource_id: Union[int, str] = Path(..., alias=self.id_name),
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(self.db_dependency),
|
|
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
|
):
|
|
f"""Get {self.resource_name} by ID"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
|
|
resource = self.service.get_by_id(
|
|
db, resource_id, tenant_id, company_id
|
|
)
|
|
|
|
if not resource:
|
|
raise HTTPException(
|
|
status_code=404, detail=f"{self.resource_name} not found"
|
|
)
|
|
return resource
|
|
|
|
# POST route
|
|
@self.router.post("/", response_model=self.response_schema, status_code=201)
|
|
async def create_resource(
|
|
data: CreateSchemaType,
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(self.db_dependency),
|
|
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
|
**path_params,
|
|
):
|
|
f"""Create {self.resource_name}"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
|
|
# Validate parent ID match if enabled and parent_id_name exists
|
|
if self.validate_parent_match and self.parent_id_name:
|
|
parent_id = path_params.get(self.parent_id_name)
|
|
data_parent_id = getattr(data, self.parent_id_name, None)
|
|
if data_parent_id is not None and data_parent_id != parent_id:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"{self.parent_id_name.replace('_', ' ').title()} mismatch",
|
|
)
|
|
|
|
resource = self.service.create(db, data, tenant_id, company_id)
|
|
return resource
|
|
|
|
# PUT route
|
|
# For parent resources: PUT /{id}
|
|
# For child resources: PUT / (parent_id comes from path)
|
|
if self.parent_id_name:
|
|
# Child resource
|
|
@self.router.put("/", response_model=self.response_schema)
|
|
async def update_resource(
|
|
data: UpdateSchemaType,
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(self.db_dependency),
|
|
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
|
**path_params,
|
|
):
|
|
f"""Update {self.resource_name}"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
parent_id = path_params.get(self.parent_id_name)
|
|
|
|
resource = self.service.update(
|
|
db, parent_id, tenant_id, company_id, data
|
|
)
|
|
|
|
if not resource:
|
|
raise HTTPException(
|
|
status_code=404, detail=f"{self.resource_name} not found"
|
|
)
|
|
return resource
|
|
|
|
else:
|
|
# Parent resource
|
|
@self.router.put(
|
|
f"/{{{self.id_name}}}", response_model=self.response_schema
|
|
)
|
|
async def update_resource_by_id(
|
|
data: UpdateSchemaType,
|
|
resource_id: Union[int, str] = Path(..., alias=self.id_name),
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(self.db_dependency),
|
|
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
|
):
|
|
f"""Update {self.resource_name}"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
|
|
resource = self.service.update(
|
|
db, resource_id, tenant_id, company_id, data
|
|
)
|
|
|
|
if not resource:
|
|
raise HTTPException(
|
|
status_code=404, detail=f"{self.resource_name} not found"
|
|
)
|
|
return resource
|
|
|
|
# DELETE route
|
|
# For parent resources: DELETE /{id}
|
|
# For child resources: DELETE / (parent_id comes from path)
|
|
if self.parent_id_name:
|
|
# Child resource
|
|
@self.router.delete("/", status_code=204)
|
|
async def delete_resource(
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(self.db_dependency),
|
|
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
|
**path_params,
|
|
):
|
|
f"""Delete {self.resource_name}"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
parent_id = path_params.get(self.parent_id_name)
|
|
|
|
success = self.service.delete(db, parent_id, tenant_id, company_id)
|
|
|
|
if not success:
|
|
raise HTTPException(
|
|
status_code=404, detail=f"{self.resource_name} not found"
|
|
)
|
|
return None
|
|
|
|
else:
|
|
# Parent resource
|
|
@self.router.delete(f"/{{{self.id_name}}}", status_code=204)
|
|
async def delete_resource_by_id(
|
|
resource_id: Union[int, str] = Path(..., alias=self.id_name),
|
|
company_id: int = Query(..., description="Company ID"),
|
|
db: Session = Depends(self.db_dependency),
|
|
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
|
):
|
|
f"""Delete {self.resource_name}"""
|
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
|
|
|
success = self.service.delete(db, resource_id, tenant_id, company_id)
|
|
|
|
if not success:
|
|
raise HTTPException(
|
|
status_code=404, detail=f"{self.resource_name} not found"
|
|
)
|
|
return None
|