371 lines
19 KiB
Python
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")
|