# 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.ai_onboarding_service import ( AIOnboardingService, OnboardingValidationResult, ProductSuggestionsResult, OnboardingImportResult, get_ai_onboarding_service ) from shared.auth.decorators import get_current_user_dep, get_current_tenant_id_dep router = APIRouter(tags=["onboarding"]) logger = structlog.get_logger() class FileValidationResponse(BaseModel): """Response for file validation step""" is_valid: bool total_records: int unique_products: int product_list: List[str] validation_errors: List[Any] validation_warnings: List[Any] summary: Dict[str, Any] class ProductSuggestionsResponse(BaseModel): """Response for AI suggestions step""" suggestions: List[Dict[str, Any]] business_model_analysis: Dict[str, Any] total_products: int high_confidence_count: int low_confidence_count: int processing_time_seconds: float 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/validate-file", response_model=FileValidationResponse) async def validate_onboarding_file( 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: AIOnboardingService = Depends(get_ai_onboarding_service) ): """ Step 1: Validate uploaded file and extract unique products This endpoint: 1. Validates the file format and content 2. Checks for required columns (date, product, etc.) 3. Extracts unique products from sales data 4. Returns validation results and product list """ 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}") # Determine file format file_format = "csv" if file.filename.lower().endswith('.csv') else "excel" # Read file content file_content = await file.read() if not file_content: raise HTTPException(status_code=400, detail="File is empty") # Convert bytes to string for CSV if file_format == "csv": file_data = file_content.decode('utf-8') else: import base64 file_data = base64.b64encode(file_content).decode('utf-8') # Validate and extract products result = await onboarding_service.validate_and_extract_products( file_data=file_data, file_format=file_format, tenant_id=tenant_id ) response = FileValidationResponse( is_valid=result.is_valid, total_records=result.total_records, unique_products=result.unique_products, product_list=result.product_list, validation_errors=result.validation_details.errors, validation_warnings=result.validation_details.warnings, summary=result.summary ) logger.info("File validation complete", filename=file.filename, is_valid=result.is_valid, unique_products=result.unique_products, tenant_id=tenant_id) return response except HTTPException: raise except Exception as e: logger.error("Failed file validation", error=str(e), filename=file.filename if file else None, tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Validation failed: {str(e)}") @router.post("/tenants/{tenant_id}/onboarding/generate-suggestions", response_model=ProductSuggestionsResponse) async def generate_inventory_suggestions( file: UploadFile = File(..., description="Same sales data file from step 1"), product_list: str = Form(..., description="JSON array of product names to classify"), 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: AIOnboardingService = Depends(get_ai_onboarding_service) ): """ Step 2: Generate AI-powered inventory suggestions This endpoint: 1. Takes the validated file and product list from step 1 2. Uses AI to classify products into inventory categories 3. Analyzes business model (production vs retail) 4. Returns detailed 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") # Parse product list import json try: products = json.loads(product_list) except json.JSONDecodeError as e: raise HTTPException(status_code=400, detail=f"Invalid product list format: {str(e)}") if not products: raise HTTPException(status_code=400, detail="No products provided") # Determine file format file_format = "csv" if file.filename.lower().endswith('.csv') else "excel" # Read file content file_content = await file.read() if not file_content: raise HTTPException(status_code=400, detail="File is empty") # Convert bytes to string for CSV if file_format == "csv": file_data = file_content.decode('utf-8') else: import base64 file_data = base64.b64encode(file_content).decode('utf-8') # Generate suggestions result = await onboarding_service.generate_inventory_suggestions( product_list=products, file_data=file_data, file_format=file_format, tenant_id=tenant_id ) # Convert suggestions to dict format suggestions_dict = [] for suggestion in result.suggestions: suggestion_dict = { "suggestion_id": suggestion.suggestion_id, "original_name": suggestion.original_name, "suggested_name": suggestion.suggested_name, "product_type": suggestion.product_type, "category": suggestion.category, "unit_of_measure": suggestion.unit_of_measure, "confidence_score": suggestion.confidence_score, "estimated_shelf_life_days": suggestion.estimated_shelf_life_days, "requires_refrigeration": suggestion.requires_refrigeration, "requires_freezing": suggestion.requires_freezing, "is_seasonal": suggestion.is_seasonal, "suggested_supplier": suggestion.suggested_supplier, "notes": suggestion.notes, "sales_data": suggestion.sales_data } suggestions_dict.append(suggestion_dict) business_model_dict = { "model": result.business_model_analysis.model, "confidence": result.business_model_analysis.confidence, "ingredient_count": result.business_model_analysis.ingredient_count, "finished_product_count": result.business_model_analysis.finished_product_count, "ingredient_ratio": result.business_model_analysis.ingredient_ratio, "recommendations": result.business_model_analysis.recommendations } response = ProductSuggestionsResponse( suggestions=suggestions_dict, business_model_analysis=business_model_dict, total_products=result.total_products, high_confidence_count=result.high_confidence_count, low_confidence_count=result.low_confidence_count, processing_time_seconds=result.processing_time_seconds ) logger.info("AI suggestions generated", total_products=result.total_products, business_model=result.business_model_analysis.model, high_confidence=result.high_confidence_count, tenant_id=tenant_id) return response except HTTPException: raise except Exception as e: logger.error("Failed to generate suggestions", error=str(e), tenant_id=tenant_id) raise HTTPException(status_code=500, detail=f"Suggestion generation 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: AIOnboardingService = Depends(get_ai_onboarding_service) ): """ Step 3: Create inventory items from approved suggestions This endpoint: 1. Takes user-approved inventory suggestions from step 2 2. Applies any user modifications to suggestions 3. Creates inventory items via inventory service 4. Returns creation results for final import step """ 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") # Create inventory items using new service result = await onboarding_service.create_inventory_from_suggestions( approved_suggestions=request.suggestions, 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: AIOnboardingService = Depends(get_ai_onboarding_service) ): """ Step 4: Final sales data import using created inventory items This endpoint: 1. Takes the same validated sales file from step 1 2. Uses the inventory mapping from step 3 3. Imports sales records using detailed processing from DataImportService 4. Returns final import results - onboarding complete! """ 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 to string mapping for the new service inventory_mapping_dict = { product_name: str(inventory_id) for product_name, inventory_id in mapping.items() } except json.JSONDecodeError as e: raise HTTPException(status_code=400, detail=f"Invalid inventory mapping format: {str(e)}") # Determine file format file_format = "csv" if file.filename.lower().endswith('.csv') else "excel" # Read file content file_content = await file.read() if not file_content: raise HTTPException(status_code=400, detail="File is empty") # Convert bytes to string for CSV if file_format == "csv": file_data = file_content.decode('utf-8') else: import base64 file_data = base64.b64encode(file_content).decode('utf-8') # Import sales data using new service result = await onboarding_service.import_sales_data_with_inventory( file_data=file_data, file_format=file_format, inventory_mapping=inventory_mapping_dict, tenant_id=tenant_id, filename=file.filename ) response = SalesImportResponse( import_job_id="onboarding-" + str(tenant_id), # Generate a simple job ID status="completed" if result.success else "failed", processed_rows=result.import_details.records_processed, successful_imports=result.import_details.records_created, failed_imports=result.import_details.records_failed, errors=[error.get("message", str(error)) for error in result.import_details.errors], warnings=[warning.get("message", str(warning)) for warning in result.import_details.warnings] ) logger.info("Sales import complete", successful=result.import_details.records_created, failed=result.import_details.records_failed, 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)}")