REFACTOR ALL APIs

This commit is contained in:
Urtzi Alfaro
2025-10-06 15:27:01 +02:00
parent dc8221bd2f
commit 38fb98bc27
166 changed files with 18454 additions and 13605 deletions

View File

@@ -1,33 +1,38 @@
# ================================================================
# services/suppliers/app/api/performance.py
# ================================================================
# services/suppliers/app/api/analytics.py
"""
Supplier Performance Tracking API endpoints
Supplier Analytics API endpoints (ANALYTICS)
Consolidates performance metrics, delivery stats, and all analytics operations
"""
from datetime import datetime, timedelta
from typing import List, Optional
from typing import List, Optional, Dict, Any
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, Path, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
import structlog
from shared.auth.decorators import get_current_user_dep
from shared.auth.access_control import require_user_role, analytics_tier_required
from shared.routing import RouteBuilder
from app.core.database import get_db
from app.services.performance_service import PerformanceTrackingService, AlertService
from app.services.dashboard_service import DashboardService
from app.services.delivery_service import DeliveryService
from app.schemas.performance import (
PerformanceMetric, PerformanceMetricCreate, PerformanceMetricUpdate,
Alert, AlertCreate, AlertUpdate, Scorecard, ScorecardCreate, ScorecardUpdate,
PerformanceDashboardSummary, SupplierPerformanceInsights, PerformanceAnalytics,
BusinessModelInsights, AlertSummary, DashboardFilter, AlertFilter,
PerformanceReportRequest, ExportDataResponse
PerformanceMetric, Alert, PerformanceDashboardSummary,
SupplierPerformanceInsights, PerformanceAnalytics, BusinessModelInsights,
AlertSummary, PerformanceReportRequest, ExportDataResponse
)
from app.schemas.suppliers import DeliveryPerformanceStats, DeliverySummaryStats
from app.models.performance import PerformancePeriod, PerformanceMetricType, AlertType, AlertSeverity
logger = structlog.get_logger()
router = APIRouter(prefix="/performance", tags=["performance"])
# Create route builder for consistent URL structure
route_builder = RouteBuilder('suppliers')
router = APIRouter(tags=["analytics"])
# ===== Dependency Injection =====
@@ -45,9 +50,58 @@ async def get_dashboard_service() -> DashboardService:
return DashboardService()
# ===== Performance Metrics Endpoints =====
# ===== Delivery Analytics =====
@router.post("/tenants/{tenant_id}/suppliers/{supplier_id}/calculate", response_model=PerformanceMetric)
@router.get(
route_builder.build_analytics_route("deliveries/performance-stats"),
response_model=DeliveryPerformanceStats
)
async def get_delivery_performance_stats(
tenant_id: UUID = Path(...),
days_back: int = Query(30, ge=1, le=365, description="Number of days to analyze"),
supplier_id: Optional[UUID] = Query(None, description="Filter by supplier ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get delivery performance statistics"""
try:
service = DeliveryService(db)
stats = await service.get_delivery_performance_stats(
tenant_id=current_user.tenant_id,
days_back=days_back,
supplier_id=supplier_id
)
return DeliveryPerformanceStats(**stats)
except Exception as e:
logger.error("Error getting delivery performance stats", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve delivery performance statistics")
@router.get(
route_builder.build_analytics_route("deliveries/summary-stats"),
response_model=DeliverySummaryStats
)
async def get_delivery_summary_stats(
tenant_id: UUID = Path(...),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get delivery summary statistics for dashboard"""
try:
service = DeliveryService(db)
stats = await service.get_upcoming_deliveries_summary(current_user.tenant_id)
return DeliverySummaryStats(**stats)
except Exception as e:
logger.error("Error getting delivery summary stats", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve delivery summary statistics")
# ===== Performance Metrics =====
@router.post(
route_builder.build_analytics_route("performance/{supplier_id}/calculate"),
response_model=PerformanceMetric
)
async def calculate_supplier_performance(
tenant_id: UUID = Path(...),
supplier_id: UUID = Path(...),
@@ -63,20 +117,20 @@ async def calculate_supplier_performance(
metric = await performance_service.calculate_supplier_performance(
db, supplier_id, tenant_id, period, period_start, period_end
)
if not metric:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Unable to calculate performance metrics"
)
logger.info("Performance metrics calculated",
tenant_id=str(tenant_id),
supplier_id=str(supplier_id),
period=period.value)
return metric
except Exception as e:
logger.error("Error calculating performance metrics",
tenant_id=str(tenant_id),
@@ -88,7 +142,10 @@ async def calculate_supplier_performance(
)
@router.get("/tenants/{tenant_id}/suppliers/{supplier_id}/metrics", response_model=List[PerformanceMetric])
@router.get(
route_builder.build_analytics_route("performance/{supplier_id}/metrics"),
response_model=List[PerformanceMetric]
)
async def get_supplier_performance_metrics(
tenant_id: UUID = Path(...),
supplier_id: UUID = Path(...),
@@ -105,9 +162,9 @@ async def get_supplier_performance_metrics(
# TODO: Implement get_supplier_performance_metrics in service
# For now, return empty list
metrics = []
return metrics
except Exception as e:
logger.error("Error getting performance metrics",
tenant_id=str(tenant_id),
@@ -119,9 +176,13 @@ async def get_supplier_performance_metrics(
)
# ===== Alert Management Endpoints =====
# ===== Alert Management =====
@router.post("/tenants/{tenant_id}/alerts/evaluate", response_model=List[Alert])
@router.post(
route_builder.build_analytics_route("performance/alerts/evaluate"),
response_model=List[Alert]
)
@require_user_role(['admin', 'owner'])
async def evaluate_performance_alerts(
tenant_id: UUID = Path(...),
supplier_id: Optional[UUID] = Query(None, description="Specific supplier to evaluate"),
@@ -132,13 +193,13 @@ async def evaluate_performance_alerts(
"""Evaluate and create performance-based alerts"""
try:
alerts = await alert_service.evaluate_performance_alerts(db, tenant_id, supplier_id)
logger.info("Performance alerts evaluated",
tenant_id=str(tenant_id),
alerts_created=len(alerts))
return alerts
except Exception as e:
logger.error("Error evaluating performance alerts",
tenant_id=str(tenant_id),
@@ -149,7 +210,10 @@ async def evaluate_performance_alerts(
)
@router.get("/tenants/{tenant_id}/alerts", response_model=List[Alert])
@router.get(
route_builder.build_analytics_route("performance/alerts"),
response_model=List[Alert]
)
async def get_supplier_alerts(
tenant_id: UUID = Path(...),
supplier_id: Optional[UUID] = Query(None),
@@ -166,9 +230,9 @@ async def get_supplier_alerts(
# TODO: Implement get_supplier_alerts in service
# For now, return empty list
alerts = []
return alerts
except Exception as e:
logger.error("Error getting supplier alerts",
tenant_id=str(tenant_id),
@@ -179,36 +243,40 @@ async def get_supplier_alerts(
)
@router.patch("/tenants/{tenant_id}/alerts/{alert_id}", response_model=Alert)
async def update_alert(
alert_update: AlertUpdate,
@router.get(
route_builder.build_analytics_route("performance/alerts/summary"),
response_model=List[AlertSummary]
)
async def get_alert_summary(
tenant_id: UUID = Path(...),
alert_id: UUID = Path(...),
date_from: Optional[datetime] = Query(None),
date_to: Optional[datetime] = Query(None),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Update an alert (acknowledge, resolve, etc.)"""
"""Get alert summary by type and severity"""
try:
# TODO: Implement update_alert in service
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Alert update not yet implemented"
)
summary = await dashboard_service.get_alert_summary(db, tenant_id, date_from, date_to)
return summary
except Exception as e:
logger.error("Error updating alert",
logger.error("Error getting alert summary",
tenant_id=str(tenant_id),
alert_id=str(alert_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update alert"
detail="Failed to retrieve alert summary"
)
# ===== Dashboard Endpoints =====
# ===== Dashboard Analytics =====
@router.get("/tenants/{tenant_id}/dashboard/summary", response_model=PerformanceDashboardSummary)
@router.get(
route_builder.build_dashboard_route("performance/summary"),
response_model=PerformanceDashboardSummary
)
async def get_performance_dashboard_summary(
tenant_id: UUID = Path(...),
date_from: Optional[datetime] = Query(None),
@@ -222,12 +290,12 @@ async def get_performance_dashboard_summary(
summary = await dashboard_service.get_performance_dashboard_summary(
db, tenant_id, date_from, date_to
)
logger.info("Performance dashboard summary retrieved",
tenant_id=str(tenant_id))
return summary
except Exception as e:
logger.error("Error getting dashboard summary",
tenant_id=str(tenant_id),
@@ -238,7 +306,10 @@ async def get_performance_dashboard_summary(
)
@router.get("/tenants/{tenant_id}/suppliers/{supplier_id}/insights", response_model=SupplierPerformanceInsights)
@router.get(
route_builder.build_analytics_route("performance/{supplier_id}/insights"),
response_model=SupplierPerformanceInsights
)
async def get_supplier_performance_insights(
tenant_id: UUID = Path(...),
supplier_id: UUID = Path(...),
@@ -252,13 +323,13 @@ async def get_supplier_performance_insights(
insights = await dashboard_service.get_supplier_performance_insights(
db, tenant_id, supplier_id, days_back
)
logger.info("Supplier performance insights retrieved",
tenant_id=str(tenant_id),
supplier_id=str(supplier_id))
return insights
except Exception as e:
logger.error("Error getting supplier insights",
tenant_id=str(tenant_id),
@@ -270,7 +341,11 @@ async def get_supplier_performance_insights(
)
@router.get("/tenants/{tenant_id}/analytics", response_model=PerformanceAnalytics)
@router.get(
route_builder.build_analytics_route("performance/performance"),
response_model=PerformanceAnalytics
)
@analytics_tier_required
async def get_performance_analytics(
tenant_id: UUID = Path(...),
period_days: int = Query(90, ge=1, le=365),
@@ -283,13 +358,13 @@ async def get_performance_analytics(
analytics = await dashboard_service.get_performance_analytics(
db, tenant_id, period_days
)
logger.info("Performance analytics retrieved",
tenant_id=str(tenant_id),
period_days=period_days)
return analytics
except Exception as e:
logger.error("Error getting performance analytics",
tenant_id=str(tenant_id),
@@ -300,7 +375,11 @@ async def get_performance_analytics(
)
@router.get("/tenants/{tenant_id}/business-model", response_model=BusinessModelInsights)
@router.get(
route_builder.build_analytics_route("performance/business-model"),
response_model=BusinessModelInsights
)
@analytics_tier_required
async def get_business_model_insights(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
@@ -310,13 +389,13 @@ async def get_business_model_insights(
"""Get business model detection and insights"""
try:
insights = await dashboard_service.get_business_model_insights(db, tenant_id)
logger.info("Business model insights retrieved",
tenant_id=str(tenant_id),
detected_model=insights.detected_model)
return insights
except Exception as e:
logger.error("Error getting business model insights",
tenant_id=str(tenant_id),
@@ -327,34 +406,13 @@ async def get_business_model_insights(
)
@router.get("/tenants/{tenant_id}/alerts/summary", response_model=List[AlertSummary])
async def get_alert_summary(
tenant_id: UUID = Path(...),
date_from: Optional[datetime] = Query(None),
date_to: Optional[datetime] = Query(None),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get alert summary by type and severity"""
try:
summary = await dashboard_service.get_alert_summary(db, tenant_id, date_from, date_to)
return summary
except Exception as e:
logger.error("Error getting alert summary",
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve alert summary"
)
# ===== Export and Reporting =====
# ===== Export and Reporting Endpoints =====
@router.post("/tenants/{tenant_id}/reports/generate", response_model=ExportDataResponse)
@router.post(
route_builder.build_analytics_route("performance/reports/generate"),
response_model=ExportDataResponse
)
@require_user_role(['admin', 'owner'])
async def generate_performance_report(
report_request: PerformanceReportRequest,
tenant_id: UUID = Path(...),
@@ -368,7 +426,7 @@ async def generate_performance_report(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Report generation not yet implemented"
)
except Exception as e:
logger.error("Error generating performance report",
tenant_id=str(tenant_id),
@@ -379,7 +437,9 @@ async def generate_performance_report(
)
@router.get("/tenants/{tenant_id}/export")
@router.get(
route_builder.build_analytics_route("performance/export")
)
async def export_performance_data(
tenant_id: UUID = Path(...),
format: str = Query("json", description="Export format: json, csv, excel"),
@@ -396,13 +456,13 @@ async def export_performance_data(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Unsupported export format. Use: json, csv, excel"
)
# TODO: Implement data export
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Data export not yet implemented"
)
except Exception as e:
logger.error("Error exporting performance data",
tenant_id=str(tenant_id),
@@ -413,9 +473,11 @@ async def export_performance_data(
)
# ===== Configuration and Health Endpoints =====
# ===== Configuration and Health =====
@router.get("/tenants/{tenant_id}/config")
@router.get(
route_builder.build_analytics_route("performance/config")
)
async def get_performance_config(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep)
@@ -423,7 +485,7 @@ async def get_performance_config(
"""Get performance tracking configuration"""
try:
from app.core.config import settings
config = {
"performance_tracking": {
"enabled": settings.PERFORMANCE_TRACKING_ENABLED,
@@ -458,9 +520,9 @@ async def get_performance_config(
"individual_bakery_threshold": settings.INDIVIDUAL_BAKERY_THRESHOLD_SUPPLIERS
}
}
return config
except Exception as e:
logger.error("Error getting performance config",
tenant_id=str(tenant_id),
@@ -471,7 +533,9 @@ async def get_performance_config(
)
@router.get("/tenants/{tenant_id}/health")
@router.get(
route_builder.build_analytics_route("performance/health")
)
async def get_performance_health(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep)
@@ -490,7 +554,7 @@ async def get_performance_health(
"business_model_detection": "enabled"
}
}
except Exception as e:
logger.error("Error getting performance health",
tenant_id=str(tenant_id),
@@ -498,4 +562,4 @@ async def get_performance_health(
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get performance health status"
)
)

View File

@@ -1,10 +1,10 @@
# services/suppliers/app/api/deliveries.py
"""
Delivery API endpoints
Delivery CRUD API endpoints (ATOMIC)
"""
from fastapi import APIRouter, Depends, HTTPException, Query, Path
from typing import List, Optional
from typing import List, Optional, Dict, Any
from uuid import UUID
import structlog
@@ -13,18 +13,23 @@ from app.core.database import get_db
from app.services.delivery_service import DeliveryService
from app.schemas.suppliers import (
DeliveryCreate, DeliveryUpdate, DeliveryResponse, DeliverySummary,
DeliverySearchParams, DeliveryStatusUpdate, DeliveryReceiptConfirmation,
DeliveryPerformanceStats, DeliverySummaryStats
DeliverySearchParams
)
from app.models.suppliers import DeliveryStatus
from shared.auth.decorators import get_current_user_dep
from typing import Dict, Any
from shared.routing import RouteBuilder
from shared.auth.access_control import require_user_role
router = APIRouter(prefix="/deliveries", tags=["deliveries"])
# Create route builder for consistent URL structure
route_builder = RouteBuilder('suppliers')
router = APIRouter(tags=["deliveries"])
logger = structlog.get_logger()
@router.post("/", response_model=DeliveryResponse)
@router.post(route_builder.build_base_route("deliveries"), response_model=DeliveryResponse)
@require_user_role(['admin', 'owner', 'member'])
async def create_delivery(
delivery_data: DeliveryCreate,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
@@ -48,7 +53,7 @@ async def create_delivery(
raise HTTPException(status_code=500, detail="Failed to create delivery")
@router.get("/", response_model=List[DeliverySummary])
@router.get(route_builder.build_base_route("deliveries"), response_model=List[DeliverySummary])
async def list_deliveries(
supplier_id: Optional[UUID] = Query(None, description="Filter by supplier ID"),
status: Optional[str] = Query(None, description="Filter by status"),
@@ -113,123 +118,7 @@ async def list_deliveries(
raise HTTPException(status_code=500, detail="Failed to retrieve deliveries")
@router.get("/today", response_model=List[DeliverySummary])
async def get_todays_deliveries(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get deliveries scheduled for today"""
# require_permissions(current_user, ["deliveries:read"])
try:
service = DeliveryService(db)
deliveries = await service.get_todays_deliveries(current_user.tenant_id)
return [DeliverySummary.from_orm(delivery) for delivery in deliveries]
except Exception as e:
logger.error("Error getting today's deliveries", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve today's deliveries")
@router.get("/overdue", response_model=List[DeliverySummary])
async def get_overdue_deliveries(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get overdue deliveries"""
# require_permissions(current_user, ["deliveries:read"])
try:
service = DeliveryService(db)
deliveries = await service.get_overdue_deliveries(current_user.tenant_id)
return [DeliverySummary.from_orm(delivery) for delivery in deliveries]
except Exception as e:
logger.error("Error getting overdue deliveries", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve overdue deliveries")
@router.get("/scheduled", response_model=List[DeliverySummary])
async def get_scheduled_deliveries(
date_from: Optional[str] = Query(None, description="From date (YYYY-MM-DD)"),
date_to: Optional[str] = Query(None, description="To date (YYYY-MM-DD)"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get scheduled deliveries for a date range"""
# require_permissions(current_user, ["deliveries:read"])
try:
from datetime import datetime
date_from_parsed = None
date_to_parsed = None
if date_from:
try:
date_from_parsed = datetime.fromisoformat(date_from)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date_from format")
if date_to:
try:
date_to_parsed = datetime.fromisoformat(date_to)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date_to format")
service = DeliveryService(db)
deliveries = await service.get_scheduled_deliveries(
tenant_id=current_user.tenant_id,
date_from=date_from_parsed,
date_to=date_to_parsed
)
return [DeliverySummary.from_orm(delivery) for delivery in deliveries]
except HTTPException:
raise
except Exception as e:
logger.error("Error getting scheduled deliveries", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve scheduled deliveries")
@router.get("/performance-stats", response_model=DeliveryPerformanceStats)
async def get_delivery_performance_stats(
days_back: int = Query(30, ge=1, le=365, description="Number of days to analyze"),
supplier_id: Optional[UUID] = Query(None, description="Filter by supplier ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get delivery performance statistics"""
# require_permissions(current_user, ["deliveries:read"])
try:
service = DeliveryService(db)
stats = await service.get_delivery_performance_stats(
tenant_id=current_user.tenant_id,
days_back=days_back,
supplier_id=supplier_id
)
return DeliveryPerformanceStats(**stats)
except Exception as e:
logger.error("Error getting delivery performance stats", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve delivery performance statistics")
@router.get("/summary-stats", response_model=DeliverySummaryStats)
async def get_delivery_summary_stats(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get delivery summary statistics for dashboard"""
# require_permissions(current_user, ["deliveries:read"])
try:
service = DeliveryService(db)
stats = await service.get_upcoming_deliveries_summary(current_user.tenant_id)
return DeliverySummaryStats(**stats)
except Exception as e:
logger.error("Error getting delivery summary stats", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve delivery summary statistics")
@router.get("/{delivery_id}", response_model=DeliveryResponse)
@router.get(route_builder.build_resource_detail_route("deliveries", "delivery_id"), response_model=DeliveryResponse)
async def get_delivery(
delivery_id: UUID = Path(..., description="Delivery ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
@@ -257,7 +146,8 @@ async def get_delivery(
raise HTTPException(status_code=500, detail="Failed to retrieve delivery")
@router.put("/{delivery_id}", response_model=DeliveryResponse)
@router.put(route_builder.build_resource_detail_route("deliveries", "delivery_id"), response_model=DeliveryResponse)
@require_user_role(['admin', 'owner', 'member'])
async def update_delivery(
delivery_data: DeliveryUpdate,
delivery_id: UUID = Path(..., description="Delivery ID"),
@@ -296,109 +186,3 @@ async def update_delivery(
raise HTTPException(status_code=500, detail="Failed to update delivery")
@router.patch("/{delivery_id}/status", response_model=DeliveryResponse)
async def update_delivery_status(
status_data: DeliveryStatusUpdate,
delivery_id: UUID = Path(..., description="Delivery ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Update delivery status"""
# require_permissions(current_user, ["deliveries:update"])
try:
service = DeliveryService(db)
# Check delivery exists and belongs to tenant
existing_delivery = await service.get_delivery(delivery_id)
if not existing_delivery:
raise HTTPException(status_code=404, detail="Delivery not found")
if existing_delivery.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
delivery = await service.update_delivery_status(
delivery_id=delivery_id,
status=status_data.status,
updated_by=current_user.user_id,
notes=status_data.notes,
update_timestamps=status_data.update_timestamps
)
if not delivery:
raise HTTPException(status_code=404, detail="Delivery not found")
return DeliveryResponse.from_orm(delivery)
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error updating delivery status", delivery_id=str(delivery_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to update delivery status")
@router.post("/{delivery_id}/receive", response_model=DeliveryResponse)
async def receive_delivery(
receipt_data: DeliveryReceiptConfirmation,
delivery_id: UUID = Path(..., description="Delivery ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Mark delivery as received with inspection details"""
# require_permissions(current_user, ["deliveries:receive"])
try:
service = DeliveryService(db)
# Check delivery exists and belongs to tenant
existing_delivery = await service.get_delivery(delivery_id)
if not existing_delivery:
raise HTTPException(status_code=404, detail="Delivery not found")
if existing_delivery.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
delivery = await service.mark_as_received(
delivery_id=delivery_id,
received_by=current_user.user_id,
inspection_passed=receipt_data.inspection_passed,
inspection_notes=receipt_data.inspection_notes,
quality_issues=receipt_data.quality_issues,
notes=receipt_data.notes
)
if not delivery:
raise HTTPException(status_code=404, detail="Delivery not found")
return DeliveryResponse.from_orm(delivery)
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error receiving delivery", delivery_id=str(delivery_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to receive delivery")
@router.get("/purchase-order/{po_id}", response_model=List[DeliverySummary])
async def get_deliveries_by_purchase_order(
po_id: UUID = Path(..., description="Purchase order ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get all deliveries for a purchase order"""
# require_permissions(current_user, ["deliveries:read"])
try:
service = DeliveryService(db)
deliveries = await service.get_deliveries_by_purchase_order(po_id)
# Check tenant access for first delivery (all should belong to same tenant)
if deliveries and deliveries[0].tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
return [DeliverySummary.from_orm(delivery) for delivery in deliveries]
except HTTPException:
raise
except Exception as e:
logger.error("Error getting deliveries by purchase order", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve deliveries for purchase order")

View File

@@ -1,10 +1,10 @@
# services/suppliers/app/api/purchase_orders.py
"""
Purchase Order API endpoints
Purchase Order CRUD API endpoints (ATOMIC)
"""
from fastapi import APIRouter, Depends, HTTPException, Query, Path
from typing import List, Optional
from typing import List, Optional, Dict, Any
from uuid import UUID
import structlog
@@ -13,18 +13,23 @@ from app.core.database import get_db
from app.services.purchase_order_service import PurchaseOrderService
from app.schemas.suppliers import (
PurchaseOrderCreate, PurchaseOrderUpdate, PurchaseOrderResponse, PurchaseOrderSummary,
PurchaseOrderSearchParams, PurchaseOrderStatusUpdate, PurchaseOrderApproval,
PurchaseOrderStatistics
PurchaseOrderSearchParams
)
from app.models.suppliers import PurchaseOrderStatus
from shared.auth.decorators import get_current_user_dep
from typing import Dict, Any
from shared.routing import RouteBuilder
from shared.auth.access_control import require_user_role
router = APIRouter(prefix="/purchase-orders", tags=["purchase-orders"])
# Create route builder for consistent URL structure
route_builder = RouteBuilder('suppliers')
router = APIRouter(tags=["purchase-orders"])
logger = structlog.get_logger()
@router.post("/", response_model=PurchaseOrderResponse)
@router.post(route_builder.build_base_route("purchase-orders"), response_model=PurchaseOrderResponse)
@require_user_role(['admin', 'owner', 'member'])
async def create_purchase_order(
po_data: PurchaseOrderCreate,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
@@ -48,7 +53,7 @@ async def create_purchase_order(
raise HTTPException(status_code=500, detail="Failed to create purchase order")
@router.get("/", response_model=List[PurchaseOrderSummary])
@router.get(route_builder.build_base_route("purchase-orders"), response_model=List[PurchaseOrderSummary])
async def list_purchase_orders(
supplier_id: Optional[UUID] = Query(None, description="Filter by supplier ID"),
status: Optional[str] = Query(None, description="Filter by status"),
@@ -115,58 +120,7 @@ async def list_purchase_orders(
raise HTTPException(status_code=500, detail="Failed to retrieve purchase orders")
@router.get("/statistics", response_model=PurchaseOrderStatistics)
async def get_purchase_order_statistics(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get purchase order statistics for dashboard"""
# require_permissions(current_user, ["purchase_orders:read"])
try:
service = PurchaseOrderService(db)
stats = await service.get_purchase_order_statistics(current_user.tenant_id)
return PurchaseOrderStatistics(**stats)
except Exception as e:
logger.error("Error getting purchase order statistics", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve statistics")
@router.get("/pending-approval", response_model=List[PurchaseOrderSummary])
async def get_orders_requiring_approval(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get purchase orders requiring approval"""
# require_permissions(current_user, ["purchase_orders:approve"])
try:
service = PurchaseOrderService(db)
orders = await service.get_orders_requiring_approval(current_user.tenant_id)
return [PurchaseOrderSummary.from_orm(order) for order in orders]
except Exception as e:
logger.error("Error getting orders requiring approval", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve orders requiring approval")
@router.get("/overdue", response_model=List[PurchaseOrderSummary])
async def get_overdue_orders(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get overdue purchase orders"""
# require_permissions(current_user, ["purchase_orders:read"])
try:
service = PurchaseOrderService(db)
orders = await service.get_overdue_orders(current_user.tenant_id)
return [PurchaseOrderSummary.from_orm(order) for order in orders]
except Exception as e:
logger.error("Error getting overdue orders", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve overdue orders")
@router.get("/{po_id}", response_model=PurchaseOrderResponse)
@router.get(route_builder.build_resource_detail_route("purchase-orders", "po_id"), response_model=PurchaseOrderResponse)
async def get_purchase_order(
po_id: UUID = Path(..., description="Purchase order ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
@@ -194,7 +148,8 @@ async def get_purchase_order(
raise HTTPException(status_code=500, detail="Failed to retrieve purchase order")
@router.put("/{po_id}", response_model=PurchaseOrderResponse)
@router.put(route_builder.build_resource_detail_route("purchase-orders", "po_id"), response_model=PurchaseOrderResponse)
@require_user_role(['admin', 'owner', 'member'])
async def update_purchase_order(
po_data: PurchaseOrderUpdate,
po_id: UUID = Path(..., description="Purchase order ID"),
@@ -233,278 +188,3 @@ async def update_purchase_order(
raise HTTPException(status_code=500, detail="Failed to update purchase order")
@router.patch("/{po_id}/status", response_model=PurchaseOrderResponse)
async def update_purchase_order_status(
status_data: PurchaseOrderStatusUpdate,
po_id: UUID = Path(..., description="Purchase order ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Update purchase order status"""
# require_permissions(current_user, ["purchase_orders:update"])
try:
service = PurchaseOrderService(db)
# Check order exists and belongs to tenant
existing_order = await service.get_purchase_order(po_id)
if not existing_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
if existing_order.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
purchase_order = await service.update_order_status(
po_id=po_id,
status=status_data.status,
updated_by=current_user.user_id,
notes=status_data.notes
)
if not purchase_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
return PurchaseOrderResponse.from_orm(purchase_order)
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error updating purchase order status", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to update purchase order status")
@router.post("/{po_id}/approve", response_model=PurchaseOrderResponse)
async def approve_purchase_order(
approval_data: PurchaseOrderApproval,
po_id: UUID = Path(..., description="Purchase order ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Approve or reject a purchase order"""
# require_permissions(current_user, ["purchase_orders:approve"])
try:
service = PurchaseOrderService(db)
# Check order exists and belongs to tenant
existing_order = await service.get_purchase_order(po_id)
if not existing_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
if existing_order.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
if approval_data.action == "approve":
purchase_order = await service.approve_purchase_order(
po_id=po_id,
approved_by=current_user.user_id,
approval_notes=approval_data.notes
)
elif approval_data.action == "reject":
if not approval_data.notes:
raise HTTPException(status_code=400, detail="Rejection reason is required")
purchase_order = await service.reject_purchase_order(
po_id=po_id,
rejection_reason=approval_data.notes,
rejected_by=current_user.user_id
)
else:
raise HTTPException(status_code=400, detail="Invalid action")
if not purchase_order:
raise HTTPException(
status_code=400,
detail="Purchase order is not in pending approval status"
)
return PurchaseOrderResponse.from_orm(purchase_order)
except HTTPException:
raise
except Exception as e:
logger.error("Error processing purchase order approval", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to process purchase order approval")
@router.post("/{po_id}/send-to-supplier", response_model=PurchaseOrderResponse)
async def send_to_supplier(
po_id: UUID = Path(..., description="Purchase order ID"),
send_email: bool = Query(True, description="Send email notification to supplier"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Send purchase order to supplier"""
# require_permissions(current_user, ["purchase_orders:send"])
try:
service = PurchaseOrderService(db)
# Check order exists and belongs to tenant
existing_order = await service.get_purchase_order(po_id)
if not existing_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
if existing_order.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
purchase_order = await service.send_to_supplier(
po_id=po_id,
sent_by=current_user.user_id,
send_email=send_email
)
if not purchase_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
return PurchaseOrderResponse.from_orm(purchase_order)
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error sending purchase order to supplier", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to send purchase order to supplier")
@router.post("/{po_id}/confirm-supplier-receipt", response_model=PurchaseOrderResponse)
async def confirm_supplier_receipt(
po_id: UUID = Path(..., description="Purchase order ID"),
supplier_reference: Optional[str] = Query(None, description="Supplier's order reference"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Confirm supplier has received and accepted the order"""
# require_permissions(current_user, ["purchase_orders:update"])
try:
service = PurchaseOrderService(db)
# Check order exists and belongs to tenant
existing_order = await service.get_purchase_order(po_id)
if not existing_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
if existing_order.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
purchase_order = await service.confirm_supplier_receipt(
po_id=po_id,
supplier_reference=supplier_reference,
confirmed_by=current_user.user_id
)
if not purchase_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
return PurchaseOrderResponse.from_orm(purchase_order)
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error confirming supplier receipt", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to confirm supplier receipt")
@router.post("/{po_id}/cancel", response_model=PurchaseOrderResponse)
async def cancel_purchase_order(
po_id: UUID = Path(..., description="Purchase order ID"),
cancellation_reason: str = Query(..., description="Reason for cancellation"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Cancel a purchase order"""
# require_permissions(current_user, ["purchase_orders:cancel"])
try:
service = PurchaseOrderService(db)
# Check order exists and belongs to tenant
existing_order = await service.get_purchase_order(po_id)
if not existing_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
if existing_order.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
purchase_order = await service.cancel_purchase_order(
po_id=po_id,
cancellation_reason=cancellation_reason,
cancelled_by=current_user.user_id
)
if not purchase_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
return PurchaseOrderResponse.from_orm(purchase_order)
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error cancelling purchase order", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to cancel purchase order")
@router.get("/supplier/{supplier_id}", response_model=List[PurchaseOrderSummary])
async def get_orders_by_supplier(
supplier_id: UUID = Path(..., description="Supplier ID"),
limit: int = Query(20, ge=1, le=100, description="Number of orders to return"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get recent purchase orders for a specific supplier"""
# require_permissions(current_user, ["purchase_orders:read"])
try:
service = PurchaseOrderService(db)
orders = await service.get_orders_by_supplier(
tenant_id=current_user.tenant_id,
supplier_id=supplier_id,
limit=limit
)
return [PurchaseOrderSummary.from_orm(order) for order in orders]
except Exception as e:
logger.error("Error getting orders by supplier", supplier_id=str(supplier_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve orders by supplier")
@router.get("/inventory-products/{inventory_product_id}/history")
async def get_inventory_product_purchase_history(
inventory_product_id: UUID = Path(..., description="Inventory Product ID"),
days_back: int = Query(90, ge=1, le=365, description="Number of days to look back"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get purchase history for a specific inventory product"""
# require_permissions(current_user, ["purchase_orders:read"])
try:
service = PurchaseOrderService(db)
history = await service.get_inventory_product_purchase_history(
tenant_id=current_user.tenant_id,
inventory_product_id=inventory_product_id,
days_back=days_back
)
return history
except Exception as e:
logger.error("Error getting inventory product purchase history", inventory_product_id=str(inventory_product_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve inventory product purchase history")
@router.get("/inventory-products/top-purchased")
async def get_top_purchased_inventory_products(
days_back: int = Query(30, ge=1, le=365, description="Number of days to look back"),
limit: int = Query(10, ge=1, le=50, description="Number of top inventory products to return"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get most purchased inventory products by value"""
# require_permissions(current_user, ["purchase_orders:read"])
try:
service = PurchaseOrderService(db)
products = await service.get_top_purchased_inventory_products(
tenant_id=current_user.tenant_id,
days_back=days_back,
limit=limit
)
return products
except Exception as e:
logger.error("Error getting top purchased inventory products", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve top purchased inventory products")

View File

@@ -0,0 +1,674 @@
# 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
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.services.delivery_service import DeliveryService
from app.services.purchase_order_service import PurchaseOrderService
from app.schemas.suppliers import (
SupplierApproval, SupplierResponse, SupplierSummary, SupplierStatistics,
DeliveryStatusUpdate, DeliveryReceiptConfirmation, DeliveryResponse, DeliverySummary,
PurchaseOrderStatusUpdate, PurchaseOrderApproval, PurchaseOrderResponse, PurchaseOrderSummary
)
from app.models.suppliers import SupplierType
from shared.auth.decorators import get_current_user_dep
from shared.routing import RouteBuilder
from shared.auth.access_control import require_user_role
# Create route builder for consistent URL structure
route_builder = RouteBuilder('suppliers')
router = APIRouter(tags=["supplier-operations"])
logger = structlog.get_logger()
# ===== Supplier Operations =====
@router.get(route_builder.build_operations_route("suppliers/statistics"), response_model=SupplierStatistics)
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")
@router.post(route_builder.build_nested_resource_route("suppliers", "supplier_id", "approve"), response_model=SupplierResponse)
@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,
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(route_builder.build_resource_detail_route("suppliers/types", "supplier_type"), response_model=List[SupplierSummary])
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")
# ===== Delivery Operations =====
@router.get(route_builder.build_operations_route("deliveries/today"), response_model=List[DeliverySummary])
async def get_todays_deliveries(
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get deliveries scheduled for today"""
try:
service = DeliveryService(db)
deliveries = await service.get_todays_deliveries(current_user.tenant_id)
return [DeliverySummary.from_orm(delivery) for delivery in deliveries]
except Exception as e:
logger.error("Error getting today's deliveries", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve today's deliveries")
@router.get(route_builder.build_operations_route("deliveries/overdue"), response_model=List[DeliverySummary])
async def get_overdue_deliveries(
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get overdue deliveries"""
try:
service = DeliveryService(db)
deliveries = await service.get_overdue_deliveries(current_user.tenant_id)
return [DeliverySummary.from_orm(delivery) for delivery in deliveries]
except Exception as e:
logger.error("Error getting overdue deliveries", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve overdue deliveries")
@router.get(route_builder.build_operations_route("deliveries/scheduled"), response_model=List[DeliverySummary])
async def get_scheduled_deliveries(
tenant_id: str = Path(..., description="Tenant ID"),
date_from: Optional[str] = Query(None, description="From date (YYYY-MM-DD)"),
date_to: Optional[str] = Query(None, description="To date (YYYY-MM-DD)"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get scheduled deliveries for a date range"""
try:
date_from_parsed = None
date_to_parsed = None
if date_from:
try:
date_from_parsed = datetime.fromisoformat(date_from)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date_from format")
if date_to:
try:
date_to_parsed = datetime.fromisoformat(date_to)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date_to format")
service = DeliveryService(db)
deliveries = await service.get_scheduled_deliveries(
tenant_id=current_user.tenant_id,
date_from=date_from_parsed,
date_to=date_to_parsed
)
return [DeliverySummary.from_orm(delivery) for delivery in deliveries]
except HTTPException:
raise
except Exception as e:
logger.error("Error getting scheduled deliveries", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve scheduled deliveries")
@router.patch(route_builder.build_nested_resource_route("deliveries", "delivery_id", "status"), response_model=DeliveryResponse)
@require_user_role(['admin', 'owner', 'member'])
async def update_delivery_status(
status_data: DeliveryStatusUpdate,
delivery_id: UUID = Path(..., description="Delivery ID"),
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Update delivery status"""
try:
service = DeliveryService(db)
# Check delivery exists and belongs to tenant
existing_delivery = await service.get_delivery(delivery_id)
if not existing_delivery:
raise HTTPException(status_code=404, detail="Delivery not found")
if existing_delivery.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
delivery = await service.update_delivery_status(
delivery_id=delivery_id,
status=status_data.status,
updated_by=current_user.user_id,
notes=status_data.notes,
update_timestamps=status_data.update_timestamps
)
if not delivery:
raise HTTPException(status_code=404, detail="Delivery not found")
return DeliveryResponse.from_orm(delivery)
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error updating delivery status", delivery_id=str(delivery_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to update delivery status")
@router.post(route_builder.build_nested_resource_route("deliveries", "delivery_id", "receive"), response_model=DeliveryResponse)
@require_user_role(['admin', 'owner', 'member'])
async def receive_delivery(
receipt_data: DeliveryReceiptConfirmation,
delivery_id: UUID = Path(..., description="Delivery ID"),
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Mark delivery as received with inspection details"""
try:
service = DeliveryService(db)
# Check delivery exists and belongs to tenant
existing_delivery = await service.get_delivery(delivery_id)
if not existing_delivery:
raise HTTPException(status_code=404, detail="Delivery not found")
if existing_delivery.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
delivery = await service.mark_as_received(
delivery_id=delivery_id,
received_by=current_user.user_id,
inspection_passed=receipt_data.inspection_passed,
inspection_notes=receipt_data.inspection_notes,
quality_issues=receipt_data.quality_issues,
notes=receipt_data.notes
)
if not delivery:
raise HTTPException(status_code=404, detail="Delivery not found")
return DeliveryResponse.from_orm(delivery)
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error receiving delivery", delivery_id=str(delivery_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to receive delivery")
@router.get(route_builder.build_resource_detail_route("deliveries/purchase-order", "po_id"), response_model=List[DeliverySummary])
async def get_deliveries_by_purchase_order(
po_id: UUID = Path(..., description="Purchase order ID"),
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get all deliveries for a purchase order"""
try:
service = DeliveryService(db)
deliveries = await service.get_deliveries_by_purchase_order(po_id)
# Check tenant access for first delivery (all should belong to same tenant)
if deliveries and deliveries[0].tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
return [DeliverySummary.from_orm(delivery) for delivery in deliveries]
except HTTPException:
raise
except Exception as e:
logger.error("Error getting deliveries by purchase order", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve deliveries for purchase order")
# ===== Purchase Order Operations =====
@router.get(route_builder.build_operations_route("purchase-orders/statistics"), response_model=dict)
async def get_purchase_order_statistics(
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get purchase order statistics for dashboard"""
try:
service = PurchaseOrderService(db)
stats = await service.get_purchase_order_statistics(current_user.tenant_id)
return stats
except Exception as e:
logger.error("Error getting purchase order statistics", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve statistics")
@router.get(route_builder.build_operations_route("purchase-orders/pending-approval"), response_model=List[PurchaseOrderSummary])
async def get_orders_requiring_approval(
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get purchase orders requiring approval"""
try:
service = PurchaseOrderService(db)
orders = await service.get_orders_requiring_approval(current_user.tenant_id)
return [PurchaseOrderSummary.from_orm(order) for order in orders]
except Exception as e:
logger.error("Error getting orders requiring approval", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve orders requiring approval")
@router.get(route_builder.build_operations_route("purchase-orders/overdue"), response_model=List[PurchaseOrderSummary])
async def get_overdue_orders(
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get overdue purchase orders"""
try:
service = PurchaseOrderService(db)
orders = await service.get_overdue_orders(current_user.tenant_id)
return [PurchaseOrderSummary.from_orm(order) for order in orders]
except Exception as e:
logger.error("Error getting overdue orders", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve overdue orders")
@router.patch(route_builder.build_nested_resource_route("purchase-orders", "po_id", "status"), response_model=PurchaseOrderResponse)
@require_user_role(['admin', 'owner', 'member'])
async def update_purchase_order_status(
status_data: PurchaseOrderStatusUpdate,
po_id: UUID = Path(..., description="Purchase order ID"),
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Update purchase order status"""
try:
service = PurchaseOrderService(db)
# Check order exists and belongs to tenant
existing_order = await service.get_purchase_order(po_id)
if not existing_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
if existing_order.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
purchase_order = await service.update_order_status(
po_id=po_id,
status=status_data.status,
updated_by=current_user.user_id,
notes=status_data.notes
)
if not purchase_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
return PurchaseOrderResponse.from_orm(purchase_order)
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error updating purchase order status", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to update purchase order status")
@router.post(route_builder.build_nested_resource_route("purchase-orders", "po_id", "approve"), response_model=PurchaseOrderResponse)
@require_user_role(['admin', 'owner', 'member'])
async def approve_purchase_order(
approval_data: PurchaseOrderApproval,
po_id: UUID = Path(..., description="Purchase order ID"),
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Approve or reject a purchase order"""
try:
service = PurchaseOrderService(db)
# Check order exists and belongs to tenant
existing_order = await service.get_purchase_order(po_id)
if not existing_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
if existing_order.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
if approval_data.action == "approve":
purchase_order = await service.approve_purchase_order(
po_id=po_id,
approved_by=current_user.user_id,
approval_notes=approval_data.notes
)
elif approval_data.action == "reject":
if not approval_data.notes:
raise HTTPException(status_code=400, detail="Rejection reason is required")
purchase_order = await service.reject_purchase_order(
po_id=po_id,
rejection_reason=approval_data.notes,
rejected_by=current_user.user_id
)
else:
raise HTTPException(status_code=400, detail="Invalid action")
if not purchase_order:
raise HTTPException(
status_code=400,
detail="Purchase order is not in pending approval status"
)
return PurchaseOrderResponse.from_orm(purchase_order)
except HTTPException:
raise
except Exception as e:
logger.error("Error processing purchase order approval", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to process purchase order approval")
@router.post(route_builder.build_nested_resource_route("purchase-orders", "po_id", "send-to-supplier"), response_model=PurchaseOrderResponse)
@require_user_role(['admin', 'owner', 'member'])
async def send_to_supplier(
po_id: UUID = Path(..., description="Purchase order ID"),
tenant_id: str = Path(..., description="Tenant ID"),
send_email: bool = Query(True, description="Send email notification to supplier"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Send purchase order to supplier"""
try:
service = PurchaseOrderService(db)
# Check order exists and belongs to tenant
existing_order = await service.get_purchase_order(po_id)
if not existing_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
if existing_order.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
purchase_order = await service.send_to_supplier(
po_id=po_id,
sent_by=current_user.user_id,
send_email=send_email
)
if not purchase_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
return PurchaseOrderResponse.from_orm(purchase_order)
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error sending purchase order to supplier", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to send purchase order to supplier")
@router.post(route_builder.build_nested_resource_route("purchase-orders", "po_id", "confirm-supplier-receipt"), response_model=PurchaseOrderResponse)
@require_user_role(['admin', 'owner', 'member'])
async def confirm_supplier_receipt(
po_id: UUID = Path(..., description="Purchase order ID"),
tenant_id: str = Path(..., description="Tenant ID"),
supplier_reference: Optional[str] = Query(None, description="Supplier's order reference"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Confirm supplier has received and accepted the order"""
try:
service = PurchaseOrderService(db)
# Check order exists and belongs to tenant
existing_order = await service.get_purchase_order(po_id)
if not existing_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
if existing_order.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
purchase_order = await service.confirm_supplier_receipt(
po_id=po_id,
supplier_reference=supplier_reference,
confirmed_by=current_user.user_id
)
if not purchase_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
return PurchaseOrderResponse.from_orm(purchase_order)
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error confirming supplier receipt", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to confirm supplier receipt")
@router.post(route_builder.build_nested_resource_route("purchase-orders", "po_id", "cancel"), response_model=PurchaseOrderResponse)
@require_user_role(['admin', 'owner', 'member'])
async def cancel_purchase_order(
po_id: UUID = Path(..., description="Purchase order ID"),
tenant_id: str = Path(..., description="Tenant ID"),
cancellation_reason: str = Query(..., description="Reason for cancellation"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Cancel a purchase order"""
try:
service = PurchaseOrderService(db)
# Check order exists and belongs to tenant
existing_order = await service.get_purchase_order(po_id)
if not existing_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
if existing_order.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
purchase_order = await service.cancel_purchase_order(
po_id=po_id,
cancellation_reason=cancellation_reason,
cancelled_by=current_user.user_id
)
if not purchase_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
return PurchaseOrderResponse.from_orm(purchase_order)
except HTTPException:
raise
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error cancelling purchase order", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to cancel purchase order")
@router.get(route_builder.build_resource_detail_route("purchase-orders/supplier", "supplier_id"), response_model=List[PurchaseOrderSummary])
async def get_orders_by_supplier(
supplier_id: UUID = Path(..., description="Supplier ID"),
tenant_id: str = Path(..., description="Tenant ID"),
limit: int = Query(20, ge=1, le=100, description="Number of orders to return"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get recent purchase orders for a specific supplier"""
try:
service = PurchaseOrderService(db)
orders = await service.get_orders_by_supplier(
tenant_id=current_user.tenant_id,
supplier_id=supplier_id,
limit=limit
)
return [PurchaseOrderSummary.from_orm(order) for order in orders]
except Exception as e:
logger.error("Error getting orders by supplier", supplier_id=str(supplier_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve orders by supplier")
@router.get(route_builder.build_nested_resource_route("purchase-orders/inventory-products", "inventory_product_id", "history"))
async def get_inventory_product_purchase_history(
inventory_product_id: UUID = Path(..., description="Inventory Product ID"),
tenant_id: str = Path(..., description="Tenant ID"),
days_back: int = Query(90, ge=1, le=365, description="Number of days to look back"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get purchase history for a specific inventory product"""
try:
service = PurchaseOrderService(db)
history = await service.get_inventory_product_purchase_history(
tenant_id=current_user.tenant_id,
inventory_product_id=inventory_product_id,
days_back=days_back
)
return history
except Exception as e:
logger.error("Error getting inventory product purchase history", inventory_product_id=str(inventory_product_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve inventory product purchase history")
@router.get(route_builder.build_operations_route("purchase-orders/inventory-products/top-purchased"))
async def get_top_purchased_inventory_products(
tenant_id: str = Path(..., description="Tenant ID"),
days_back: int = Query(30, ge=1, le=365, description="Number of days to look back"),
limit: int = Query(10, ge=1, le=50, description="Number of top inventory products to return"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get most purchased inventory products by value"""
try:
service = PurchaseOrderService(db)
products = await service.get_top_purchased_inventory_products(
tenant_id=current_user.tenant_id,
days_back=days_back,
limit=limit
)
return products
except Exception as e:
logger.error("Error getting top purchased inventory products", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve top purchased inventory products")

View File

@@ -1,10 +1,10 @@
# services/suppliers/app/api/suppliers.py
"""
Supplier API endpoints
Supplier CRUD API endpoints (ATOMIC)
"""
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Request
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Path
from typing import List, Optional, Dict, Any
from uuid import UUID
import structlog
@@ -13,23 +13,28 @@ 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
SupplierSearchParams
)
from shared.auth.decorators import get_current_user_dep
from typing import Dict, Any
from shared.routing import RouteBuilder
from shared.auth.access_control import require_user_role
router = APIRouter(prefix="/tenants/{tenant_id}/suppliers", tags=["suppliers"])
# Create route builder for consistent URL structure
route_builder = RouteBuilder('suppliers')
router = APIRouter(tags=["suppliers"])
logger = structlog.get_logger()
@router.post("", response_model=SupplierResponse)
@router.post("/", response_model=SupplierResponse)
@router.post(route_builder.build_base_route("suppliers"), response_model=SupplierResponse)
@require_user_role(['admin', 'owner', 'member'])
async def create_supplier(
supplier_data: SupplierCreate,
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Create a new supplier"""
try:
service = SupplierService(db)
supplier = await service.create_supplier(
@@ -45,8 +50,7 @@ async def create_supplier(
raise HTTPException(status_code=500, detail="Failed to create supplier")
@router.get("", response_model=List[SupplierSummary])
@router.get("/", response_model=List[SupplierSummary])
@router.get(route_builder.build_base_route("suppliers"), response_model=List[SupplierSummary])
async def list_suppliers(
tenant_id: str = Path(..., description="Tenant ID"),
search_term: Optional[str] = Query(None, description="Search term"),
@@ -57,8 +61,6 @@ async def list_suppliers(
db: AsyncSession = Depends(get_db)
):
"""List suppliers with optional filters"""
# require_permissions(current_user, ["suppliers:read"])
try:
service = SupplierService(db)
search_params = SupplierSearchParams(
@@ -78,94 +80,20 @@ async def list_suppliers(
raise HTTPException(status_code=500, detail="Failed to retrieve suppliers")
@router.get("/statistics", response_model=SupplierStatistics)
async def get_supplier_statistics(
tenant_id: str = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Get supplier statistics for dashboard"""
# require_permissions(current_user, ["suppliers:read"])
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("/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"""
# require_permissions(current_user, ["suppliers:read"])
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("/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"""
# require_permissions(current_user, ["suppliers:read"])
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("/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"""
# require_permissions(current_user, ["suppliers:read"])
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")
@router.get("/{supplier_id}", response_model=SupplierResponse)
@router.get(route_builder.build_resource_detail_route("suppliers", "supplier_id"), response_model=SupplierResponse)
async def get_supplier(
supplier_id: UUID = Path(..., description="Supplier ID"),
tenant_id: str = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Get supplier by ID"""
# require_permissions(current_user, ["suppliers:read"])
try:
service = SupplierService(db)
supplier = await service.get_supplier(supplier_id)
if not supplier:
raise HTTPException(status_code=404, detail="Supplier not found")
return SupplierResponse.from_orm(supplier)
except HTTPException:
raise
@@ -174,7 +102,8 @@ async def get_supplier(
raise HTTPException(status_code=500, detail="Failed to retrieve supplier")
@router.put("/{supplier_id}", response_model=SupplierResponse)
@router.put(route_builder.build_resource_detail_route("suppliers", "supplier_id"), response_model=SupplierResponse)
@require_user_role(['admin', 'owner', 'member'])
async def update_supplier(
supplier_data: SupplierUpdate,
supplier_id: UUID = Path(..., description="Supplier ID"),
@@ -182,25 +111,23 @@ async def update_supplier(
db: AsyncSession = Depends(get_db)
):
"""Update supplier information"""
# require_permissions(current_user, ["suppliers:update"])
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")
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
@@ -211,103 +138,28 @@ async def update_supplier(
raise HTTPException(status_code=500, detail="Failed to update supplier")
@router.delete("/{supplier_id}")
@router.delete(route_builder.build_resource_detail_route("suppliers", "supplier_id"))
@require_user_role(['admin', 'owner'])
async def delete_supplier(
supplier_id: UUID = Path(..., description="Supplier ID"),
db: AsyncSession = Depends(get_db)
):
"""Delete supplier (soft delete)"""
# require_permissions(current_user, ["suppliers:delete"])
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")
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"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Approve or reject a pending supplier"""
# require_permissions(current_user, ["suppliers:approve"])
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,
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"),
tenant_id: str = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Get suppliers by type"""
# require_permissions(current_user, ["suppliers:read"])
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(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")
raise HTTPException(status_code=500, detail="Failed to delete supplier")