Fix new services implementation 2
This commit is contained in:
@@ -5,8 +5,8 @@ AI-powered product classification for onboarding automation
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path
|
||||
from typing import List, Dict, Any
|
||||
from uuid import UUID
|
||||
from typing import List, Dict, Any, Optional
|
||||
from uuid import UUID, uuid4
|
||||
from pydantic import BaseModel, Field
|
||||
import structlog
|
||||
|
||||
@@ -38,12 +38,12 @@ class ProductSuggestionResponse(BaseModel):
|
||||
category: str
|
||||
unit_of_measure: str
|
||||
confidence_score: float
|
||||
estimated_shelf_life_days: int = None
|
||||
estimated_shelf_life_days: Optional[int] = None
|
||||
requires_refrigeration: bool = False
|
||||
requires_freezing: bool = False
|
||||
is_seasonal: bool = False
|
||||
suggested_supplier: str = None
|
||||
notes: str = None
|
||||
suggested_supplier: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class BusinessModelAnalysisResponse(BaseModel):
|
||||
@@ -87,7 +87,7 @@ async def classify_single_product(
|
||||
|
||||
# Convert to response format
|
||||
response = ProductSuggestionResponse(
|
||||
suggestion_id=str(UUID.uuid4()), # Generate unique ID for tracking
|
||||
suggestion_id=str(uuid4()), # Generate unique ID for tracking
|
||||
original_name=suggestion.original_name,
|
||||
suggested_name=suggestion.suggested_name,
|
||||
product_type=suggestion.product_type.value,
|
||||
@@ -144,7 +144,7 @@ async def classify_products_batch(
|
||||
suggestion_responses = []
|
||||
for suggestion in suggestions:
|
||||
suggestion_responses.append(ProductSuggestionResponse(
|
||||
suggestion_id=str(UUID.uuid4()),
|
||||
suggestion_id=str(uuid4()),
|
||||
original_name=suggestion.original_name,
|
||||
suggested_name=suggestion.suggested_name,
|
||||
product_type=suggestion.product_type.value,
|
||||
@@ -159,39 +159,58 @@ async def classify_products_batch(
|
||||
notes=suggestion.notes
|
||||
))
|
||||
|
||||
# Analyze business model
|
||||
# Analyze business model with enhanced detection
|
||||
ingredient_count = sum(1 for s in suggestions if s.product_type.value == 'ingredient')
|
||||
finished_count = sum(1 for s in suggestions if s.product_type.value == 'finished_product')
|
||||
semi_finished_count = sum(1 for s in suggestions if 'semi' in s.suggested_name.lower() or 'frozen' in s.suggested_name.lower() or 'pre' in s.suggested_name.lower())
|
||||
total = len(suggestions)
|
||||
ingredient_ratio = ingredient_count / total if total > 0 else 0
|
||||
semi_finished_ratio = semi_finished_count / total if total > 0 else 0
|
||||
|
||||
# Determine business model
|
||||
# Enhanced business model determination
|
||||
if ingredient_ratio >= 0.7:
|
||||
model = 'production'
|
||||
model = 'individual_bakery' # Full production from raw ingredients
|
||||
elif ingredient_ratio <= 0.2 and semi_finished_ratio >= 0.3:
|
||||
model = 'central_baker_satellite' # Receives semi-finished products from central baker
|
||||
elif ingredient_ratio <= 0.3:
|
||||
model = 'retail'
|
||||
model = 'retail_bakery' # Sells finished products from suppliers
|
||||
else:
|
||||
model = 'hybrid'
|
||||
model = 'hybrid_bakery' # Mixed model
|
||||
|
||||
confidence = max(abs(ingredient_ratio - 0.5) * 2, 0.1)
|
||||
# Calculate confidence based on clear distinction
|
||||
if model == 'individual_bakery':
|
||||
confidence = min(ingredient_ratio * 1.2, 0.95)
|
||||
elif model == 'central_baker_satellite':
|
||||
confidence = min((semi_finished_ratio + (1 - ingredient_ratio)) / 2 * 1.2, 0.95)
|
||||
else:
|
||||
confidence = max(abs(ingredient_ratio - 0.5) * 2, 0.1)
|
||||
|
||||
recommendations = {
|
||||
'production': [
|
||||
'Focus on ingredient inventory management',
|
||||
'Set up recipe cost calculation',
|
||||
'Configure supplier relationships',
|
||||
'Enable production planning features'
|
||||
'individual_bakery': [
|
||||
'Set up raw ingredient inventory management',
|
||||
'Configure recipe cost calculation and production planning',
|
||||
'Enable supplier relationships for flour, yeast, sugar, etc.',
|
||||
'Set up full production workflow with proofing and baking schedules',
|
||||
'Enable waste tracking for overproduction'
|
||||
],
|
||||
'retail': [
|
||||
'Configure central baker relationships',
|
||||
'Set up delivery schedule tracking',
|
||||
'Enable finished product freshness monitoring',
|
||||
'Focus on sales forecasting'
|
||||
'central_baker_satellite': [
|
||||
'Configure central baker delivery schedules',
|
||||
'Set up semi-finished product inventory (frozen dough, par-baked items)',
|
||||
'Enable finish-baking workflow and timing optimization',
|
||||
'Track freshness and shelf-life for received products',
|
||||
'Focus on customer demand forecasting for final products'
|
||||
],
|
||||
'hybrid': [
|
||||
'Configure both ingredient and finished product management',
|
||||
'Set up flexible inventory categories',
|
||||
'Enable both production and retail features'
|
||||
'retail_bakery': [
|
||||
'Set up finished product supplier relationships',
|
||||
'Configure delivery schedule tracking',
|
||||
'Enable freshness monitoring and expiration management',
|
||||
'Focus on sales forecasting and customer preferences'
|
||||
],
|
||||
'hybrid_bakery': [
|
||||
'Configure both ingredient and semi-finished product management',
|
||||
'Set up flexible production workflows',
|
||||
'Enable both supplier and central baker relationships',
|
||||
'Configure multi-tier inventory categories'
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ API endpoints for ingredient management
|
||||
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
@@ -17,10 +17,9 @@ from app.schemas.inventory import (
|
||||
InventoryFilter,
|
||||
PaginatedResponse
|
||||
)
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.tenant_access import verify_tenant_access_dep
|
||||
from shared.auth.decorators import get_current_user_dep, get_current_tenant_id_dep
|
||||
|
||||
router = APIRouter(prefix="/ingredients", tags=["ingredients"])
|
||||
router = APIRouter(tags=["ingredients"])
|
||||
|
||||
# Helper function to extract user ID from user object
|
||||
def get_current_user_id(current_user: dict = Depends(get_current_user_dep)) -> UUID:
|
||||
@@ -34,15 +33,31 @@ def get_current_user_id(current_user: dict = Depends(get_current_user_dep)) -> U
|
||||
return UUID(user_id)
|
||||
|
||||
|
||||
@router.post("/", response_model=IngredientResponse)
|
||||
@router.post("/tenants/{tenant_id}/ingredients", response_model=IngredientResponse)
|
||||
async def create_ingredient(
|
||||
ingredient_data: IngredientCreate,
|
||||
tenant_id: UUID = Depends(verify_tenant_access_dep),
|
||||
user_id: UUID = Depends(get_current_user_id),
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Create a new ingredient"""
|
||||
try:
|
||||
# Verify tenant access
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
||||
|
||||
# Extract user ID - handle service tokens that don't have UUID user_ids
|
||||
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
|
||||
@@ -58,14 +73,20 @@ async def create_ingredient(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{ingredient_id}", response_model=IngredientResponse)
|
||||
@router.get("/tenants/{tenant_id}/ingredients/{ingredient_id}", response_model=IngredientResponse)
|
||||
async def get_ingredient(
|
||||
ingredient_id: UUID,
|
||||
tenant_id: UUID = Depends(verify_tenant_access_dep),
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get ingredient by ID"""
|
||||
try:
|
||||
# Verify tenant access
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
||||
|
||||
service = InventoryService()
|
||||
ingredient = await service.get_ingredient(ingredient_id, tenant_id)
|
||||
|
||||
@@ -85,15 +106,21 @@ async def get_ingredient(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{ingredient_id}", response_model=IngredientResponse)
|
||||
@router.put("/tenants/{tenant_id}/ingredients/{ingredient_id}", response_model=IngredientResponse)
|
||||
async def update_ingredient(
|
||||
ingredient_id: UUID,
|
||||
ingredient_data: IngredientUpdate,
|
||||
tenant_id: UUID = Depends(verify_tenant_access_dep),
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Update ingredient"""
|
||||
try:
|
||||
# Verify tenant access
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
||||
|
||||
service = InventoryService()
|
||||
ingredient = await service.update_ingredient(ingredient_id, ingredient_data, tenant_id)
|
||||
|
||||
@@ -118,8 +145,9 @@ async def update_ingredient(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_model=List[IngredientResponse])
|
||||
@router.get("/tenants/{tenant_id}/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"),
|
||||
@@ -127,11 +155,16 @@ async def list_ingredients(
|
||||
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"),
|
||||
tenant_id: UUID = Depends(verify_tenant_access_dep),
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""List ingredients with filtering"""
|
||||
try:
|
||||
# Verify tenant access
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
||||
|
||||
service = InventoryService()
|
||||
|
||||
# Build filters
|
||||
@@ -156,14 +189,20 @@ async def list_ingredients(
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{ingredient_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete("/tenants/{tenant_id}/ingredients/{ingredient_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_ingredient(
|
||||
ingredient_id: UUID,
|
||||
tenant_id: UUID = Depends(verify_tenant_access_dep),
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Soft delete ingredient (mark as inactive)"""
|
||||
try:
|
||||
# Verify tenant access
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
||||
|
||||
service = InventoryService()
|
||||
ingredient = await service.update_ingredient(
|
||||
ingredient_id,
|
||||
@@ -187,15 +226,21 @@ async def delete_ingredient(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{ingredient_id}/stock", response_model=List[dict])
|
||||
@router.get("/tenants/{tenant_id}/ingredients/{ingredient_id}/stock", response_model=List[dict])
|
||||
async def get_ingredient_stock(
|
||||
ingredient_id: UUID,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
include_unavailable: bool = Query(False, description="Include unavailable stock"),
|
||||
tenant_id: UUID = Depends(verify_tenant_access_dep),
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get stock entries for an ingredient"""
|
||||
try:
|
||||
# Verify tenant access
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(status_code=403, detail="Access denied to this tenant")
|
||||
|
||||
service = InventoryService()
|
||||
stock_entries = await service.get_stock_by_ingredient(
|
||||
ingredient_id, tenant_id, include_unavailable
|
||||
|
||||
Reference in New Issue
Block a user