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

829 lines
35 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.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
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")
# ===== 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)
2025-10-27 16:33:26 +01:00
deliveries = await service.get_todays_deliveries(current_user["tenant_id"])
2025-10-06 15:27:01 +02:00
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)
2025-10-27 16:33:26 +01:00
deliveries = await service.get_overdue_deliveries(current_user["tenant_id"])
2025-10-06 15:27:01 +02:00
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(
2025-10-27 16:33:26 +01:00
tenant_id=current_user["tenant_id"],
2025-10-06 15:27:01 +02:00
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")
2025-10-27 16:33:26 +01:00
if existing_delivery.tenant_id != current_user["tenant_id"]:
2025-10-06 15:27:01 +02:00
raise HTTPException(status_code=403, detail="Access denied")
delivery = await service.update_delivery_status(
delivery_id=delivery_id,
status=status_data.status,
2025-10-27 16:33:26 +01:00
updated_by=current_user["user_id"],
2025-10-06 15:27:01 +02:00
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")
2025-10-27 16:33:26 +01:00
if existing_delivery.tenant_id != current_user["tenant_id"]:
2025-10-06 15:27:01 +02:00
raise HTTPException(status_code=403, detail="Access denied")
delivery = await service.mark_as_received(
delivery_id=delivery_id,
2025-10-27 16:33:26 +01:00
received_by=current_user["user_id"],
2025-10-06 15:27:01 +02:00
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)
2025-10-27 16:33:26 +01:00
if deliveries and deliveries[0].tenant_id != current_user["tenant_id"]:
2025-10-06 15:27:01 +02:00
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)
2025-10-27 16:33:26 +01:00
stats = await service.get_purchase_order_statistics(current_user["tenant_id"])
2025-10-06 15:27:01 +02:00
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)
2025-10-27 16:33:26 +01:00
orders = await service.get_orders_requiring_approval(current_user["tenant_id"])
2025-10-06 15:27:01 +02:00
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)
2025-10-27 16:33:26 +01:00
orders = await service.get_overdue_orders(current_user["tenant_id"])
2025-10-06 15:27:01 +02:00
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")
2025-10-27 16:33:26 +01:00
if existing_order.tenant_id != current_user["tenant_id"]:
2025-10-06 15:27:01 +02:00
raise HTTPException(status_code=403, detail="Access denied")
purchase_order = await service.update_order_status(
po_id=po_id,
status=status_data.status,
2025-10-27 16:33:26 +01:00
updated_by=current_user["user_id"],
2025-10-06 15:27:01 +02:00
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'])
2025-10-06 15:27:01 +02:00
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 (Admin+ only)"""
2025-10-06 15:27:01 +02:00
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")
2025-10-27 16:33:26 +01:00
if existing_order.tenant_id != current_user["tenant_id"]:
2025-10-06 15:27:01 +02:00
raise HTTPException(status_code=403, detail="Access denied")
# Capture PO details for audit
po_details = {
"po_number": existing_order.order_number,
"supplier_id": str(existing_order.supplier_id),
"total_amount": float(existing_order.total_amount) if existing_order.total_amount else 0.0,
"expected_delivery_date": existing_order.expected_delivery_date.isoformat() if existing_order.expected_delivery_date else None
}
2025-10-06 15:27:01 +02:00
if approval_data.action == "approve":
purchase_order = await service.approve_purchase_order(
po_id=po_id,
2025-10-27 16:33:26 +01:00
approved_by=current_user["user_id"],
2025-10-06 15:27:01 +02:00
approval_notes=approval_data.notes
)
action = "approve"
description = f"Admin {current_user.get('email', 'unknown')} approved purchase order {po_details['po_number']}"
2025-10-06 15:27:01 +02:00
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,
2025-10-27 16:33:26 +01:00
rejected_by=current_user["user_id"]
2025-10-06 15:27:01 +02:00
)
action = "reject"
description = f"Admin {current_user.get('email', 'unknown')} rejected purchase order {po_details['po_number']}"
2025-10-06 15:27:01 +02:00
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"
)
# Log HIGH severity audit event for purchase order approval/rejection
try:
await audit_logger.log_event(
db_session=db,
tenant_id=tenant_id,
user_id=current_user["user_id"],
action=action,
resource_type="purchase_order",
resource_id=str(po_id),
severity=AuditSeverity.HIGH.value,
description=description,
changes={
"action": approval_data.action,
"notes": approval_data.notes,
"po_details": po_details
},
endpoint=f"/purchase-orders/{po_id}/approve",
method="POST"
)
except Exception as audit_error:
logger.warning("Failed to log audit event", error=str(audit_error))
logger.info("Purchase order approval processed",
po_id=str(po_id),
action=approval_data.action,
tenant_id=tenant_id,
user_id=current_user["user_id"])
2025-10-06 15:27:01 +02:00
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")
2025-10-27 16:33:26 +01:00
if existing_order.tenant_id != current_user["tenant_id"]:
2025-10-06 15:27:01 +02:00
raise HTTPException(status_code=403, detail="Access denied")
purchase_order = await service.send_to_supplier(
po_id=po_id,
2025-10-27 16:33:26 +01:00
sent_by=current_user["user_id"],
2025-10-06 15:27:01 +02:00
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")
2025-10-27 16:33:26 +01:00
if existing_order.tenant_id != current_user["tenant_id"]:
2025-10-06 15:27:01 +02:00
raise HTTPException(status_code=403, detail="Access denied")
purchase_order = await service.confirm_supplier_receipt(
po_id=po_id,
supplier_reference=supplier_reference,
2025-10-27 16:33:26 +01:00
confirmed_by=current_user["user_id"]
2025-10-06 15:27:01 +02:00
)
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")
2025-10-27 16:33:26 +01:00
if existing_order.tenant_id != current_user["tenant_id"]:
2025-10-06 15:27:01 +02:00
raise HTTPException(status_code=403, detail="Access denied")
purchase_order = await service.cancel_purchase_order(
po_id=po_id,
cancellation_reason=cancellation_reason,
2025-10-27 16:33:26 +01:00
cancelled_by=current_user["user_id"]
2025-10-06 15:27:01 +02:00
)
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(
2025-10-27 16:33:26 +01:00
tenant_id=current_user["tenant_id"],
2025-10-06 15:27:01 +02:00
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(
2025-10-27 16:33:26 +01:00
tenant_id=current_user["tenant_id"],
2025-10-06 15:27:01 +02:00
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(
2025-10-27 16:33:26 +01:00
tenant_id=current_user["tenant_id"],
2025-10-06 15:27:01 +02:00
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")
@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)}")