Files
ALwrity/backend/api/onboarding_utils/step_management_service.py

371 lines
19 KiB
Python

"""
Step Management Service
Handles onboarding step operations and progress tracking.
"""
from typing import Dict, Any, List, Optional
from fastapi import HTTPException
from loguru import logger
from services.onboarding.progress_service import get_onboarding_progress_service
from services.onboarding.database_service import OnboardingDatabaseService
from services.database import get_db
class StepManagementService:
"""Service for handling onboarding step management."""
def __init__(self):
pass
async def get_onboarding_status(self, current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Get the current onboarding status (per user)."""
try:
user_id = str(current_user.get('id'))
status = get_onboarding_progress_service().get_onboarding_status(user_id)
return {
"is_completed": status["is_completed"],
"current_step": status["current_step"],
"completion_percentage": status["completion_percentage"],
"next_step": 6 if status["is_completed"] else max(1, status["current_step"]),
"started_at": status["started_at"],
"completed_at": status["completed_at"],
"can_proceed_to_final": True if status["is_completed"] else status["current_step"] >= 5,
}
except Exception as e:
logger.error(f"Error getting onboarding status: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_onboarding_progress_full(self, current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Get the full onboarding progress data."""
try:
user_id = str(current_user.get('id'))
progress_service = get_onboarding_progress_service()
status = progress_service.get_onboarding_status(user_id)
data = progress_service.get_completion_data(user_id)
def completed(b: bool) -> str:
return 'completed' if b else 'pending'
api_keys = data.get('api_keys') or {}
website = data.get('website_analysis') or {}
research = data.get('research_preferences') or {}
persona = data.get('persona_data') or {}
steps = [
{
"step_number": 1,
"title": "API Keys",
"description": "Connect your AI services",
"status": completed(any(v for v in api_keys.values() if v)),
"completed_at": None,
"data": None,
"validation_errors": []
},
{
"step_number": 2,
"title": "Website",
"description": "Set up your website",
"status": completed(bool(website.get('website_url') or website.get('writing_style'))),
"completed_at": None,
"data": website or None,
"validation_errors": []
},
{
"step_number": 3,
"title": "Research",
"description": "Discover competitors",
"status": completed(bool(research.get('research_depth') or research.get('content_types'))),
"completed_at": None,
"data": research or None,
"validation_errors": []
},
{
"step_number": 4,
"title": "Personalization",
"description": "Customize your experience",
"status": completed(bool(persona.get('corePersona') or persona.get('platformPersonas'))),
"completed_at": None,
"data": persona or None,
"validation_errors": []
},
{
"step_number": 5,
"title": "Integrations",
"description": "Connect additional services",
"status": completed(status['current_step'] >= 5),
"completed_at": None,
"data": None,
"validation_errors": []
},
{
"step_number": 6,
"title": "Finish",
"description": "Complete setup",
"status": completed(status['is_completed']),
"completed_at": status['completed_at'],
"data": None,
"validation_errors": []
}
]
return {
"steps": steps,
"current_step": 6 if status['is_completed'] else status['current_step'],
"started_at": status['started_at'],
"last_updated": status['last_updated'],
"is_completed": status['is_completed'],
"completed_at": status['completed_at'],
"completion_percentage": status['completion_percentage']
}
except Exception as e:
logger.error(f"Error getting onboarding progress: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def get_step_data(self, step_number: int, current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Get data for a specific step."""
try:
user_id = str(current_user.get('id'))
db = next(get_db())
db_service = OnboardingDatabaseService()
if step_number == 2:
website = db_service.get_website_analysis(user_id, db) or {}
return {
"step_number": 2,
"title": "Website",
"description": "Set up your website",
"status": 'completed' if (website.get('website_url') or website.get('writing_style')) else 'pending',
"completed_at": None,
"data": website,
"validation_errors": []
}
if step_number == 3:
research = db_service.get_research_preferences(user_id, db) or {}
return {
"step_number": 3,
"title": "Research",
"description": "Discover competitors",
"status": 'completed' if (research.get('research_depth') or research.get('content_types')) else 'pending',
"completed_at": None,
"data": research,
"validation_errors": []
}
if step_number == 4:
persona = db_service.get_persona_data(user_id, db) or {}
return {
"step_number": 4,
"title": "Personalization",
"description": "Customize your experience",
"status": 'completed' if (persona.get('corePersona') or persona.get('platformPersonas')) else 'pending',
"completed_at": None,
"data": persona,
"validation_errors": []
}
status = get_onboarding_progress_service().get_onboarding_status(user_id)
mapping = {
1: ('API Keys', 'Connect your AI services', status['current_step'] >= 1),
5: ('Integrations', 'Connect additional services', status['current_step'] >= 5),
6: ('Finish', 'Complete setup', status['is_completed'])
}
title, description, done = mapping.get(step_number, (f'Step {step_number}', 'Onboarding step', False))
return {
"step_number": step_number,
"title": title,
"description": description,
"status": 'completed' if done else 'pending',
"completed_at": status['completed_at'] if step_number == 6 and done else None,
"data": None,
"validation_errors": []
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting step data: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def complete_step(self, step_number: int, request_data: Dict[str, Any], current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Mark a step as completed."""
try:
logger.info(f"[complete_step] Completing step {step_number}")
user_id = str(current_user.get('id'))
# Optional validation
try:
from services.validation import validate_step_data
logger.info(f"[complete_step] Validating step {step_number} with data: {request_data}")
validation_errors = validate_step_data(step_number, request_data)
if validation_errors:
logger.warning(f"[complete_step] Step {step_number} validation failed: {validation_errors}")
raise HTTPException(status_code=400, detail=f"Step validation failed: {'; '.join(validation_errors)}")
except ImportError:
pass
db = next(get_db())
db_service = OnboardingDatabaseService()
save_errors = [] # Track save failures
# Step-specific side effects: save data to DB
if step_number == 1 and request_data:
# Step 1: Save API keys
step_data = request_data.get('data') or request_data
logger.info(f"🔍 Step 1: Raw request_data keys: {list(request_data.keys()) if request_data else 'None'}")
logger.info(f"🔍 Step 1: Extracted step_data keys: {list(step_data.keys()) if step_data else 'None'}")
api_keys = step_data.get('api_keys', {})
logger.info(f"🔍 Step 1: API keys found: {list(api_keys.keys()) if api_keys else 'None'}")
if api_keys:
for provider, key in api_keys.items():
if key:
try:
saved = db_service.save_api_key(user_id, provider, key, db)
if saved:
logger.info(f"✅ Saved API key for provider {provider}")
else:
# This should not happen anymore since save_api_key now raises exceptions
raise Exception(f"API key save returned False for provider {provider}")
except Exception as e:
logger.error(f"❌ BLOCKING ERROR: Failed to save API key for provider {provider}: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Failed to save API key for {provider}. Onboarding cannot proceed until this is resolved."
) from e
# Step 2: Save website analysis data
elif step_number == 2 and request_data:
website_data = request_data.get('data') or request_data
logger.info(f"🔍 Step 2: Raw request_data keys: {list(request_data.keys()) if request_data else 'None'}")
logger.info(f"🔍 Step 2: Extracted website_data keys: {list(website_data.keys()) if website_data else 'None'}")
logger.info(f"🔍 Step 2: website_data.website: {website_data.get('website') if website_data else 'None'}")
logger.info(f"🔍 Step 2: website_data.analysis: {bool(website_data.get('analysis')) if website_data else 'None'}")
if website_data.get('analysis'):
logger.info(f"🔍 Step 2: analysis keys: {list(website_data['analysis'].keys()) if isinstance(website_data.get('analysis'), dict) else 'Not dict'}")
if website_data:
try:
saved = db_service.save_website_analysis(user_id, website_data, db)
if saved:
logger.info(f"✅ Saved website analysis for user {user_id}")
else:
# This should not happen anymore since save_website_analysis now raises exceptions
raise Exception("Website analysis save returned False")
except Exception as e:
logger.error(f"❌ BLOCKING ERROR: Failed to save website analysis: {str(e)}")
raise HTTPException(
status_code=500,
detail="Failed to save website analysis data. Onboarding cannot proceed until this is resolved."
) from e
# Step 3: Save research preferences data
elif step_number == 3 and request_data:
research_data = request_data.get('data') or request_data
logger.info(f"🔍 Step 3: Raw request_data keys: {list(request_data.keys()) if request_data else 'None'}")
logger.info(f"🔍 Step 3: Extracted research_data keys: {list(research_data.keys()) if research_data else 'None'}")
if research_data:
# Note: Competitor data is saved separately via discover-competitors endpoint
# This saves research preferences (content_types, target_audience, etc.)
try:
saved = db_service.save_research_preferences(user_id, research_data, db)
if saved:
logger.info(f"✅ Saved research preferences for user {user_id}")
else:
# This should not happen anymore since save_research_preferences now raises exceptions
raise Exception("Research preferences save returned False")
except Exception as e:
logger.error(f"❌ BLOCKING ERROR: Failed to save research preferences: {str(e)}")
raise HTTPException(
status_code=500,
detail="Failed to save research preferences. Onboarding cannot proceed until this is resolved."
) from e
# Step 4: Save persona data
elif step_number == 4 and request_data:
persona_data = request_data.get('data') or request_data
logger.info(f"🔍 Step 4: Raw request_data keys: {list(request_data.keys()) if request_data else 'None'}")
logger.info(f"🔍 Step 4: Extracted persona_data keys: {list(persona_data.keys()) if persona_data else 'None'}")
if persona_data:
try:
saved = db_service.save_persona_data(user_id, persona_data, db)
if saved:
logger.info(f"✅ Saved persona data for user {user_id}")
else:
# This should not happen anymore since save_persona_data now raises exceptions
raise Exception("Persona data save returned False")
except Exception as e:
logger.error(f"❌ BLOCKING ERROR: Failed to save persona data: {str(e)}")
raise HTTPException(
status_code=500,
detail="Failed to save persona data. Onboarding cannot proceed until this is resolved."
) from e
# Persist current step and progress in DB
db_service.update_step(user_id, step_number, db)
try:
progress_pct = min(100.0, round((step_number / 6) * 100))
db_service.update_progress(user_id, float(progress_pct), db)
except Exception as e:
logger.warning(f"Failed to update progress: {e}")
# Log save errors but don't block step completion (non-blocking)
if save_errors:
logger.warning(f"⚠️ Step {step_number} completed but some data save operations failed: {save_errors}")
logger.info(f"[complete_step] Step {step_number} persisted to DB for user {user_id}")
return {
"message": "Step completed successfully",
"step_number": step_number,
"data": request_data or {},
"warnings": save_errors if save_errors else None # Include warnings in response
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error completing step: {str(e)}")
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail="Internal server error")
async def skip_step(self, step_number: int, current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Skip a step (for optional steps)."""
try:
user_id = str(current_user.get('id'))
progress = get_onboarding_progress_for_user(user_id)
step = progress.get_step_data(step_number)
if not step:
raise HTTPException(status_code=404, detail=f"Step {step_number} not found")
# Mark step as skipped
progress.mark_step_skipped(step_number)
return {
"message": f"Step {step_number} skipped successfully",
"step_number": step_number
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error skipping step: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def validate_step_access(self, step_number: int, current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Validate if user can access a specific step."""
try:
user_id = str(current_user.get('id'))
progress = get_onboarding_progress_for_user(user_id)
if not progress.can_proceed_to_step(step_number):
return {
"can_proceed": False,
"validation_errors": [f"Cannot proceed to step {step_number}. Complete previous steps first."],
"step_status": "locked"
}
return {
"can_proceed": True,
"validation_errors": [],
"step_status": "available"
}
except Exception as e:
logger.error(f"Error validating step access: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")