REFACTOR ALL APIs
This commit is contained in:
262
services/orders/app/api/customers.py
Normal file
262
services/orders/app/api/customers.py
Normal 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"
|
||||
)
|
||||
237
services/orders/app/api/order_operations.py
Normal file
237
services/orders/app/api/order_operations.py
Normal 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"
|
||||
)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user