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

273 lines
11 KiB
Python
Raw Normal View History

# services/suppliers/app/api/purchase_orders.py
"""
2025-10-06 15:27:01 +02:00
Purchase Order CRUD API endpoints (ATOMIC)
"""
from fastapi import APIRouter, Depends, HTTPException, Query, Path
2025-10-06 15:27:01 +02:00
from typing import List, Optional, Dict, Any
from uuid import UUID
import structlog
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.services.purchase_order_service import PurchaseOrderService
from app.schemas.suppliers import (
PurchaseOrderCreate, PurchaseOrderUpdate, PurchaseOrderResponse, PurchaseOrderSummary,
2025-10-06 15:27:01 +02:00
PurchaseOrderSearchParams
)
from app.models.suppliers import PurchaseOrderStatus
2025-10-29 06:58:05 +01:00
from app.models import AuditLog
2025-08-15 22:40:19 +02:00
from shared.auth.decorators import get_current_user_dep
2025-10-06 15:27:01 +02:00
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=["purchase-orders"])
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
@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,
2025-08-15 22:40:19 +02:00
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Create a new purchase order"""
2025-08-15 22:40:19 +02:00
# require_permissions(current_user, ["purchase_orders:create"])
try:
service = PurchaseOrderService(db)
purchase_order = await service.create_purchase_order(
2025-10-21 19:50:07 +02:00
tenant_id=current_user["tenant_id"],
po_data=po_data,
2025-10-21 19:50:07 +02:00
created_by=current_user["user_id"]
)
return PurchaseOrderResponse.from_orm(purchase_order)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error creating purchase order", error=str(e))
raise HTTPException(status_code=500, detail="Failed to create purchase order")
2025-10-06 15:27:01 +02:00
@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"),
priority: Optional[str] = Query(None, description="Filter by priority"),
date_from: Optional[str] = Query(None, description="Filter from date (YYYY-MM-DD)"),
date_to: Optional[str] = Query(None, description="Filter to date (YYYY-MM-DD)"),
search_term: Optional[str] = Query(None, description="Search term"),
limit: int = Query(50, ge=1, le=1000, description="Number of results to return"),
offset: int = Query(0, ge=0, description="Number of results to skip"),
2025-08-15 22:40:19 +02:00
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""List purchase orders with optional filters"""
2025-08-15 22:40:19 +02:00
# require_permissions(current_user, ["purchase_orders:read"])
try:
from datetime import datetime
# Parse date filters
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")
# Validate status
status_enum = None
if status:
try:
2025-10-21 19:50:07 +02:00
# Convert from PENDING_APPROVAL to pending_approval format
status_value = status.lower()
status_enum = PurchaseOrderStatus(status_value)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid status")
service = PurchaseOrderService(db)
search_params = PurchaseOrderSearchParams(
supplier_id=supplier_id,
status=status_enum,
priority=priority,
date_from=date_from_parsed,
date_to=date_to_parsed,
search_term=search_term,
limit=limit,
offset=offset
)
orders = await service.search_purchase_orders(
2025-10-21 19:50:07 +02:00
tenant_id=current_user["tenant_id"],
search_params=search_params
)
2025-10-21 19:50:07 +02:00
# Convert to response with supplier names
response = []
for order in orders:
order_dict = {
"id": order.id,
"po_number": order.po_number,
"supplier_id": order.supplier_id,
"supplier_name": order.supplier.name if order.supplier else None,
"status": order.status,
"priority": order.priority,
"order_date": order.order_date,
"required_delivery_date": order.required_delivery_date,
"total_amount": order.total_amount,
"currency": order.currency,
"created_at": order.created_at
}
response.append(PurchaseOrderSummary(**order_dict))
return response
except HTTPException:
raise
except Exception as e:
logger.error("Error listing purchase orders", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve purchase orders")
2025-10-06 15:27:01 +02:00
@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"),
2025-08-15 22:40:19 +02:00
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Get purchase order by ID with items"""
2025-08-15 22:40:19 +02:00
# require_permissions(current_user, ["purchase_orders:read"])
try:
service = PurchaseOrderService(db)
purchase_order = await service.get_purchase_order(po_id)
2025-10-21 19:50:07 +02:00
if not purchase_order:
raise HTTPException(status_code=404, detail="Purchase order not found")
2025-10-21 19:50:07 +02:00
# Tenant access control is handled by the gateway
return PurchaseOrderResponse.from_orm(purchase_order)
except HTTPException:
raise
except Exception as e:
logger.error("Error getting purchase order", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve purchase order")
2025-10-06 15:27:01 +02:00
@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"),
2025-08-15 22:40:19 +02:00
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: Session = Depends(get_db)
):
"""Update purchase order information"""
2025-08-15 22:40:19 +02:00
# 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")
2025-10-21 19:50:07 +02:00
if existing_order.tenant_id != current_user["tenant_id"]:
raise HTTPException(status_code=403, detail="Access denied")
purchase_order = await service.update_purchase_order(
po_id=po_id,
po_data=po_data,
2025-10-21 19:50:07 +02:00
updated_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 updating purchase order", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to update purchase order")
@router.delete(route_builder.build_resource_detail_route("purchase-orders", "po_id"))
@require_user_role(['admin', 'owner'])
async def delete_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)
):
"""Delete purchase order (soft delete, Admin+ only)"""
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-21 19:50:07 +02:00
if existing_order.tenant_id != current_user["tenant_id"]:
raise HTTPException(status_code=403, detail="Access denied")
# Capture PO data before deletion
po_data = {
"po_number": existing_order.order_number,
"supplier_id": str(existing_order.supplier_id),
"status": existing_order.status.value if existing_order.status else None,
"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
}
# Delete purchase order (likely soft delete in service)
success = await service.delete_purchase_order(po_id)
if not success:
raise HTTPException(status_code=404, detail="Purchase order not found")
# Log audit event for purchase order deletion
try:
await audit_logger.log_deletion(
db_session=db,
tenant_id=tenant_id,
user_id=current_user["user_id"],
resource_type="purchase_order",
resource_id=str(po_id),
resource_data=po_data,
description=f"Admin {current_user.get('email', 'unknown')} deleted purchase order {po_data['po_number']}",
endpoint=f"/purchase-orders/{po_id}",
method="DELETE"
)
except Exception as audit_error:
logger.warning("Failed to log audit event", error=str(audit_error))
logger.info("Deleted purchase order",
po_id=str(po_id),
tenant_id=tenant_id,
user_id=current_user["user_id"])
return {"message": "Purchase order deleted successfully"}
except HTTPException:
raise
except Exception as e:
logger.error("Error deleting purchase order", po_id=str(po_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to delete purchase order")