2025-08-13 17:39:35 +02:00
|
|
|
# services/suppliers/app/api/suppliers.py
|
|
|
|
|
"""
|
|
|
|
|
Supplier API endpoints
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Path
|
|
|
|
|
from typing import List, Optional
|
|
|
|
|
from uuid import UUID
|
|
|
|
|
import structlog
|
|
|
|
|
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
from app.core.database import get_db
|
|
|
|
|
from app.services.supplier_service import SupplierService
|
|
|
|
|
from app.schemas.suppliers import (
|
|
|
|
|
SupplierCreate, SupplierUpdate, SupplierResponse, SupplierSummary,
|
|
|
|
|
SupplierSearchParams, SupplierApproval, SupplierStatistics
|
|
|
|
|
)
|
2025-08-15 22:40:19 +02:00
|
|
|
from shared.auth.decorators import get_current_user_dep
|
|
|
|
|
from typing import Dict, Any
|
2025-08-13 17:39:35 +02:00
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/suppliers", tags=["suppliers"])
|
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/", response_model=SupplierResponse)
|
|
|
|
|
async def create_supplier(
|
|
|
|
|
supplier_data: SupplierCreate,
|
2025-08-15 22:40:19 +02:00
|
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
2025-08-13 17:39:35 +02:00
|
|
|
db: Session = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Create a new supplier"""
|
2025-08-15 22:40:19 +02:00
|
|
|
# require_permissions(current_user, ["suppliers:create"])
|
2025-08-13 17:39:35 +02:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
service = SupplierService(db)
|
|
|
|
|
supplier = await service.create_supplier(
|
|
|
|
|
tenant_id=current_user.tenant_id,
|
|
|
|
|
supplier_data=supplier_data,
|
|
|
|
|
created_by=current_user.user_id
|
|
|
|
|
)
|
|
|
|
|
return SupplierResponse.from_orm(supplier)
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error creating supplier", error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to create supplier")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/", response_model=List[SupplierSummary])
|
|
|
|
|
async def list_suppliers(
|
|
|
|
|
search_term: Optional[str] = Query(None, description="Search term"),
|
|
|
|
|
supplier_type: Optional[str] = Query(None, description="Supplier type filter"),
|
|
|
|
|
status: Optional[str] = Query(None, description="Status filter"),
|
|
|
|
|
limit: int = Query(50, ge=1, le=1000, description="Number of results to return"),
|
|
|
|
|
offset: int = Query(0, ge=0, description="Number of results to skip"),
|
2025-08-15 22:40:19 +02:00
|
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
2025-08-13 17:39:35 +02:00
|
|
|
db: Session = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""List suppliers with optional filters"""
|
2025-08-15 22:40:19 +02:00
|
|
|
# require_permissions(current_user, ["suppliers:read"])
|
2025-08-13 17:39:35 +02:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
service = SupplierService(db)
|
|
|
|
|
search_params = SupplierSearchParams(
|
|
|
|
|
search_term=search_term,
|
|
|
|
|
supplier_type=supplier_type,
|
|
|
|
|
status=status,
|
|
|
|
|
limit=limit,
|
|
|
|
|
offset=offset
|
|
|
|
|
)
|
|
|
|
|
suppliers = await service.search_suppliers(
|
|
|
|
|
tenant_id=current_user.tenant_id,
|
|
|
|
|
search_params=search_params
|
|
|
|
|
)
|
|
|
|
|
return [SupplierSummary.from_orm(supplier) for supplier in suppliers]
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error listing suppliers", error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to retrieve suppliers")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/statistics", response_model=SupplierStatistics)
|
|
|
|
|
async def get_supplier_statistics(
|
2025-08-15 22:40:19 +02:00
|
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
2025-08-13 17:39:35 +02:00
|
|
|
db: Session = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Get supplier statistics for dashboard"""
|
2025-08-15 22:40:19 +02:00
|
|
|
# require_permissions(current_user, ["suppliers:read"])
|
2025-08-13 17:39:35 +02:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
service = SupplierService(db)
|
|
|
|
|
stats = await service.get_supplier_statistics(current_user.tenant_id)
|
|
|
|
|
return SupplierStatistics(**stats)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting supplier statistics", error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to retrieve statistics")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/active", response_model=List[SupplierSummary])
|
|
|
|
|
async def get_active_suppliers(
|
2025-08-15 22:40:19 +02:00
|
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
2025-08-13 17:39:35 +02:00
|
|
|
db: Session = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Get all active suppliers"""
|
2025-08-15 22:40:19 +02:00
|
|
|
# require_permissions(current_user, ["suppliers:read"])
|
2025-08-13 17:39:35 +02:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
service = SupplierService(db)
|
|
|
|
|
suppliers = await service.get_active_suppliers(current_user.tenant_id)
|
|
|
|
|
return [SupplierSummary.from_orm(supplier) for supplier in suppliers]
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting active suppliers", error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to retrieve active suppliers")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/top", response_model=List[SupplierSummary])
|
|
|
|
|
async def get_top_suppliers(
|
|
|
|
|
limit: int = Query(10, ge=1, le=50, description="Number of top suppliers to return"),
|
2025-08-15 22:40:19 +02:00
|
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
2025-08-13 17:39:35 +02:00
|
|
|
db: Session = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Get top performing suppliers"""
|
2025-08-15 22:40:19 +02:00
|
|
|
# require_permissions(current_user, ["suppliers:read"])
|
2025-08-13 17:39:35 +02:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
service = SupplierService(db)
|
|
|
|
|
suppliers = await service.get_top_suppliers(current_user.tenant_id, limit)
|
|
|
|
|
return [SupplierSummary.from_orm(supplier) for supplier in suppliers]
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting top suppliers", error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to retrieve top suppliers")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/pending-review", response_model=List[SupplierSummary])
|
|
|
|
|
async def get_suppliers_needing_review(
|
|
|
|
|
days_since_last_order: int = Query(30, ge=1, le=365, description="Days since last order"),
|
2025-08-15 22:40:19 +02:00
|
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
2025-08-13 17:39:35 +02:00
|
|
|
db: Session = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Get suppliers that may need performance review"""
|
2025-08-15 22:40:19 +02:00
|
|
|
# require_permissions(current_user, ["suppliers:read"])
|
2025-08-13 17:39:35 +02:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
service = SupplierService(db)
|
|
|
|
|
suppliers = await service.get_suppliers_needing_review(
|
|
|
|
|
current_user.tenant_id, days_since_last_order
|
|
|
|
|
)
|
|
|
|
|
return [SupplierSummary.from_orm(supplier) for supplier in suppliers]
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting suppliers needing review", error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to retrieve suppliers needing review")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{supplier_id}", response_model=SupplierResponse)
|
|
|
|
|
async def get_supplier(
|
|
|
|
|
supplier_id: UUID = Path(..., description="Supplier ID"),
|
2025-08-15 22:40:19 +02:00
|
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
2025-08-13 17:39:35 +02:00
|
|
|
db: Session = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Get supplier by ID"""
|
2025-08-15 22:40:19 +02:00
|
|
|
# require_permissions(current_user, ["suppliers:read"])
|
2025-08-13 17:39:35 +02:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
service = SupplierService(db)
|
|
|
|
|
supplier = await service.get_supplier(supplier_id)
|
|
|
|
|
|
|
|
|
|
if not supplier:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Supplier not found")
|
|
|
|
|
|
|
|
|
|
# Check tenant access
|
|
|
|
|
if supplier.tenant_id != current_user.tenant_id:
|
|
|
|
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
|
|
|
|
|
|
|
|
return SupplierResponse.from_orm(supplier)
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting supplier", supplier_id=str(supplier_id), error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to retrieve supplier")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/{supplier_id}", response_model=SupplierResponse)
|
|
|
|
|
async def update_supplier(
|
|
|
|
|
supplier_data: SupplierUpdate,
|
|
|
|
|
supplier_id: UUID = Path(..., description="Supplier ID"),
|
2025-08-15 22:40:19 +02:00
|
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
2025-08-13 17:39:35 +02:00
|
|
|
db: Session = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Update supplier information"""
|
2025-08-15 22:40:19 +02:00
|
|
|
# require_permissions(current_user, ["suppliers:update"])
|
2025-08-13 17:39:35 +02:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
service = SupplierService(db)
|
|
|
|
|
|
|
|
|
|
# Check supplier exists and belongs to tenant
|
|
|
|
|
existing_supplier = await service.get_supplier(supplier_id)
|
|
|
|
|
if not existing_supplier:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Supplier not found")
|
|
|
|
|
if existing_supplier.tenant_id != current_user.tenant_id:
|
|
|
|
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
|
|
|
|
|
|
|
|
supplier = await service.update_supplier(
|
|
|
|
|
supplier_id=supplier_id,
|
|
|
|
|
supplier_data=supplier_data,
|
|
|
|
|
updated_by=current_user.user_id
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not supplier:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Supplier not found")
|
|
|
|
|
|
|
|
|
|
return SupplierResponse.from_orm(supplier)
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error updating supplier", supplier_id=str(supplier_id), error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to update supplier")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/{supplier_id}")
|
|
|
|
|
async def delete_supplier(
|
|
|
|
|
supplier_id: UUID = Path(..., description="Supplier ID"),
|
2025-08-15 22:40:19 +02:00
|
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
2025-08-13 17:39:35 +02:00
|
|
|
db: Session = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Delete supplier (soft delete)"""
|
2025-08-15 22:40:19 +02:00
|
|
|
# require_permissions(current_user, ["suppliers:delete"])
|
2025-08-13 17:39:35 +02:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
service = SupplierService(db)
|
|
|
|
|
|
|
|
|
|
# Check supplier exists and belongs to tenant
|
|
|
|
|
existing_supplier = await service.get_supplier(supplier_id)
|
|
|
|
|
if not existing_supplier:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Supplier not found")
|
|
|
|
|
if existing_supplier.tenant_id != current_user.tenant_id:
|
|
|
|
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
|
|
|
|
|
|
|
|
success = await service.delete_supplier(supplier_id)
|
|
|
|
|
if not success:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Supplier not found")
|
|
|
|
|
|
|
|
|
|
return {"message": "Supplier deleted successfully"}
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error deleting supplier", supplier_id=str(supplier_id), error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to delete supplier")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{supplier_id}/approve", response_model=SupplierResponse)
|
|
|
|
|
async def approve_supplier(
|
|
|
|
|
approval_data: SupplierApproval,
|
|
|
|
|
supplier_id: UUID = Path(..., description="Supplier ID"),
|
2025-08-15 22:40:19 +02:00
|
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
2025-08-13 17:39:35 +02:00
|
|
|
db: Session = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Approve or reject a pending supplier"""
|
2025-08-15 22:40:19 +02:00
|
|
|
# require_permissions(current_user, ["suppliers:approve"])
|
2025-08-13 17:39:35 +02:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
service = SupplierService(db)
|
|
|
|
|
|
|
|
|
|
# Check supplier exists and belongs to tenant
|
|
|
|
|
existing_supplier = await service.get_supplier(supplier_id)
|
|
|
|
|
if not existing_supplier:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Supplier not found")
|
|
|
|
|
if existing_supplier.tenant_id != current_user.tenant_id:
|
|
|
|
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
|
|
|
|
|
|
|
|
if approval_data.action == "approve":
|
|
|
|
|
supplier = await service.approve_supplier(
|
|
|
|
|
supplier_id=supplier_id,
|
|
|
|
|
approved_by=current_user.user_id,
|
|
|
|
|
notes=approval_data.notes
|
|
|
|
|
)
|
|
|
|
|
elif approval_data.action == "reject":
|
|
|
|
|
if not approval_data.notes:
|
|
|
|
|
raise HTTPException(status_code=400, detail="Rejection reason is required")
|
|
|
|
|
supplier = await service.reject_supplier(
|
|
|
|
|
supplier_id=supplier_id,
|
|
|
|
|
rejection_reason=approval_data.notes,
|
|
|
|
|
rejected_by=current_user.user_id
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
raise HTTPException(status_code=400, detail="Invalid action")
|
|
|
|
|
|
|
|
|
|
if not supplier:
|
|
|
|
|
raise HTTPException(status_code=400, detail="Supplier is not in pending approval status")
|
|
|
|
|
|
|
|
|
|
return SupplierResponse.from_orm(supplier)
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error processing supplier approval", supplier_id=str(supplier_id), error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to process supplier approval")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/types/{supplier_type}", response_model=List[SupplierSummary])
|
|
|
|
|
async def get_suppliers_by_type(
|
|
|
|
|
supplier_type: str = Path(..., description="Supplier type"),
|
2025-08-15 22:40:19 +02:00
|
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
2025-08-13 17:39:35 +02:00
|
|
|
db: Session = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""Get suppliers by type"""
|
2025-08-15 22:40:19 +02:00
|
|
|
# require_permissions(current_user, ["suppliers:read"])
|
2025-08-13 17:39:35 +02:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from app.models.suppliers import SupplierType
|
|
|
|
|
|
|
|
|
|
# Validate supplier type
|
|
|
|
|
try:
|
|
|
|
|
type_enum = SupplierType(supplier_type.upper())
|
|
|
|
|
except ValueError:
|
|
|
|
|
raise HTTPException(status_code=400, detail="Invalid supplier type")
|
|
|
|
|
|
|
|
|
|
service = SupplierService(db)
|
|
|
|
|
suppliers = await service.get_suppliers_by_type(current_user.tenant_id, type_enum)
|
|
|
|
|
return [SupplierSummary.from_orm(supplier) for supplier in suppliers]
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting suppliers by type", supplier_type=supplier_type, error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail="Failed to retrieve suppliers by type")
|