# 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)}")