REFACTOR ALL APIs
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# services/inventory/app/api/ingredients.py
|
||||
"""
|
||||
API endpoints for ingredient management
|
||||
Base CRUD operations for inventory ingredients resources
|
||||
Following standardized URL structure: /api/v1/tenants/{tenant_id}/inventory/{resource}
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
@@ -15,10 +16,15 @@ from app.schemas.inventory import (
|
||||
IngredientUpdate,
|
||||
IngredientResponse,
|
||||
StockResponse,
|
||||
InventoryFilter,
|
||||
PaginatedResponse
|
||||
StockCreate,
|
||||
StockUpdate,
|
||||
)
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.access_control import require_user_role, admin_role_required, owner_role_required
|
||||
from shared.routing import RouteBuilder
|
||||
|
||||
# Create route builder for consistent URL structure
|
||||
route_builder = RouteBuilder('inventory')
|
||||
|
||||
router = APIRouter(tags=["ingredients"])
|
||||
|
||||
@@ -34,26 +40,32 @@ def get_current_user_id(current_user: dict = Depends(get_current_user_dep)) -> U
|
||||
return UUID(user_id)
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/ingredients", response_model=IngredientResponse)
|
||||
# ===== INGREDIENTS ENDPOINTS =====
|
||||
|
||||
@router.post(
|
||||
route_builder.build_base_route("ingredients"),
|
||||
response_model=IngredientResponse,
|
||||
status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
@require_user_role(['admin', 'owner'])
|
||||
async def create_ingredient(
|
||||
ingredient_data: IngredientCreate,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Create a new ingredient"""
|
||||
"""Create a new ingredient (Admin/Manager only)"""
|
||||
try:
|
||||
# Extract user ID - handle service tokens that don't have UUID user_ids
|
||||
# Extract user ID - handle service tokens
|
||||
raw_user_id = current_user.get('user_id')
|
||||
if current_user.get('type') == 'service':
|
||||
# For service tokens, user_id might not be a UUID, so set to None
|
||||
user_id = None
|
||||
else:
|
||||
try:
|
||||
user_id = UUID(raw_user_id)
|
||||
except (ValueError, TypeError):
|
||||
user_id = None
|
||||
|
||||
|
||||
service = InventoryService()
|
||||
ingredient = await service.create_ingredient(ingredient_data, tenant_id, user_id)
|
||||
return ingredient
|
||||
@@ -69,14 +81,16 @@ async def create_ingredient(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/ingredients/count")
|
||||
@router.get(
|
||||
route_builder.build_base_route("ingredients/count"),
|
||||
response_model=dict
|
||||
)
|
||||
async def count_ingredients(
|
||||
tenant_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
) -> dict:
|
||||
"""Get count of ingredients for a tenant"""
|
||||
|
||||
):
|
||||
"""Get count of ingredients for a tenant (All users)"""
|
||||
try:
|
||||
service = InventoryService()
|
||||
count = await service.count_ingredients_by_tenant(tenant_id)
|
||||
@@ -93,23 +107,27 @@ async def count_ingredients(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/ingredients/{ingredient_id}", response_model=IngredientResponse)
|
||||
@router.get(
|
||||
route_builder.build_resource_detail_route("ingredients", "ingredient_id"),
|
||||
response_model=IngredientResponse
|
||||
)
|
||||
async def get_ingredient(
|
||||
ingredient_id: UUID,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get ingredient by ID"""
|
||||
"""Get ingredient by ID (All users)"""
|
||||
try:
|
||||
service = InventoryService()
|
||||
ingredient = await service.get_ingredient(ingredient_id, tenant_id)
|
||||
|
||||
|
||||
if not ingredient:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Ingredient not found"
|
||||
)
|
||||
|
||||
|
||||
return ingredient
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -120,24 +138,29 @@ async def get_ingredient(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/tenants/{tenant_id}/ingredients/{ingredient_id}", response_model=IngredientResponse)
|
||||
@router.put(
|
||||
route_builder.build_resource_detail_route("ingredients", "ingredient_id"),
|
||||
response_model=IngredientResponse
|
||||
)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def update_ingredient(
|
||||
ingredient_id: UUID,
|
||||
ingredient_data: IngredientUpdate,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Update ingredient"""
|
||||
try:
|
||||
"""Update ingredient (Admin/Manager/User)"""
|
||||
try:
|
||||
service = InventoryService()
|
||||
ingredient = await service.update_ingredient(ingredient_id, ingredient_data, tenant_id)
|
||||
|
||||
|
||||
if not ingredient:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Ingredient not found"
|
||||
)
|
||||
|
||||
|
||||
return ingredient
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
@@ -153,24 +176,27 @@ async def update_ingredient(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/ingredients", response_model=List[IngredientResponse])
|
||||
@router.get(
|
||||
route_builder.build_base_route("ingredients"),
|
||||
response_model=List[IngredientResponse]
|
||||
)
|
||||
async def list_ingredients(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="Number of records to return"),
|
||||
category: Optional[str] = Query(None, description="Filter by category"),
|
||||
product_type: Optional[str] = Query(None, description="Filter by product type (ingredient or finished_product)"),
|
||||
product_type: Optional[str] = Query(None, description="Filter by product type"),
|
||||
is_active: Optional[bool] = Query(None, description="Filter by active status"),
|
||||
is_low_stock: Optional[bool] = Query(None, description="Filter by low stock status"),
|
||||
needs_reorder: Optional[bool] = Query(None, description="Filter by reorder needed"),
|
||||
search: Optional[str] = Query(None, description="Search in name, SKU, or barcode"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""List ingredients with filtering"""
|
||||
"""List ingredients with filtering (All users)"""
|
||||
try:
|
||||
|
||||
service = InventoryService()
|
||||
|
||||
|
||||
# Build filters
|
||||
filters = {}
|
||||
if category:
|
||||
@@ -185,7 +211,7 @@ async def list_ingredients(
|
||||
filters['needs_reorder'] = needs_reorder
|
||||
if search:
|
||||
filters['search'] = search
|
||||
|
||||
|
||||
ingredients = await service.get_ingredients(tenant_id, skip, limit, filters)
|
||||
return ingredients
|
||||
except Exception as e:
|
||||
@@ -195,13 +221,18 @@ async def list_ingredients(
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/tenants/{tenant_id}/ingredients/{ingredient_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
route_builder.build_resource_detail_route("ingredients", "ingredient_id"),
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
@admin_role_required
|
||||
async def soft_delete_ingredient(
|
||||
ingredient_id: UUID,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Soft delete ingredient (mark as inactive)"""
|
||||
"""Soft delete ingredient - mark as inactive (Admin only)"""
|
||||
try:
|
||||
service = InventoryService()
|
||||
result = await service.soft_delete_ingredient(ingredient_id, tenant_id)
|
||||
@@ -218,13 +249,18 @@ async def soft_delete_ingredient(
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/tenants/{tenant_id}/ingredients/{ingredient_id}/hard")
|
||||
@router.delete(
|
||||
route_builder.build_nested_resource_route("ingredients", "ingredient_id", "hard"),
|
||||
response_model=dict
|
||||
)
|
||||
@admin_role_required
|
||||
async def hard_delete_ingredient(
|
||||
ingredient_id: UUID,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Hard delete ingredient and all associated data (stock, movements, etc.)"""
|
||||
"""Hard delete ingredient and all associated data (Admin only)"""
|
||||
try:
|
||||
service = InventoryService()
|
||||
deletion_summary = await service.hard_delete_ingredient(ingredient_id, tenant_id)
|
||||
@@ -241,16 +277,19 @@ async def hard_delete_ingredient(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/ingredients/{ingredient_id}/stock", response_model=List[StockResponse])
|
||||
@router.get(
|
||||
route_builder.build_nested_resource_route("ingredients", "ingredient_id", "stock"),
|
||||
response_model=List[StockResponse]
|
||||
)
|
||||
async def get_ingredient_stock(
|
||||
ingredient_id: UUID,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
include_unavailable: bool = Query(False, description="Include unavailable stock"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get stock entries for an ingredient"""
|
||||
"""Get stock entries for an ingredient (All users)"""
|
||||
try:
|
||||
|
||||
service = InventoryService()
|
||||
stock_entries = await service.get_stock_by_ingredient(
|
||||
ingredient_id, tenant_id, include_unavailable
|
||||
@@ -260,4 +299,4 @@ async def get_ingredient_stock(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get ingredient stock"
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user