""" 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