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

328 lines
16 KiB
Python

"""
Onboarding Completion Service
Handles the complex logic for completing the onboarding process.
"""
from typing import Dict, Any, List
from datetime import datetime
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
from services.persona_analysis_service import PersonaAnalysisService
from services.research.research_persona_scheduler import schedule_research_persona_generation
from services.persona.facebook.facebook_persona_scheduler import schedule_facebook_persona_generation
from services.oauth_token_monitoring_service import create_oauth_monitoring_tasks
class OnboardingCompletionService:
"""Service for handling onboarding completion logic."""
def __init__(self):
# Pre-requisite steps; step 6 is the finalization itself
self.required_steps = [1, 2, 3, 4, 5]
async def complete_onboarding(self, current_user: Dict[str, Any]) -> Dict[str, Any]:
"""Complete the onboarding process with full validation."""
try:
user_id = str(current_user.get('id'))
progress_service = get_onboarding_progress_service()
# Strict DB-only validation now that step persistence is solid
missing_steps = self._validate_required_steps_database(user_id)
if missing_steps:
missing_steps_str = ", ".join(missing_steps)
raise HTTPException(
status_code=400,
detail=f"Cannot complete onboarding. The following steps must be completed first: {missing_steps_str}"
)
# Require API keys in DB for completion
self._validate_api_keys(user_id)
# Generate writing persona from onboarding data only if not already present
persona_generated = await self._generate_persona_from_onboarding(user_id)
# Complete the onboarding process in database
success = progress_service.complete_onboarding(user_id)
if not success:
raise HTTPException(status_code=500, detail="Failed to mark onboarding as complete")
# Schedule research persona generation 20 minutes after onboarding completion
try:
schedule_research_persona_generation(user_id, delay_minutes=20)
logger.info(f"Scheduled research persona generation for user {user_id} (20 minutes after onboarding)")
except Exception as e:
# Non-critical: log but don't fail onboarding completion
logger.warning(f"Failed to schedule research persona generation for user {user_id}: {e}")
# Schedule Facebook persona generation 20 minutes after onboarding completion
try:
schedule_facebook_persona_generation(user_id, delay_minutes=20)
logger.info(f"Scheduled Facebook persona generation for user {user_id} (20 minutes after onboarding)")
except Exception as e:
# Non-critical: log but don't fail onboarding completion
logger.warning(f"Failed to schedule Facebook persona generation for user {user_id}: {e}")
# Create OAuth token monitoring tasks for connected platforms
try:
from services.database import SessionLocal
db = SessionLocal()
try:
monitoring_tasks = create_oauth_monitoring_tasks(user_id, db)
logger.info(
f"Created {len(monitoring_tasks)} OAuth token monitoring tasks for user {user_id} "
f"on onboarding completion"
)
finally:
db.close()
except Exception as e:
# Non-critical: log but don't fail onboarding completion
logger.warning(f"Failed to create OAuth token monitoring tasks for user {user_id}: {e}")
# Create website analysis tasks for user's website and competitors
try:
from services.database import SessionLocal
from services.website_analysis_monitoring_service import create_website_analysis_tasks
db = SessionLocal()
try:
result = create_website_analysis_tasks(user_id=user_id, db=db)
if result.get('success'):
tasks_count = result.get('tasks_created', 0)
logger.info(
f"Created {tasks_count} website analysis tasks for user {user_id} "
f"on onboarding completion"
)
else:
error = result.get('error', 'Unknown error')
logger.warning(
f"Failed to create website analysis tasks for user {user_id}: {error}"
)
finally:
db.close()
except Exception as e:
# Non-critical: log but don't fail onboarding completion
logger.warning(f"Failed to create website analysis tasks for user {user_id}: {e}")
return {
"message": "Onboarding completed successfully",
"completed_at": datetime.now().isoformat(),
"completion_percentage": 100.0,
"persona_generated": persona_generated
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error completing onboarding: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
def _validate_required_steps_database(self, user_id: str) -> List[str]:
"""Validate that all required steps are completed using database only."""
missing_steps = []
try:
db = next(get_db())
db_service = OnboardingDatabaseService()
# Debug logging
logger.info(f"Validating steps for user {user_id}")
# Check each required step
for step_num in self.required_steps:
step_completed = False
if step_num == 1: # API Keys
api_keys = db_service.get_api_keys(user_id, db)
logger.info(f"Step 1 - API Keys: {api_keys}")
step_completed = any(v for v in api_keys.values() if v)
logger.info(f"Step 1 completed: {step_completed}")
elif step_num == 2: # Website Analysis
website = db_service.get_website_analysis(user_id, db)
logger.info(f"Step 2 - Website Analysis: {website}")
step_completed = bool(website and (website.get('website_url') or website.get('writing_style')))
logger.info(f"Step 2 completed: {step_completed}")
elif step_num == 3: # Research Preferences
research = db_service.get_research_preferences(user_id, db)
logger.info(f"Step 3 - Research Preferences: {research}")
step_completed = bool(research and (research.get('research_depth') or research.get('content_types')))
logger.info(f"Step 3 completed: {step_completed}")
elif step_num == 4: # Persona Generation
persona = db_service.get_persona_data(user_id, db)
logger.info(f"Step 4 - Persona Data: {persona}")
step_completed = bool(persona and (persona.get('corePersona') or persona.get('platformPersonas')))
logger.info(f"Step 4 completed: {step_completed}")
elif step_num == 5: # Integrations
# For now, consider this always completed if we reach this point
step_completed = True
logger.info(f"Step 5 completed: {step_completed}")
if not step_completed:
missing_steps.append(f"Step {step_num}")
logger.info(f"Missing steps: {missing_steps}")
return missing_steps
except Exception as e:
logger.error(f"Error validating required steps: {e}")
return ["Validation error"]
def _validate_required_steps(self, user_id: str, progress) -> List[str]:
"""Validate that all required steps are completed.
This method trusts the progress tracker, but also falls back to
database presence for Steps 2 and 3 so migration from file→DB
does not block completion.
"""
missing_steps = []
db = None
db_service = None
try:
db = next(get_db())
db_service = OnboardingDatabaseService(db)
except Exception:
db = None
db_service = None
logger.info(f"OnboardingCompletionService: Validating steps for user {user_id}")
logger.info(f"OnboardingCompletionService: Current step: {progress.current_step}")
logger.info(f"OnboardingCompletionService: Required steps: {self.required_steps}")
for step_num in self.required_steps:
step = progress.get_step_data(step_num)
logger.info(f"OnboardingCompletionService: Step {step_num} - status: {step.status if step else 'None'}")
if step and step.status in [StepStatus.COMPLETED, StepStatus.SKIPPED]:
logger.info(f"OnboardingCompletionService: Step {step_num} already completed/skipped")
continue
# DB-aware fallbacks for migration period
try:
if db_service:
if step_num == 1:
# Treat as completed if user has any API key in DB
keys = db_service.get_api_keys(user_id, db)
if keys and any(v for v in keys.values()):
try:
progress.mark_step_completed(1, {'source': 'db-fallback'})
except Exception:
pass
continue
if step_num == 2:
# Treat as completed if website analysis exists in DB
website = db_service.get_website_analysis(user_id, db)
if website and (website.get('website_url') or website.get('writing_style')):
# Optionally mark as completed in progress to keep state consistent
try:
progress.mark_step_completed(2, {'source': 'db-fallback'})
except Exception:
pass
continue
# Secondary fallback: research preferences captured style data
prefs = db_service.get_research_preferences(user_id, db)
if prefs and (prefs.get('writing_style') or prefs.get('content_characteristics')):
try:
progress.mark_step_completed(2, {'source': 'research-prefs-fallback'})
except Exception:
pass
continue
# Tertiary fallback: persona data created implies earlier steps done
persona = None
try:
persona = db_service.get_persona_data(user_id, db)
except Exception:
persona = None
if persona and persona.get('corePersona'):
try:
progress.mark_step_completed(2, {'source': 'persona-fallback'})
except Exception:
pass
continue
if step_num == 3:
# Treat as completed if research preferences exist in DB
prefs = db_service.get_research_preferences(user_id, db)
if prefs and prefs.get('research_depth'):
try:
progress.mark_step_completed(3, {'source': 'db-fallback'})
except Exception:
pass
continue
if step_num == 4:
# Treat as completed if persona data exists in DB
persona = None
try:
persona = db_service.get_persona_data(user_id, db)
except Exception:
persona = None
if persona and persona.get('corePersona'):
try:
progress.mark_step_completed(4, {'source': 'db-fallback'})
except Exception:
pass
continue
if step_num == 5:
# Treat as completed if integrations data exists in DB
# For now, we'll consider step 5 completed if the user has reached the final step
# This is a simplified approach - in the future, we could check for specific integration data
try:
# Check if user has completed previous steps and is on final step
if progress.current_step >= 6: # FinalStep is step 6
progress.mark_step_completed(5, {'source': 'final-step-fallback'})
continue
except Exception:
pass
except Exception:
# If DB check fails, fall back to progress status only
pass
if step:
missing_steps.append(step.title)
return missing_steps
def _validate_api_keys(self, user_id: str):
"""Validate that API keys are configured for the current user (DB-only)."""
try:
db = next(get_db())
db_service = OnboardingDatabaseService()
user_keys = db_service.get_api_keys(user_id, db)
if not user_keys or not any(v for v in user_keys.values()):
raise HTTPException(
status_code=400,
detail="Cannot complete onboarding. At least one AI provider API key must be configured in your account."
)
except HTTPException:
raise
except Exception:
raise HTTPException(
status_code=400,
detail="Cannot complete onboarding. API key validation failed."
)
async def _generate_persona_from_onboarding(self, user_id: str) -> bool:
"""Generate writing persona from onboarding data."""
try:
persona_service = PersonaAnalysisService()
# If a persona already exists for this user, skip regeneration
try:
existing = persona_service.get_user_personas(int(user_id))
if existing and len(existing) > 0:
logger.info("Persona already exists for user %s; skipping regeneration during completion", user_id)
return False
except Exception:
# Non-fatal; proceed to attempt generation
pass
# Generate persona for this user
persona_result = persona_service.generate_persona_from_onboarding(int(user_id))
if "error" not in persona_result:
logger.info(f"✅ Writing persona generated during onboarding completion: {persona_result.get('persona_id')}")
return True
else:
logger.warning(f"⚠️ Persona generation failed during onboarding: {persona_result['error']}")
return False
except Exception as e:
logger.warning(f"⚠️ Non-critical error generating persona during onboarding: {str(e)}")
return False