Files
bakery-ia/services/sales/app/api/onboarding.py
2025-08-13 17:39:35 +02:00

368 lines
14 KiB
Python

# services/sales/app/api/onboarding.py
"""
Onboarding API Endpoints
Handles sales data import with automated inventory creation
"""
from fastapi import APIRouter, Depends, HTTPException, Path, UploadFile, File, Form
from typing import List, Dict, Any, Optional
from uuid import UUID
from pydantic import BaseModel, Field
import structlog
from app.services.onboarding_import_service import (
OnboardingImportService,
OnboardingImportResult,
InventoryCreationRequest,
get_onboarding_import_service
)
from shared.auth.decorators import get_current_user_dep, get_current_tenant_id_dep
router = APIRouter(tags=["onboarding"])
logger = structlog.get_logger()
class OnboardingAnalysisResponse(BaseModel):
"""Response for onboarding analysis"""
total_products_found: int
inventory_suggestions: List[Dict[str, Any]]
business_model_analysis: Dict[str, Any]
import_job_id: str
status: str
processed_rows: int
errors: List[str]
warnings: List[str]
class InventoryApprovalRequest(BaseModel):
"""Request to approve/modify inventory suggestions"""
suggestions: List[Dict[str, Any]] = Field(..., description="Approved suggestions with modifications")
class InventoryCreationResponse(BaseModel):
"""Response for inventory creation"""
created_items: List[Dict[str, Any]]
failed_items: List[Dict[str, Any]]
total_approved: int
success_rate: float
class SalesImportResponse(BaseModel):
"""Response for final sales import"""
import_job_id: str
status: str
processed_rows: int
successful_imports: int
failed_imports: int
errors: List[str]
warnings: List[str]
@router.post("/tenants/{tenant_id}/onboarding/analyze", response_model=OnboardingAnalysisResponse)
async def analyze_onboarding_data(
file: UploadFile = File(..., description="Sales data CSV/Excel file"),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
onboarding_service: OnboardingImportService = Depends(get_onboarding_import_service)
):
"""
Step 1: Analyze uploaded sales data and suggest inventory items
This endpoint:
1. Parses the uploaded sales file
2. Extracts unique products and sales metrics
3. Uses AI to classify products and suggest inventory items
4. Analyzes business model (production vs retail)
5. Returns suggestions for user review
"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# Validate file
if not file.filename:
raise HTTPException(status_code=400, detail="No file provided")
allowed_extensions = ['.csv', '.xlsx', '.xls']
if not any(file.filename.lower().endswith(ext) for ext in allowed_extensions):
raise HTTPException(status_code=400, detail=f"Unsupported file format. Allowed: {allowed_extensions}")
# Read file content
file_content = await file.read()
if not file_content:
raise HTTPException(status_code=400, detail="File is empty")
# Analyze the data
result = await onboarding_service.analyze_sales_data_for_onboarding(
file_content=file_content,
filename=file.filename,
tenant_id=tenant_id,
user_id=UUID(current_user['user_id'])
)
response = OnboardingAnalysisResponse(
total_products_found=result.total_products_found,
inventory_suggestions=result.inventory_suggestions,
business_model_analysis=result.business_model_analysis,
import_job_id=str(result.import_job_id),
status=result.status,
processed_rows=result.processed_rows,
errors=result.errors,
warnings=result.warnings
)
logger.info("Onboarding analysis complete",
filename=file.filename,
products_found=result.total_products_found,
business_model=result.business_model_analysis.get('model'),
tenant_id=tenant_id)
return response
except HTTPException:
raise
except Exception as e:
logger.error("Failed onboarding analysis",
error=str(e), filename=file.filename if file else None, tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
@router.post("/tenants/{tenant_id}/onboarding/create-inventory", response_model=InventoryCreationResponse)
async def create_inventory_from_suggestions(
request: InventoryApprovalRequest,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
onboarding_service: OnboardingImportService = Depends(get_onboarding_import_service)
):
"""
Step 2: Create inventory items from approved suggestions
This endpoint:
1. Takes user-approved inventory suggestions
2. Applies any user modifications
3. Creates inventory items via inventory service
4. Returns creation results
"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
if not request.suggestions:
raise HTTPException(status_code=400, detail="No suggestions provided")
# Convert to internal format
approval_requests = []
for suggestion in request.suggestions:
approval_requests.append(InventoryCreationRequest(
suggestion_id=suggestion.get('suggestion_id'),
approved=suggestion.get('approved', False),
modifications=suggestion.get('modifications', {})
))
# Create inventory items
result = await onboarding_service.create_inventory_from_suggestions(
suggestions_approval=approval_requests,
tenant_id=tenant_id,
user_id=UUID(current_user['user_id'])
)
response = InventoryCreationResponse(
created_items=result['created_items'],
failed_items=result['failed_items'],
total_approved=result['total_approved'],
success_rate=result['success_rate']
)
logger.info("Inventory creation complete",
created=len(result['created_items']),
failed=len(result['failed_items']),
tenant_id=tenant_id)
return response
except HTTPException:
raise
except Exception as e:
logger.error("Failed inventory creation",
error=str(e), tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Inventory creation failed: {str(e)}")
@router.post("/tenants/{tenant_id}/onboarding/import-sales", response_model=SalesImportResponse)
async def import_sales_with_inventory(
file: UploadFile = File(..., description="Sales data CSV/Excel file"),
inventory_mapping: str = Form(..., description="JSON mapping of product names to inventory IDs"),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
onboarding_service: OnboardingImportService = Depends(get_onboarding_import_service)
):
"""
Step 3: Import sales data using created inventory items
This endpoint:
1. Takes the same sales file from step 1
2. Uses the inventory mapping from step 2
3. Imports sales records with proper inventory product references
4. Returns import results
"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
# Validate file
if not file.filename:
raise HTTPException(status_code=400, detail="No file provided")
# Parse inventory mapping
import json
try:
mapping = json.loads(inventory_mapping)
# Convert string UUIDs to UUID objects
inventory_mapping_uuids = {
product_name: UUID(inventory_id)
for product_name, inventory_id in mapping.items()
}
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(status_code=400, detail=f"Invalid inventory mapping format: {str(e)}")
# Read file content
file_content = await file.read()
if not file_content:
raise HTTPException(status_code=400, detail="File is empty")
# Import sales data
result = await onboarding_service.import_sales_data_with_inventory(
file_content=file_content,
filename=file.filename,
tenant_id=tenant_id,
user_id=UUID(current_user['user_id']),
inventory_mapping=inventory_mapping_uuids
)
response = SalesImportResponse(
import_job_id=str(result.import_job_id),
status=result.status,
processed_rows=result.processed_rows,
successful_imports=result.successful_imports,
failed_imports=result.failed_imports,
errors=result.errors,
warnings=result.warnings
)
logger.info("Sales import complete",
successful=result.successful_imports,
failed=result.failed_imports,
filename=file.filename,
tenant_id=tenant_id)
return response
except HTTPException:
raise
except Exception as e:
logger.error("Failed sales import",
error=str(e), filename=file.filename if file else None, tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Sales import failed: {str(e)}")
@router.get("/tenants/{tenant_id}/onboarding/business-model-guide")
async def get_business_model_guide(
model: str,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_tenant: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep)
):
"""
Get setup recommendations based on detected business model
"""
try:
# Verify tenant access
if str(tenant_id) != current_tenant:
raise HTTPException(status_code=403, detail="Access denied to this tenant")
guides = {
'production': {
'title': 'Production Bakery Setup',
'description': 'Your bakery produces items from raw ingredients',
'next_steps': [
'Set up supplier relationships for ingredients',
'Configure recipe management and costing',
'Enable production planning and scheduling',
'Set up ingredient inventory alerts and reorder points'
],
'recommended_features': [
'Recipe & Production Management',
'Supplier & Procurement',
'Ingredient Inventory Tracking',
'Production Cost Analysis'
],
'sample_workflows': [
'Create recipes with ingredient costs',
'Plan daily production based on sales forecasts',
'Track ingredient usage and waste',
'Generate supplier purchase orders'
]
},
'retail': {
'title': 'Retail Bakery Setup',
'description': 'Your bakery sells finished products from central bakers',
'next_steps': [
'Configure central baker relationships',
'Set up delivery schedules and tracking',
'Enable finished product freshness monitoring',
'Focus on sales forecasting and ordering'
],
'recommended_features': [
'Central Baker Management',
'Delivery Schedule Tracking',
'Freshness Monitoring',
'Sales Forecasting'
],
'sample_workflows': [
'Set up central baker delivery schedules',
'Track product freshness and expiration',
'Forecast demand and place orders',
'Monitor sales performance by product'
]
},
'hybrid': {
'title': 'Hybrid Bakery Setup',
'description': 'Your bakery both produces items and sells finished products',
'next_steps': [
'Configure both production and retail features',
'Set up flexible inventory categories',
'Enable comprehensive analytics',
'Plan workflows for both business models'
],
'recommended_features': [
'Full Inventory Management',
'Recipe & Production Management',
'Central Baker Management',
'Advanced Analytics'
],
'sample_workflows': [
'Manage both ingredients and finished products',
'Balance production vs purchasing decisions',
'Track costs across both models',
'Optimize inventory mix based on profitability'
]
}
}
if model not in guides:
raise HTTPException(status_code=400, detail="Invalid business model")
return guides[model]
except HTTPException:
raise
except Exception as e:
logger.error("Failed to get business model guide",
error=str(e), model=model, tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to get guide: {str(e)}")