IMPORVE ONBOARDING STEPS

This commit is contained in:
Urtzi Alfaro
2025-11-09 09:22:08 +01:00
parent 4678f96f8f
commit cbe19a3cd1
27 changed files with 2801 additions and 1149 deletions

View File

@@ -49,12 +49,15 @@ ONBOARDING_STEPS = [
# Phase 2: Core Setup
"setup", # Basic bakery setup and tenant creation
# Phase 2a: AI-Assisted Path (ONLY PATH - manual path removed)
"smart-inventory-setup", # Sales data upload and AI analysis
"product-categorization", # Categorize products as ingredients vs finished products
# Phase 2a: AI-Assisted Inventory Setup (REFACTORED - split into 3 focused steps)
"upload-sales-data", # File upload, validation, and AI classification
"inventory-review", # Review and confirm AI-detected products with type selection
"initial-stock-entry", # Capture initial stock levels
# Phase 2b: Suppliers (shared by all paths)
# Phase 2b: Product Categorization (optional advanced categorization)
"product-categorization", # Advanced categorization (may be deprecated)
# Phase 2c: Suppliers (shared by all paths)
"suppliers-setup", # Suppliers configuration
# Phase 3: Advanced Configuration (all optional)
@@ -78,13 +81,16 @@ STEP_DEPENDENCIES = {
# Core setup - no longer depends on data-source-choice (removed)
"setup": ["user_registered", "bakery-type-selection"],
# AI-Assisted path dependencies (ONLY path now)
"smart-inventory-setup": ["user_registered", "setup"],
"product-categorization": ["user_registered", "setup", "smart-inventory-setup"],
"initial-stock-entry": ["user_registered", "setup", "smart-inventory-setup", "product-categorization"],
# AI-Assisted Inventory Setup - REFACTORED into 3 sequential steps
"upload-sales-data": ["user_registered", "setup"],
"inventory-review": ["user_registered", "setup", "upload-sales-data"],
"initial-stock-entry": ["user_registered", "setup", "upload-sales-data", "inventory-review"],
# Suppliers (after AI inventory setup)
"suppliers-setup": ["user_registered", "setup", "smart-inventory-setup"],
# Advanced product categorization (optional, may be deprecated)
"product-categorization": ["user_registered", "setup", "upload-sales-data"],
# Suppliers (after inventory review)
"suppliers-setup": ["user_registered", "setup", "inventory-review"],
# Advanced configuration (optional, minimal dependencies)
"recipes-setup": ["user_registered", "setup"],
@@ -92,8 +98,8 @@ STEP_DEPENDENCIES = {
"quality-setup": ["user_registered", "setup"],
"team-setup": ["user_registered", "setup"],
# ML Training - requires AI path completion
"ml-training": ["user_registered", "setup", "smart-inventory-setup"],
# ML Training - requires AI path completion (upload-sales-data with inventory review)
"ml-training": ["user_registered", "setup", "upload-sales-data", "inventory-review"],
# Review and completion
"setup-review": ["user_registered", "setup"],
@@ -277,20 +283,24 @@ class OnboardingService:
# SPECIAL VALIDATION FOR ML TRAINING STEP
if step_name == "ml-training":
# ML training requires AI-assisted path completion (only path available now)
ai_path_complete = user_progress_data.get("smart-inventory-setup", {}).get("completed", False)
# ML training requires AI-assisted path completion
# Check if upload-sales-data and inventory-review are completed
upload_complete = user_progress_data.get("upload-sales-data", {}).get("completed", False)
inventory_complete = user_progress_data.get("inventory-review", {}).get("completed", False)
if ai_path_complete:
if upload_complete and inventory_complete:
# Validate sales data was imported
smart_inventory_data = user_progress_data.get("smart-inventory-setup", {}).get("data", {})
sales_import_result = smart_inventory_data.get("salesImportResult", {})
has_sales_data_imported = (
sales_import_result.get("records_created", 0) > 0 or
sales_import_result.get("success", False) or
sales_import_result.get("imported", False)
upload_data = user_progress_data.get("upload-sales-data", {}).get("data", {})
inventory_data = user_progress_data.get("inventory-review", {}).get("data", {})
# Check if sales data was processed
has_sales_data = (
upload_data.get("validationResult", {}).get("is_valid", False) or
upload_data.get("aiSuggestions", []) or
inventory_data.get("inventoryItemsCreated", 0) > 0
)
if has_sales_data_imported:
if has_sales_data:
logger.info(f"ML training allowed for user {user_id}: AI path with sales data")
return True

View File

@@ -114,10 +114,11 @@ class Ingredient(Base):
last_purchase_price = Column(Numeric(10, 2), nullable=True)
standard_cost = Column(Numeric(10, 2), nullable=True)
# Stock management
low_stock_threshold = Column(Float, nullable=False, default=10.0)
reorder_point = Column(Float, nullable=False, default=20.0)
reorder_quantity = Column(Float, nullable=False, default=50.0)
# Stock management - now optional to simplify onboarding
# These can be configured later based on actual usage patterns
low_stock_threshold = Column(Float, nullable=True, default=None)
reorder_point = Column(Float, nullable=True, default=None)
reorder_quantity = Column(Float, nullable=True, default=None)
max_stock_level = Column(Float, nullable=True)
# Shelf life (critical for finished products) - default values only

View File

@@ -46,12 +46,14 @@ class IngredientCreate(InventoryBaseSchema):
# Pricing
# Note: average_cost is calculated automatically from purchases (not set on create)
# All cost fields are optional - can be added later after onboarding
standard_cost: Optional[Decimal] = Field(None, ge=0, description="Standard/target cost per unit for budgeting")
# Stock management
low_stock_threshold: float = Field(10.0, ge=0, description="Low stock alert threshold")
reorder_point: float = Field(20.0, ge=0, description="Reorder point")
reorder_quantity: float = Field(50.0, gt=0, description="Default reorder quantity")
# Stock management - all optional with sensible defaults for onboarding
# These can be configured later based on actual usage patterns
low_stock_threshold: Optional[float] = Field(None, ge=0, description="Low stock alert threshold")
reorder_point: Optional[float] = Field(None, ge=0, description="Reorder point")
reorder_quantity: Optional[float] = Field(None, gt=0, description="Default reorder quantity")
max_stock_level: Optional[float] = Field(None, gt=0, description="Maximum stock level")
# Shelf life (default value only - actual per batch)
@@ -67,8 +69,15 @@ class IngredientCreate(InventoryBaseSchema):
@validator('reorder_point')
def validate_reorder_point(cls, v, values):
if 'low_stock_threshold' in values and v <= values['low_stock_threshold']:
raise ValueError('Reorder point must be greater than low stock threshold')
# Only validate if both values are provided and not None
low_stock = values.get('low_stock_threshold')
if v is not None and low_stock is not None:
try:
if v <= low_stock:
raise ValueError('Reorder point must be greater than low stock threshold')
except TypeError:
# Skip validation if comparison fails due to type mismatch
pass
return v
@@ -125,9 +134,9 @@ class IngredientResponse(InventoryBaseSchema):
average_cost: Optional[float]
last_purchase_price: Optional[float]
standard_cost: Optional[float]
low_stock_threshold: float
reorder_point: float
reorder_quantity: float
low_stock_threshold: Optional[float] # Now optional
reorder_point: Optional[float] # Now optional
reorder_quantity: Optional[float] # Now optional
max_stock_level: Optional[float]
shelf_life_days: Optional[int] # Default value only
is_active: bool
@@ -209,9 +218,15 @@ class StockCreate(InventoryBaseSchema):
@validator('storage_temperature_max')
def validate_temperature_range(cls, v, values):
# Only validate if both values are provided and not None
min_temp = values.get('storage_temperature_min')
if v is not None and min_temp is not None and v <= min_temp:
raise ValueError('Max temperature must be greater than min temperature')
if v is not None and min_temp is not None:
try:
if v <= min_temp:
raise ValueError('Max temperature must be greater than min temperature')
except TypeError:
# Skip validation if comparison fails due to type mismatch
pass
return v
class StockUpdate(InventoryBaseSchema):

View File

@@ -1062,9 +1062,12 @@ class InventoryService:
async def _validate_ingredient_data(self, ingredient_data: IngredientCreate, tenant_id: UUID):
"""Validate ingredient data for business rules"""
# Add business validation logic here
if ingredient_data.reorder_point <= ingredient_data.low_stock_threshold:
raise ValueError("Reorder point must be greater than low stock threshold")
# Only validate reorder_point if both values are provided
# During onboarding, these fields may be None, which is valid
if (ingredient_data.reorder_point is not None and
ingredient_data.low_stock_threshold is not None):
if ingredient_data.reorder_point <= ingredient_data.low_stock_threshold:
raise ValueError("Reorder point must be greater than low stock threshold")
# Storage requirements validation moved to stock level (not ingredient level)
# This is now handled in stock creation/update validation

View File

@@ -0,0 +1,84 @@
"""make_stock_management_fields_nullable
Revision ID: make_stock_fields_nullable
Revises: add_local_production_support
Create Date: 2025-11-08 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'make_stock_fields_nullable'
down_revision = 'add_local_production_support'
branch_labels = None
depends_on = None
def upgrade() -> None:
"""Make stock management fields nullable to simplify onboarding
These fields (low_stock_threshold, reorder_point, reorder_quantity) are now optional
during onboarding and can be configured later based on actual usage patterns.
"""
# Make low_stock_threshold nullable
op.alter_column('ingredients', 'low_stock_threshold',
existing_type=sa.Float(),
nullable=True,
existing_nullable=False)
# Make reorder_point nullable
op.alter_column('ingredients', 'reorder_point',
existing_type=sa.Float(),
nullable=True,
existing_nullable=False)
# Make reorder_quantity nullable
op.alter_column('ingredients', 'reorder_quantity',
existing_type=sa.Float(),
nullable=True,
existing_nullable=False)
def downgrade() -> None:
"""Revert stock management fields to NOT NULL
WARNING: This will fail if any records have NULL values in these fields.
You must set default values before running this downgrade.
"""
# Set default values for any NULL records before making fields NOT NULL
op.execute("""
UPDATE ingredients
SET low_stock_threshold = 10.0
WHERE low_stock_threshold IS NULL
""")
op.execute("""
UPDATE ingredients
SET reorder_point = 20.0
WHERE reorder_point IS NULL
""")
op.execute("""
UPDATE ingredients
SET reorder_quantity = 50.0
WHERE reorder_quantity IS NULL
""")
# Make fields NOT NULL again
op.alter_column('ingredients', 'low_stock_threshold',
existing_type=sa.Float(),
nullable=False,
existing_nullable=True)
op.alter_column('ingredients', 'reorder_point',
existing_type=sa.Float(),
nullable=False,
existing_nullable=True)
op.alter_column('ingredients', 'reorder_quantity',
existing_type=sa.Float(),
nullable=False,
existing_nullable=True)