Coverage for integrations / social / onboarding_service.py: 68.0%
100 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2HevolveSocial - Onboarding Service
3Progressive 7-step onboarding with auto-advancement and rewards.
4"""
5import json
6import logging
7from datetime import datetime
8from typing import Optional, Dict, List
10from sqlalchemy.orm import Session
12from .models import User, OnboardingProgress
13from .resonance_engine import ResonanceService
15logger = logging.getLogger('hevolve_social')
17ONBOARDING_STEPS = [
18 {'key': 'welcome', 'title': 'Welcome', 'reward_type': 'pulse', 'reward_amount': 50},
19 {'key': 'first_follow', 'title': 'Follow Someone', 'reward_type': 'pulse', 'reward_amount': 25},
20 {'key': 'join_community', 'title': 'Join a Community', 'reward_type': 'pulse', 'reward_amount': 50},
21 {'key': 'first_vote', 'title': 'Vote on a Post', 'reward_type': 'spark', 'reward_amount': 25},
22 {'key': 'first_comment', 'title': 'Leave a Comment', 'reward_type': 'spark', 'reward_amount': 25},
23 {'key': 'first_post', 'title': 'Create a Post', 'reward_type': 'xp', 'reward_amount': 100},
24 {'key': 'explore_agents', 'title': 'Explore Agents', 'reward_type': 'xp', 'reward_amount': 100},
25 {'key': 'invite_friends', 'title': 'Invite Friends', 'reward_type': 'spark', 'reward_amount': 100},
26]
29class OnboardingService:
31 @staticmethod
32 def get_or_create_progress(db: Session, user_id: str) -> OnboardingProgress:
33 progress = db.query(OnboardingProgress).filter_by(user_id=user_id).first()
34 if not progress:
35 progress = OnboardingProgress(
36 user_id=user_id,
37 steps_completed='{}',
38 current_step='welcome',
39 )
40 db.add(progress)
41 db.flush()
42 return progress
44 @staticmethod
45 def get_progress(db: Session, user_id: str) -> Dict:
46 progress = OnboardingService.get_or_create_progress(db, user_id)
47 try:
48 steps = json.loads(progress.steps_completed) if progress.steps_completed else {}
49 except (json.JSONDecodeError, TypeError):
50 steps = {}
52 total = len(ONBOARDING_STEPS)
53 done = sum(1 for s in ONBOARDING_STEPS if steps.get(s['key']))
55 return {
56 'user_id': user_id,
57 'steps_completed': steps,
58 'current_step': progress.current_step,
59 'completed_at': progress.completed_at.isoformat() if progress.completed_at else None,
60 'tutorial_dismissed': progress.tutorial_dismissed,
61 'total_steps': total,
62 'done_steps': done,
63 'steps': [{
64 'key': s['key'],
65 'title': s['title'],
66 'completed': bool(steps.get(s['key'])),
67 'reward_type': s['reward_type'],
68 'reward_amount': s['reward_amount'],
69 } for s in ONBOARDING_STEPS],
70 }
72 @staticmethod
73 def complete_step(db: Session, user_id: str, step_key: str) -> Dict:
74 """Mark an onboarding step as complete and award rewards."""
75 progress = OnboardingService.get_or_create_progress(db, user_id)
76 try:
77 steps = json.loads(progress.steps_completed) if progress.steps_completed else {}
78 except (json.JSONDecodeError, TypeError):
79 steps = {}
81 if steps.get(step_key):
82 return {'already_completed': True, 'step': step_key}
84 # Find step definition
85 step_def = next((s for s in ONBOARDING_STEPS if s['key'] == step_key), None)
86 if not step_def:
87 return {'error': 'Unknown step'}
89 steps[step_key] = datetime.utcnow().isoformat()
90 progress.steps_completed = json.dumps(steps)
92 # Track specific timestamps
93 timestamp_map = {
94 'first_post': 'first_post_at',
95 'first_comment': 'first_comment_at',
96 'first_vote': 'first_vote_at',
97 'first_follow': 'first_follow_at',
98 'join_community': 'first_community_join_at',
99 }
100 attr = timestamp_map.get(step_key)
101 if attr and hasattr(progress, attr) and not getattr(progress, attr):
102 setattr(progress, attr, datetime.utcnow())
104 # Award reward
105 reward_result = {}
106 rtype = step_def['reward_type']
107 ramount = step_def['reward_amount']
108 if rtype == 'pulse':
109 ResonanceService.award_pulse(db, user_id, ramount, 'onboarding', step_key,
110 f'Onboarding: {step_def["title"]}')
111 reward_result = {'pulse': ramount}
112 elif rtype == 'spark':
113 ResonanceService.award_spark(db, user_id, ramount, 'onboarding', step_key,
114 f'Onboarding: {step_def["title"]}')
115 reward_result = {'spark': ramount}
116 elif rtype == 'xp':
117 ResonanceService.award_xp(db, user_id, ramount, 'onboarding', step_key,
118 f'Onboarding: {step_def["title"]}')
119 reward_result = {'xp': ramount}
121 # Advance current step
122 all_done = all(steps.get(s['key']) for s in ONBOARDING_STEPS)
123 if all_done:
124 progress.completed_at = datetime.utcnow()
125 progress.current_step = 'complete'
126 # Bonus for completing all steps
127 ResonanceService.award_spark(db, user_id, 50, 'onboarding', 'complete',
128 'Onboarding complete bonus')
129 reward_result['completion_bonus'] = {'spark': 50}
130 else:
131 # Find next incomplete step
132 for s in ONBOARDING_STEPS:
133 if not steps.get(s['key']):
134 progress.current_step = s['key']
135 break
137 return {
138 'step': step_key,
139 'completed': True,
140 'rewards': reward_result,
141 'all_complete': all_done,
142 'next_step': progress.current_step,
143 }
145 @staticmethod
146 def dismiss(db: Session, user_id: str) -> bool:
147 progress = OnboardingService.get_or_create_progress(db, user_id)
148 progress.tutorial_dismissed = True
149 return True
151 @staticmethod
152 def get_suggestion(db: Session, user_id: str) -> Optional[Dict]:
153 """Get the next suggested action for onboarding."""
154 progress = OnboardingService.get_or_create_progress(db, user_id)
155 if progress.completed_at or progress.tutorial_dismissed:
156 return None
158 try:
159 steps = json.loads(progress.steps_completed) if progress.steps_completed else {}
160 except (json.JSONDecodeError, TypeError):
161 steps = {}
163 for s in ONBOARDING_STEPS:
164 if not steps.get(s['key']):
165 return {
166 'step': s['key'],
167 'title': s['title'],
168 'reward_type': s['reward_type'],
169 'reward_amount': s['reward_amount'],
170 }
171 return None
173 @staticmethod
174 def auto_advance(db: Session, user_id: str, action: str):
175 """Auto-advance onboarding based on natural user actions.
176 Called by service hooks after relevant actions."""
177 action_to_step = {
178 'follow': 'first_follow',
179 'join_community': 'join_community',
180 'vote': 'first_vote',
181 'comment': 'first_comment',
182 'post': 'first_post',
183 'view_agents': 'explore_agents',
184 'share_referral': 'invite_friends',
185 }
186 step_key = action_to_step.get(action)
187 if step_key:
188 progress = db.query(OnboardingProgress).filter_by(user_id=user_id).first()
189 if progress and not progress.completed_at:
190 try:
191 steps = json.loads(progress.steps_completed) if progress.steps_completed else {}
192 except (json.JSONDecodeError, TypeError):
193 steps = {}
194 if not steps.get(step_key):
195 OnboardingService.complete_step(db, user_id, step_key)