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
|
|
|
|
|
"""
|
|
|
|
|
|
2025-10-15 16:12:49 +02:00
|
|
|
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
|
2025-10-15 16:12:49 +02:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
2025-10-15 16:12:49 +02:00
|
|
|
@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"])
|
2025-10-15 16:12:49 +02:00
|
|
|
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)}")
|