Files
bakery-ia/services/suppliers/app/api/supplier_operations.py

281 lines
11 KiB
Python
Raw Normal View History

2025-10-06 15:27:01 +02:00
# services/suppliers/app/api/supplier_operations.py
"""
Supplier Business Operations API endpoints (BUSINESS)
Handles approvals, status updates, active/top suppliers, and delivery/PO operations
"""
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Header
2025-10-06 15:27:01 +02:00
from typing import List, Optional, Dict, Any
from uuid import UUID
from datetime import datetime
import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.services.supplier_service import SupplierService
from app.schemas.suppliers import (
2025-12-05 20:07:01 +01:00
SupplierApproval, SupplierResponse, SupplierSummary, SupplierStatistics
2025-10-06 15:27:01 +02:00
)
from app.models.suppliers import SupplierType
2025-10-29 06:58:05 +01:00
from app.models import AuditLog
2025-10-06 15:27:01 +02:00
from shared.auth.decorators import get_current_user_dep
from shared.routing import RouteBuilder
from shared.auth.access_control import require_user_role
from shared.security import create_audit_logger, AuditSeverity, AuditAction
2025-10-06 15:27:01 +02:00
# Create route builder for consistent URL structure
route_builder = RouteBuilder('suppliers')
router = APIRouter(tags=["supplier-operations"])
logger = structlog.get_logger()
2025-10-29 06:58:05 +01:00
audit_logger = create_audit_logger("suppliers-service", AuditLog)
2025-10-06 15:27:01 +02:00
# ===== Supplier Operations =====
2025-10-07 07:15:07 +02:00
@router.get(route_builder.build_operations_route("statistics"), response_model=SupplierStatistics)
2025-10-06 15:27:01 +02:00
async def get_supplier_statistics(
tenant_id: str = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Get supplier statistics for dashboard"""
try:
service = SupplierService(db)
stats = await service.get_supplier_statistics(UUID(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(route_builder.build_operations_route("suppliers/active"), response_model=List[SupplierSummary])
async def get_active_suppliers(
tenant_id: str = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Get all active suppliers"""
try:
service = SupplierService(db)
suppliers = await service.get_active_suppliers(UUID(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(route_builder.build_operations_route("suppliers/top"), response_model=List[SupplierSummary])
async def get_top_suppliers(
tenant_id: str = Path(..., description="Tenant ID"),
limit: int = Query(10, ge=1, le=50, description="Number of top suppliers to return"),
db: AsyncSession = Depends(get_db)
):
"""Get top performing suppliers"""
try:
service = SupplierService(db)
suppliers = await service.get_top_suppliers(UUID(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(route_builder.build_operations_route("suppliers/pending-review"), response_model=List[SupplierSummary])
async def get_suppliers_needing_review(
tenant_id: str = Path(..., description="Tenant ID"),
days_since_last_order: int = Query(30, ge=1, le=365, description="Days since last order"),
db: AsyncSession = Depends(get_db)
):
"""Get suppliers that may need performance review"""
try:
service = SupplierService(db)
suppliers = await service.get_suppliers_needing_review(
UUID(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")
2025-10-27 16:33:26 +01:00
@router.post(route_builder.build_resource_action_route("", "supplier_id", "approve"), response_model=SupplierResponse)
2025-10-06 15:27:01 +02:00
@require_user_role(['admin', 'owner', 'member'])
async def approve_supplier(
approval_data: SupplierApproval,
supplier_id: UUID = Path(..., description="Supplier ID"),
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Approve or reject a pending supplier"""
try:
service = SupplierService(db)
# Check supplier exists
existing_supplier = await service.get_supplier(supplier_id)
if not existing_supplier:
raise HTTPException(status_code=404, detail="Supplier not found")
if approval_data.action == "approve":
supplier = await service.approve_supplier(
supplier_id=supplier_id,
2025-10-27 16:33:26 +01:00
approved_by=current_user["user_id"],
2025-10-06 15:27:01 +02:00
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,
2025-10-27 16:33:26 +01:00
rejected_by=current_user["user_id"]
2025-10-06 15:27:01 +02:00
)
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")
2025-10-27 16:33:26 +01:00
@router.get(route_builder.build_resource_detail_route("types", "supplier_type"), response_model=List[SupplierSummary])
2025-10-06 15:27:01 +02:00
async def get_suppliers_by_type(
supplier_type: str = Path(..., description="Supplier type"),
tenant_id: str = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Get suppliers by type"""
try:
# 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(UUID(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")
@router.get(route_builder.build_operations_route("count"))
async def get_supplier_count(
tenant_id: str = Path(..., description="Tenant ID"),
x_internal_request: str = Header(None),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""
Get total count of suppliers for a tenant
Internal endpoint for subscription usage tracking
"""
if x_internal_request != "true":
raise HTTPException(status_code=403, detail="Internal endpoint only")
try:
service = SupplierService(db)
2025-10-27 16:33:26 +01:00
suppliers = await service.get_suppliers(tenant_id=current_user["tenant_id"])
count = len(suppliers)
return {"count": count}
except Exception as e:
logger.error("Error getting supplier count", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve supplier count")
2025-10-31 11:54:19 +01:00
# ============================================================================
# Tenant Data Deletion Operations (Internal Service Only)
# ============================================================================
from shared.auth.access_control import service_only_access
from shared.services.tenant_deletion import TenantDataDeletionResult
from app.services.tenant_deletion_service import SuppliersTenantDeletionService
@router.delete(
route_builder.build_base_route("tenant/{tenant_id}", include_tenant_prefix=False),
response_model=dict
)
@service_only_access
async def delete_tenant_data(
tenant_id: str = Path(..., description="Tenant ID to delete data for"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Delete all suppliers data for a tenant (Internal service only)
"""
try:
logger.info("suppliers.tenant_deletion.api_called", tenant_id=tenant_id)
deletion_service = SuppliersTenantDeletionService(db)
result = await deletion_service.safe_delete_tenant_data(tenant_id)
if not result.success:
raise HTTPException(
status_code=500,
detail=f"Tenant data deletion failed: {', '.join(result.errors)}"
)
return {
"message": "Tenant data deletion completed successfully",
"summary": result.to_dict()
}
except HTTPException:
raise
except Exception as e:
logger.error("suppliers.tenant_deletion.api_error", tenant_id=tenant_id, error=str(e), exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to delete tenant data: {str(e)}")
@router.get(
route_builder.build_base_route("tenant/{tenant_id}/deletion-preview", include_tenant_prefix=False),
response_model=dict
)
@service_only_access
async def preview_tenant_data_deletion(
tenant_id: str = Path(..., description="Tenant ID to preview deletion for"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Preview what data would be deleted for a tenant (dry-run)
"""
try:
logger.info("suppliers.tenant_deletion.preview_called", tenant_id=tenant_id)
deletion_service = SuppliersTenantDeletionService(db)
preview_data = await deletion_service.get_tenant_data_preview(tenant_id)
result = TenantDataDeletionResult(tenant_id=tenant_id, service_name=deletion_service.service_name)
result.deleted_counts = preview_data
result.success = True
if not result.success:
raise HTTPException(
status_code=500,
detail=f"Tenant deletion preview failed: {', '.join(result.errors)}"
)
return {
"tenant_id": tenant_id,
"service": "suppliers-service",
"data_counts": result.deleted_counts,
"total_items": sum(result.deleted_counts.values())
}
except HTTPException:
raise
except Exception as e:
logger.error("suppliers.tenant_deletion.preview_error", tenant_id=tenant_id, error=str(e), exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to preview tenant data deletion: {str(e)}")