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

@@ -0,0 +1,262 @@
# ================================================================
# services/orders/app/api/customers.py
# ================================================================
"""
Customers API endpoints - ATOMIC CRUD operations
"""
from typing import List
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
import structlog
from shared.auth.decorators import get_current_user_dep
from shared.auth.access_control import require_user_role
from shared.routing import RouteBuilder
from app.core.database import get_db
from app.services.orders_service import OrdersService
from app.schemas.order_schemas import (
CustomerCreate,
CustomerUpdate,
CustomerResponse
)
logger = structlog.get_logger()
# Create route builder for consistent URL structure
route_builder = RouteBuilder('orders')
router = APIRouter()
# ===== Dependency Injection =====
async def get_orders_service(db = Depends(get_db)) -> OrdersService:
"""Get orders service with dependencies"""
from app.repositories.order_repository import (
OrderRepository,
CustomerRepository,
OrderItemRepository,
OrderStatusHistoryRepository
)
from shared.clients import (
get_inventory_client,
get_production_client,
get_sales_client
)
return OrdersService(
order_repo=OrderRepository(),
customer_repo=CustomerRepository(),
order_item_repo=OrderItemRepository(),
status_history_repo=OrderStatusHistoryRepository(),
inventory_client=get_inventory_client(),
production_client=get_production_client(),
sales_client=get_sales_client()
)
# ===== Customer CRUD Endpoints =====
@router.post(
route_builder.build_base_route("customers"),
response_model=CustomerResponse,
status_code=status.HTTP_201_CREATED
)
@require_user_role(['admin', 'owner', 'member'])
async def create_customer(
customer_data: CustomerCreate,
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Create a new customer"""
try:
# Ensure tenant_id matches
customer_data.tenant_id = tenant_id
# Check if customer code already exists
existing_customer = await orders_service.customer_repo.get_by_customer_code(
db, customer_data.customer_code, tenant_id
)
if existing_customer:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Customer code already exists"
)
customer = await orders_service.customer_repo.create(
db,
obj_in=customer_data.dict(),
created_by=UUID(current_user["sub"])
)
logger.info("Customer created successfully",
customer_id=str(customer.id),
customer_code=customer.customer_code)
return CustomerResponse.from_orm(customer)
except HTTPException:
raise
except Exception as e:
logger.error("Error creating customer", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create customer"
)
@router.get(
route_builder.build_resource_detail_route("customers", "customer_id"),
response_model=CustomerResponse
)
async def get_customer(
tenant_id: UUID = Path(...),
customer_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get customer details by ID"""
try:
customer = await orders_service.customer_repo.get(db, customer_id, tenant_id)
if not customer:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Customer not found"
)
return CustomerResponse.from_orm(customer)
except HTTPException:
raise
except Exception as e:
logger.error("Error getting customer",
customer_id=str(customer_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve customer"
)
@router.get(
route_builder.build_base_route("customers"),
response_model=List[CustomerResponse]
)
async def get_customers(
tenant_id: UUID = Path(...),
active_only: bool = Query(True, description="Filter for active customers only"),
skip: int = Query(0, ge=0, description="Number of customers to skip"),
limit: int = Query(100, ge=1, le=1000, description="Number of customers to return"),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get customers with filtering and pagination"""
try:
if active_only:
customers = await orders_service.customer_repo.get_active_customers(
db, tenant_id, skip, limit
)
else:
customers = await orders_service.customer_repo.get_multi(
db, tenant_id, skip, limit, order_by="name"
)
return [CustomerResponse.from_orm(customer) for customer in customers]
except Exception as e:
logger.error("Error getting customers", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve customers"
)
@router.put(
route_builder.build_resource_detail_route("customers", "customer_id"),
response_model=CustomerResponse
)
@require_user_role(['admin', 'owner', 'member'])
async def update_customer(
customer_data: CustomerUpdate,
tenant_id: UUID = Path(...),
customer_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Update customer information"""
try:
# Get existing customer
customer = await orders_service.customer_repo.get(db, customer_id, tenant_id)
if not customer:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Customer not found"
)
# Update customer
updated_customer = await orders_service.customer_repo.update(
db,
db_obj=customer,
obj_in=customer_data.dict(exclude_unset=True),
updated_by=UUID(current_user["sub"])
)
logger.info("Customer updated successfully",
customer_id=str(customer_id))
return CustomerResponse.from_orm(updated_customer)
except HTTPException:
raise
except Exception as e:
logger.error("Error updating customer",
customer_id=str(customer_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update customer"
)
@router.delete(
route_builder.build_resource_detail_route("customers", "customer_id"),
status_code=status.HTTP_204_NO_CONTENT
)
@require_user_role(['admin', 'owner'])
async def delete_customer(
tenant_id: UUID = Path(...),
customer_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Delete a customer (soft delete)"""
try:
customer = await orders_service.customer_repo.get(db, customer_id, tenant_id)
if not customer:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Customer not found"
)
await orders_service.customer_repo.delete(db, customer_id, tenant_id)
logger.info("Customer deleted successfully",
customer_id=str(customer_id))
except HTTPException:
raise
except Exception as e:
logger.error("Error deleting customer",
customer_id=str(customer_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete customer"
)

View File

@@ -0,0 +1,237 @@
# ================================================================
# services/orders/app/api/order_operations.py
# ================================================================
"""
Order Operations API endpoints - BUSINESS logic operations
Includes status updates, demand calculation, dashboard, and business intelligence
"""
from datetime import date, datetime
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
import structlog
from shared.auth.decorators import get_current_user_dep
from shared.routing import RouteBuilder
from app.core.database import get_db
from app.services.orders_service import OrdersService
from app.schemas.order_schemas import (
OrderResponse,
OrdersDashboardSummary,
DemandRequirements
)
logger = structlog.get_logger()
# Create route builder for consistent URL structure
route_builder = RouteBuilder('orders')
router = APIRouter()
# ===== Dependency Injection =====
async def get_orders_service(db = Depends(get_db)) -> OrdersService:
"""Get orders service with dependencies"""
from app.repositories.order_repository import (
OrderRepository,
CustomerRepository,
OrderItemRepository,
OrderStatusHistoryRepository
)
from shared.clients import (
get_inventory_client,
get_production_client,
get_sales_client
)
return OrdersService(
order_repo=OrderRepository(),
customer_repo=CustomerRepository(),
order_item_repo=OrderItemRepository(),
status_history_repo=OrderStatusHistoryRepository(),
inventory_client=get_inventory_client(),
production_client=get_production_client(),
sales_client=get_sales_client()
)
# ===== Dashboard and Analytics Endpoints =====
@router.get(
route_builder.build_base_route("dashboard-summary"),
response_model=OrdersDashboardSummary
)
async def get_dashboard_summary(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get comprehensive dashboard summary for orders"""
try:
summary = await orders_service.get_dashboard_summary(db, tenant_id)
logger.info("Dashboard summary retrieved",
tenant_id=str(tenant_id),
total_orders=summary.total_orders_today)
return summary
except Exception as e:
logger.error("Error getting dashboard summary",
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve dashboard summary"
)
@router.get(
route_builder.build_base_route("demand-requirements"),
response_model=DemandRequirements
)
async def get_demand_requirements(
tenant_id: UUID = Path(...),
target_date: date = Query(..., description="Date for demand analysis"),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get demand requirements for production planning"""
try:
requirements = await orders_service.get_demand_requirements(db, tenant_id, target_date)
logger.info("Demand requirements calculated",
tenant_id=str(tenant_id),
target_date=str(target_date),
total_orders=requirements.total_orders)
return requirements
except Exception as e:
logger.error("Error getting demand requirements",
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to calculate demand requirements"
)
# ===== Order Status Management =====
@router.put(
route_builder.build_base_route("{order_id}/status"),
response_model=OrderResponse
)
async def update_order_status(
new_status: str,
tenant_id: UUID = Path(...),
order_id: UUID = Path(...),
reason: Optional[str] = Query(None, description="Reason for status change"),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Update order status with validation and history tracking"""
try:
# Validate status
valid_statuses = [
"pending", "confirmed", "in_production", "ready",
"out_for_delivery", "delivered", "cancelled", "failed"
]
if new_status not in valid_statuses:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid status. Must be one of: {', '.join(valid_statuses)}"
)
order = await orders_service.update_order_status(
db,
order_id,
tenant_id,
new_status,
user_id=UUID(current_user["sub"]),
reason=reason
)
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found"
)
logger.info("Order status updated",
order_id=str(order_id),
new_status=new_status)
return order
except HTTPException:
raise
except Exception as e:
logger.error("Error updating order status",
order_id=str(order_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update order status"
)
# ===== Business Intelligence Endpoints =====
@router.get(
route_builder.build_base_route("business-model")
)
async def detect_business_model(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Detect business model based on order patterns"""
try:
business_model = await orders_service.detect_business_model(db, tenant_id)
return {
"business_model": business_model,
"confidence": "high" if business_model else "unknown",
"detected_at": datetime.now().isoformat()
}
except Exception as e:
logger.error("Error detecting business model", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to detect business model"
)
# ===== Health and Status Endpoints =====
@router.get(
route_builder.build_base_route("status")
)
async def get_service_status(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep)
):
"""Get orders service status"""
try:
return {
"service": "orders-service",
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"tenant_id": str(tenant_id)
}
except Exception as e:
logger.error("Error getting service status", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get service status"
)

View File

@@ -2,33 +2,31 @@
# services/orders/app/api/orders.py
# ================================================================
"""
Orders API endpoints for Orders Service
Orders API endpoints - ATOMIC CRUD operations only
"""
from datetime import date, datetime
from datetime import date
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
from fastapi.responses import JSONResponse
import structlog
from shared.auth.decorators import get_current_user_dep
from shared.auth.access_control import require_user_role
from shared.routing import RouteBuilder
from app.core.database import get_db
from app.services.orders_service import OrdersService
from app.schemas.order_schemas import (
OrderCreate,
OrderUpdate,
OrderResponse,
CustomerCreate,
CustomerUpdate,
CustomerResponse,
OrdersDashboardSummary,
DemandRequirements,
ProcurementPlanningData
OrderResponse
)
logger = structlog.get_logger()
# Create route builder for consistent URL structure
route_builder = RouteBuilder('orders')
router = APIRouter()
@@ -47,7 +45,7 @@ async def get_orders_service(db = Depends(get_db)) -> OrdersService:
get_production_client,
get_sales_client
)
return OrdersService(
order_repo=OrderRepository(),
customer_repo=CustomerRepository(),
@@ -59,67 +57,14 @@ async def get_orders_service(db = Depends(get_db)) -> OrdersService:
)
# ===== Dashboard and Analytics Endpoints =====
# ===== Order CRUD Endpoints =====
@router.get("/tenants/{tenant_id}/orders/dashboard-summary", response_model=OrdersDashboardSummary)
async def get_dashboard_summary(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get comprehensive dashboard summary for orders"""
try:
summary = await orders_service.get_dashboard_summary(db, tenant_id)
logger.info("Dashboard summary retrieved",
tenant_id=str(tenant_id),
total_orders=summary.total_orders_today)
return summary
except Exception as e:
logger.error("Error getting dashboard summary",
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve dashboard summary"
)
@router.get("/tenants/{tenant_id}/orders/demand-requirements", response_model=DemandRequirements)
async def get_demand_requirements(
tenant_id: UUID = Path(...),
target_date: date = Query(..., description="Date for demand analysis"),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get demand requirements for production planning"""
try:
requirements = await orders_service.get_demand_requirements(db, tenant_id, target_date)
logger.info("Demand requirements calculated",
tenant_id=str(tenant_id),
target_date=str(target_date),
total_orders=requirements.total_orders)
return requirements
except Exception as e:
logger.error("Error getting demand requirements",
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to calculate demand requirements"
)
# ===== Order Management Endpoints =====
@router.post("/tenants/{tenant_id}/orders", response_model=OrderResponse, status_code=status.HTTP_201_CREATED)
@router.post(
route_builder.build_base_route("orders"),
response_model=OrderResponse,
status_code=status.HTTP_201_CREATED
)
@require_user_role(['admin', 'owner', 'member'])
async def create_order(
order_data: OrderCreate,
tenant_id: UUID = Path(...),
@@ -158,7 +103,8 @@ async def create_order(
)
@router.get("/tenants/{tenant_id}/orders/{order_id}", response_model=OrderResponse)
@router.get(
route_builder.build_base_route("{order_id}"), response_model=OrderResponse)
async def get_order(
tenant_id: UUID = Path(...),
order_id: UUID = Path(...),
@@ -189,7 +135,10 @@ async def get_order(
)
@router.get("/tenants/{tenant_id}/orders", response_model=List[OrderResponse])
@router.get(
route_builder.build_base_route("orders"),
response_model=List[OrderResponse]
)
async def get_orders(
tenant_id: UUID = Path(...),
status_filter: Optional[str] = Query(None, description="Filter by order status"),
@@ -216,9 +165,9 @@ async def get_orders(
orders = await orders_service.order_repo.get_multi(
db, tenant_id, skip, limit, order_by="order_date", order_desc=True
)
return [OrderResponse.from_orm(order) for order in orders]
except Exception as e:
logger.error("Error getting orders", error=str(e))
raise HTTPException(
@@ -227,214 +176,87 @@ async def get_orders(
)
@router.put("/tenants/{tenant_id}/orders/{order_id}/status", response_model=OrderResponse)
async def update_order_status(
new_status: str,
@router.put(
route_builder.build_base_route("{order_id}"),
response_model=OrderResponse
)
@require_user_role(['admin', 'owner', 'member'])
async def update_order(
order_data: OrderUpdate,
tenant_id: UUID = Path(...),
order_id: UUID = Path(...),
reason: Optional[str] = Query(None, description="Reason for status change"),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Update order status"""
"""Update order information"""
try:
# Validate status
valid_statuses = ["pending", "confirmed", "in_production", "ready", "out_for_delivery", "delivered", "cancelled", "failed"]
if new_status not in valid_statuses:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid status. Must be one of: {', '.join(valid_statuses)}"
)
order = await orders_service.update_order_status(
db,
order_id,
tenant_id,
new_status,
user_id=UUID(current_user["sub"]),
reason=reason
)
# Get existing order
order = await orders_service.order_repo.get(db, order_id, tenant_id)
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found"
)
logger.info("Order status updated",
order_id=str(order_id),
new_status=new_status)
return order
# Update order
updated_order = await orders_service.order_repo.update(
db,
db_obj=order,
obj_in=order_data.dict(exclude_unset=True),
updated_by=UUID(current_user["sub"])
)
logger.info("Order updated successfully",
order_id=str(order_id))
return OrderResponse.from_orm(updated_order)
except HTTPException:
raise
except Exception as e:
logger.error("Error updating order status",
order_id=str(order_id),
logger.error("Error updating order",
order_id=str(order_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update order status"
detail="Failed to update order"
)
# ===== Customer Management Endpoints =====
@router.post("/tenants/{tenant_id}/customers", response_model=CustomerResponse, status_code=status.HTTP_201_CREATED)
async def create_customer(
customer_data: CustomerCreate,
@router.delete(
route_builder.build_base_route("{order_id}"),
status_code=status.HTTP_204_NO_CONTENT
)
@require_user_role(['admin', 'owner'])
async def delete_order(
tenant_id: UUID = Path(...),
order_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Create a new customer"""
"""Delete an order (soft delete)"""
try:
# Ensure tenant_id matches
customer_data.tenant_id = tenant_id
# Check if customer code already exists
existing_customer = await orders_service.customer_repo.get_by_customer_code(
db, customer_data.customer_code, tenant_id
)
if existing_customer:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Customer code already exists"
)
customer = await orders_service.customer_repo.create(
db,
obj_in=customer_data.dict(),
created_by=UUID(current_user["sub"])
)
logger.info("Customer created successfully",
customer_id=str(customer.id),
customer_code=customer.customer_code)
return CustomerResponse.from_orm(customer)
except HTTPException:
raise
except Exception as e:
logger.error("Error creating customer", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create customer"
)
@router.get("/tenants/{tenant_id}/customers", response_model=List[CustomerResponse])
async def get_customers(
tenant_id: UUID = Path(...),
active_only: bool = Query(True, description="Filter for active customers only"),
skip: int = Query(0, ge=0, description="Number of customers to skip"),
limit: int = Query(100, ge=1, le=1000, description="Number of customers to return"),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get customers with filtering and pagination"""
try:
if active_only:
customers = await orders_service.customer_repo.get_active_customers(
db, tenant_id, skip, limit
)
else:
customers = await orders_service.customer_repo.get_multi(
db, tenant_id, skip, limit, order_by="name"
)
return [CustomerResponse.from_orm(customer) for customer in customers]
except Exception as e:
logger.error("Error getting customers", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve customers"
)
@router.get("/tenants/{tenant_id}/customers/{customer_id}", response_model=CustomerResponse)
async def get_customer(
tenant_id: UUID = Path(...),
customer_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Get customer details"""
try:
customer = await orders_service.customer_repo.get(db, customer_id, tenant_id)
if not customer:
order = await orders_service.order_repo.get(db, order_id, tenant_id)
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Customer not found"
detail="Order not found"
)
return CustomerResponse.from_orm(customer)
await orders_service.order_repo.delete(db, order_id, tenant_id)
logger.info("Order deleted successfully",
order_id=str(order_id))
except HTTPException:
raise
except Exception as e:
logger.error("Error getting customer",
customer_id=str(customer_id),
logger.error("Error deleting order",
order_id=str(order_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve customer"
)
# ===== Business Intelligence Endpoints =====
@router.get("/tenants/{tenant_id}/orders/business-model")
async def detect_business_model(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
orders_service: OrdersService = Depends(get_orders_service),
db = Depends(get_db)
):
"""Detect business model based on order patterns"""
try:
business_model = await orders_service.detect_business_model(db, tenant_id)
return {
"business_model": business_model,
"confidence": "high" if business_model else "unknown",
"detected_at": datetime.now().isoformat()
}
except Exception as e:
logger.error("Error detecting business model", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to detect business model"
)
# ===== Health and Status Endpoints =====
@router.get("/tenants/{tenant_id}/orders/status")
async def get_service_status(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep)
):
"""Get orders service status"""
try:
return {
"service": "orders-service",
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"tenant_id": str(tenant_id)
}
except Exception as e:
logger.error("Error getting service status", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get service status"
detail="Failed to delete order"
)

View File

@@ -1,8 +1,9 @@
# ================================================================
# services/orders/app/api/procurement.py
# services/orders/app/api/procurement_operations.py
# ================================================================
"""
Procurement API Endpoints - RESTful APIs for procurement planning
Procurement Operations API Endpoints - BUSINESS logic for procurement planning
RESTful APIs for procurement planning, approval workflows, and PO management
"""
import uuid
@@ -29,9 +30,22 @@ from shared.clients.inventory_client import InventoryServiceClient
from shared.clients.forecast_client import ForecastServiceClient
from shared.config.base import BaseServiceSettings
from shared.monitoring.decorators import monitor_performance
from shared.routing import RouteBuilder
from shared.auth.access_control import (
require_user_role,
admin_role_required,
owner_role_required,
require_subscription_tier,
analytics_tier_required,
enterprise_tier_required
)
# Create router - tenant-scoped
router = APIRouter(prefix="/tenants/{tenant_id}", tags=["Procurement Planning"])
# Create route builder for consistent URL structure
route_builder = RouteBuilder('orders')
# Create router
router = APIRouter(tags=["Procurement Planning"])
# Create service settings
service_settings = BaseServiceSettings()
@@ -88,7 +102,11 @@ async def get_procurement_service(db: AsyncSession = Depends(get_db)) -> Procure
# PROCUREMENT PLAN ENDPOINTS
# ================================================================
@router.get("/procurement/plans/current", response_model=Optional[ProcurementPlanResponse])
@router.get(
route_builder.build_operations_route("procurement/plans/current"),
response_model=Optional[ProcurementPlanResponse]
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
@monitor_performance("get_current_procurement_plan")
async def get_current_procurement_plan(
tenant_id: uuid.UUID,
@@ -110,7 +128,11 @@ async def get_current_procurement_plan(
)
@router.get("/procurement/plans/date/{plan_date}", response_model=Optional[ProcurementPlanResponse])
@router.get(
route_builder.build_operations_route("procurement/plans/date/{plan_date}"),
response_model=Optional[ProcurementPlanResponse]
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
@monitor_performance("get_procurement_plan_by_date")
async def get_procurement_plan_by_date(
tenant_id: uuid.UUID,
@@ -133,7 +155,11 @@ async def get_procurement_plan_by_date(
)
@router.get("/procurement/plans", response_model=PaginatedProcurementPlans)
@router.get(
route_builder.build_operations_route("procurement/plans"),
response_model=PaginatedProcurementPlans
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
@monitor_performance("list_procurement_plans")
async def list_procurement_plans(
tenant_id: uuid.UUID,
@@ -191,7 +217,11 @@ async def list_procurement_plans(
)
@router.post("/procurement/plans/generate", response_model=GeneratePlanResponse)
@router.post(
route_builder.build_operations_route("procurement/plans/generate"),
response_model=GeneratePlanResponse
)
@require_user_role(['member', 'admin', 'owner'])
@monitor_performance("generate_procurement_plan")
async def generate_procurement_plan(
tenant_id: uuid.UUID,
@@ -233,7 +263,10 @@ async def generate_procurement_plan(
)
@router.put("/procurement/plans/{plan_id}/status")
@router.put(
route_builder.build_operations_route("procurement/plans/{plan_id}/status")
)
@require_user_role(['admin', 'owner'])
@monitor_performance("update_procurement_plan_status")
async def update_procurement_plan_status(
tenant_id: uuid.UUID,
@@ -272,7 +305,11 @@ async def update_procurement_plan_status(
)
@router.get("/procurement/plans/id/{plan_id}", response_model=Optional[ProcurementPlanResponse])
@router.get(
route_builder.build_operations_route("procurement/plans/id/{plan_id}"),
response_model=Optional[ProcurementPlanResponse]
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
@monitor_performance("get_procurement_plan_by_id")
async def get_procurement_plan_by_id(
tenant_id: uuid.UUID,
@@ -309,7 +346,11 @@ async def get_procurement_plan_by_id(
# DASHBOARD ENDPOINTS
# ================================================================
@router.get("/procurement/dashboard", response_model=Optional[DashboardData])
@router.get(
route_builder.build_dashboard_route("procurement"),
response_model=Optional[DashboardData]
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
@monitor_performance("get_procurement_dashboard")
async def get_procurement_dashboard(
tenant_id: uuid.UUID,
@@ -341,7 +382,10 @@ async def get_procurement_dashboard(
# REQUIREMENT MANAGEMENT ENDPOINTS
# ================================================================
@router.get("/procurement/plans/{plan_id}/requirements")
@router.get(
route_builder.build_operations_route("procurement/plans/{plan_id}/requirements")
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
@monitor_performance("get_plan_requirements")
async def get_plan_requirements(
tenant_id: uuid.UUID,
@@ -385,7 +429,10 @@ async def get_plan_requirements(
)
@router.get("/procurement/requirements/critical")
@router.get(
route_builder.build_operations_route("procurement/requirements/critical")
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
@monitor_performance("get_critical_requirements")
async def get_critical_requirements(
tenant_id: uuid.UUID,
@@ -413,7 +460,11 @@ async def get_critical_requirements(
# NEW FEATURE ENDPOINTS
# ================================================================
@router.post("/procurement/plans/{plan_id}/recalculate", response_model=GeneratePlanResponse)
@router.post(
route_builder.build_operations_route("procurement/plans/{plan_id}/recalculate"),
response_model=GeneratePlanResponse
)
@require_user_role(['member', 'admin', 'owner'])
@monitor_performance("recalculate_procurement_plan")
async def recalculate_procurement_plan(
tenant_id: uuid.UUID,
@@ -451,7 +502,10 @@ async def recalculate_procurement_plan(
)
@router.post("/procurement/requirements/{requirement_id}/link-purchase-order")
@router.post(
route_builder.build_operations_route("procurement/requirements/{requirement_id}/link-purchase-order")
)
@require_user_role(['member', 'admin', 'owner'])
@monitor_performance("link_requirement_to_po")
async def link_requirement_to_purchase_order(
tenant_id: uuid.UUID,
@@ -506,7 +560,10 @@ async def link_requirement_to_purchase_order(
)
@router.put("/procurement/requirements/{requirement_id}/delivery-status")
@router.put(
route_builder.build_operations_route("procurement/requirements/{requirement_id}/delivery-status")
)
@require_user_role(['member', 'admin', 'owner'])
@monitor_performance("update_delivery_status")
async def update_requirement_delivery_status(
tenant_id: uuid.UUID,
@@ -561,7 +618,10 @@ async def update_requirement_delivery_status(
)
@router.post("/procurement/plans/{plan_id}/approve")
@router.post(
route_builder.build_operations_route("procurement/plans/{plan_id}/approve")
)
@require_user_role(['admin', 'owner'])
@monitor_performance("approve_procurement_plan")
async def approve_procurement_plan(
tenant_id: uuid.UUID,
@@ -614,7 +674,10 @@ async def approve_procurement_plan(
)
@router.post("/procurement/plans/{plan_id}/reject")
@router.post(
route_builder.build_operations_route("procurement/plans/{plan_id}/reject")
)
@require_user_role(['admin', 'owner'])
@monitor_performance("reject_procurement_plan")
async def reject_procurement_plan(
tenant_id: uuid.UUID,
@@ -667,7 +730,10 @@ async def reject_procurement_plan(
)
@router.post("/procurement/plans/{plan_id}/create-purchase-orders")
@router.post(
route_builder.build_operations_route("procurement/plans/{plan_id}/create-purchase-orders")
)
@require_user_role(['admin', 'owner'])
@monitor_performance("create_pos_from_plan")
async def create_purchase_orders_from_plan(
tenant_id: uuid.UUID,
@@ -714,7 +780,9 @@ async def create_purchase_orders_from_plan(
# UTILITY ENDPOINTS
# ================================================================
@router.post("/procurement/scheduler/trigger")
@router.post(
route_builder.build_operations_route("procurement/scheduler/trigger")
)
@monitor_performance("trigger_daily_scheduler")
async def trigger_daily_scheduler(
tenant_id: uuid.UUID,
@@ -753,7 +821,9 @@ async def trigger_daily_scheduler(
@router.get("/procurement/health")
@router.get(
route_builder.build_base_route("procurement/health")
)
async def procurement_health_check():
"""
Health check endpoint for procurement service

View File

@@ -11,7 +11,9 @@ from sqlalchemy import text
from app.core.config import settings
from app.core.database import database_manager
from app.api.orders import router as orders_router
from app.api.procurement import router as procurement_router
from app.api.customers import router as customers_router
from app.api.order_operations import router as order_operations_router
from app.api.procurement_operations import router as procurement_operations_router
from app.services.procurement_scheduler_service import ProcurementSchedulerService
from shared.service_base import StandardFastAPIService
@@ -52,7 +54,7 @@ class OrdersService(StandardFastAPIService):
app_name=settings.APP_NAME,
description=settings.DESCRIPTION,
version=settings.VERSION,
api_prefix="/api/v1",
api_prefix="", # Empty because RouteBuilder already includes /api/v1
database_manager=database_manager,
expected_tables=orders_expected_tables
)
@@ -94,9 +96,14 @@ app = service.create_app()
# Setup standard endpoints
service.setup_standard_endpoints()
# Include routers
# Include routers - organized by ATOMIC and BUSINESS operations
# ATOMIC: Direct CRUD operations
service.add_router(orders_router)
service.add_router(procurement_router)
service.add_router(customers_router)
# BUSINESS: Complex operations and workflows
service.add_router(order_operations_router)
service.add_router(procurement_operations_router)
@app.post("/test/procurement-scheduler")

View File

@@ -1,8 +1,8 @@
"""initial_schema_20251001_1118
"""initial_schema_20251006_1516
Revision ID: 07e130577d3f
Revision ID: 1927463e0d6e
Revises:
Create Date: 2025-10-01 11:18:52.812809+02:00
Create Date: 2025-10-06 15:16:17.959902+02:00
"""
from typing import Sequence, Union
@@ -12,7 +12,7 @@ import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '07e130577d3f'
revision: str = '1927463e0d6e'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None