Fix onboarding process not getting the subcription plan
This commit is contained in:
@@ -28,6 +28,9 @@ export interface UserRegistration {
|
|||||||
phone?: string;
|
phone?: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
|
subscription_plan?: string;
|
||||||
|
use_trial?: boolean;
|
||||||
|
payment_method_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserLogin {
|
export interface UserLogin {
|
||||||
|
|||||||
@@ -104,10 +104,14 @@ export const OnboardingWizard: React.FC = () => {
|
|||||||
console.log('🔄 Auto-completing user_registered step for new user...');
|
console.log('🔄 Auto-completing user_registered step for new user...');
|
||||||
setAutoCompletionAttempted(true);
|
setAutoCompletionAttempted(true);
|
||||||
|
|
||||||
|
// Merge with any existing data (e.g., subscription_plan from registration)
|
||||||
|
const existingData = userRegisteredStep?.data || {};
|
||||||
|
|
||||||
markStepCompleted.mutate({
|
markStepCompleted.mutate({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
stepName: 'user_registered',
|
stepName: 'user_registered',
|
||||||
data: {
|
data: {
|
||||||
|
...existingData, // Preserve existing data like subscription_plan
|
||||||
auto_completed: true,
|
auto_completed: true,
|
||||||
completed_at: new Date().toISOString(),
|
completed_at: new Date().toISOString(),
|
||||||
source: 'onboarding_wizard_auto_completion'
|
source: 'onboarding_wizard_auto_completion'
|
||||||
|
|||||||
@@ -29,7 +29,15 @@ export interface AuthState {
|
|||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
login: (email: string, password: string) => Promise<void>;
|
login: (email: string, password: string) => Promise<void>;
|
||||||
register: (userData: { email: string; password: string; full_name: string; tenant_name?: string }) => Promise<void>;
|
register: (userData: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
full_name: string;
|
||||||
|
tenant_name?: string;
|
||||||
|
subscription_plan?: string;
|
||||||
|
use_trial?: boolean;
|
||||||
|
payment_method_id?: string;
|
||||||
|
}) => Promise<void>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
refreshAuth: () => Promise<void>;
|
refreshAuth: () => Promise<void>;
|
||||||
updateUser: (updates: Partial<User>) => void;
|
updateUser: (updates: Partial<User>) => void;
|
||||||
@@ -93,7 +101,15 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
register: async (userData: { email: string; password: string; full_name: string; tenant_name?: string }) => {
|
register: async (userData: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
full_name: string;
|
||||||
|
tenant_name?: string;
|
||||||
|
subscription_plan?: string;
|
||||||
|
use_trial?: boolean;
|
||||||
|
payment_method_id?: string;
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
|
|
||||||
|
|||||||
@@ -373,6 +373,39 @@ async def get_user_progress(
|
|||||||
detail="Failed to get onboarding progress"
|
detail="Failed to get onboarding progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@router.get("/{user_id}/onboarding/progress", response_model=UserProgress)
|
||||||
|
async def get_user_progress_by_id(
|
||||||
|
user_id: str,
|
||||||
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get onboarding progress for a specific user
|
||||||
|
Available for service-to-service calls and admin users
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Allow service tokens or admin users
|
||||||
|
user_type = current_user.get("type", "user")
|
||||||
|
user_role = current_user.get("role", "user")
|
||||||
|
|
||||||
|
if user_type != "service" and user_role not in ["admin", "super_admin"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Insufficient permissions to access other users' onboarding progress"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
onboarding_service = OnboardingService(db)
|
||||||
|
progress = await onboarding_service.get_user_progress(user_id)
|
||||||
|
return progress
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Get onboarding progress error for user {user_id}: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Failed to get onboarding progress"
|
||||||
|
)
|
||||||
|
|
||||||
@router.put("/me/onboarding/step", response_model=UserProgress)
|
@router.put("/me/onboarding/step", response_model=UserProgress)
|
||||||
async def update_onboarding_step(
|
async def update_onboarding_step(
|
||||||
update_request: UpdateStepRequest,
|
update_request: UpdateStepRequest,
|
||||||
|
|||||||
@@ -179,6 +179,57 @@ class OnboardingRepository:
|
|||||||
await self.db.rollback()
|
await self.db.rollback()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def save_step_data(
|
||||||
|
self,
|
||||||
|
user_id: str,
|
||||||
|
step_name: str,
|
||||||
|
step_data: Dict[str, Any]
|
||||||
|
) -> UserOnboardingProgress:
|
||||||
|
"""Save data for a specific step without marking it as completed"""
|
||||||
|
try:
|
||||||
|
# Get existing step or create new one
|
||||||
|
existing_step = await self.get_user_step(user_id, step_name)
|
||||||
|
|
||||||
|
if existing_step:
|
||||||
|
# Update existing step data (merge with existing data)
|
||||||
|
merged_data = {**(existing_step.step_data or {}), **step_data}
|
||||||
|
|
||||||
|
stmt = update(UserOnboardingProgress).where(
|
||||||
|
and_(
|
||||||
|
UserOnboardingProgress.user_id == user_id,
|
||||||
|
UserOnboardingProgress.step_name == step_name
|
||||||
|
)
|
||||||
|
).values(
|
||||||
|
step_data=merged_data,
|
||||||
|
updated_at=datetime.now(timezone.utc)
|
||||||
|
).returning(UserOnboardingProgress)
|
||||||
|
|
||||||
|
result = await self.db.execute(stmt)
|
||||||
|
await self.db.commit()
|
||||||
|
return result.scalars().first()
|
||||||
|
else:
|
||||||
|
# Create new step with data but not completed
|
||||||
|
return await self.upsert_user_step(
|
||||||
|
user_id=user_id,
|
||||||
|
step_name=step_name,
|
||||||
|
completed=False,
|
||||||
|
step_data=step_data
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error saving step data for {step_name}, user {user_id}: {e}")
|
||||||
|
await self.db.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def get_step_data(self, user_id: str, step_name: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get data for a specific step"""
|
||||||
|
try:
|
||||||
|
step = await self.get_user_step(user_id, step_name)
|
||||||
|
return step.step_data if step else None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting step data for {step_name}, user {user_id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
async def get_completion_stats(self) -> Dict[str, Any]:
|
async def get_completion_stats(self) -> Dict[str, Any]:
|
||||||
"""Get completion statistics across all users"""
|
"""Get completion statistics across all users"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ class UserRegistration(BaseModel):
|
|||||||
full_name: str = Field(..., min_length=1, max_length=255)
|
full_name: str = Field(..., min_length=1, max_length=255)
|
||||||
tenant_name: Optional[str] = Field(None, max_length=255)
|
tenant_name: Optional[str] = Field(None, max_length=255)
|
||||||
role: Optional[str] = Field("admin", pattern=r'^(user|admin|manager|super_admin)$')
|
role: Optional[str] = Field("admin", pattern=r'^(user|admin|manager|super_admin)$')
|
||||||
|
subscription_plan: Optional[str] = Field("starter", description="Selected subscription plan (starter, professional, enterprise)")
|
||||||
|
use_trial: Optional[bool] = Field(False, description="Whether to use trial period")
|
||||||
|
payment_method_id: Optional[str] = Field(None, description="Stripe payment method ID")
|
||||||
|
|
||||||
class UserLogin(BaseModel):
|
class UserLogin(BaseModel):
|
||||||
"""User login request"""
|
"""User login request"""
|
||||||
|
|||||||
@@ -112,6 +112,32 @@ class EnhancedAuthService:
|
|||||||
# Commit transaction
|
# Commit transaction
|
||||||
await uow.commit()
|
await uow.commit()
|
||||||
|
|
||||||
|
# Store subscription plan selection in onboarding progress for later retrieval
|
||||||
|
if user_data.subscription_plan or user_data.use_trial or user_data.payment_method_id:
|
||||||
|
try:
|
||||||
|
from app.repositories.onboarding_repository import OnboardingRepository
|
||||||
|
from app.models.onboarding import UserOnboardingProgress
|
||||||
|
|
||||||
|
onboarding_repo = OnboardingRepository(db_session)
|
||||||
|
plan_data = {
|
||||||
|
"subscription_plan": user_data.subscription_plan or "starter",
|
||||||
|
"use_trial": user_data.use_trial or False,
|
||||||
|
"payment_method_id": user_data.payment_method_id,
|
||||||
|
"saved_at": datetime.now(timezone.utc).isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
await onboarding_repo.save_step_data(
|
||||||
|
str(new_user.id),
|
||||||
|
"user_registered",
|
||||||
|
plan_data
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Subscription plan saved to onboarding progress",
|
||||||
|
user_id=new_user.id,
|
||||||
|
plan=user_data.subscription_plan)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Failed to save subscription plan to onboarding progress", error=str(e))
|
||||||
|
|
||||||
# Publish registration event (non-blocking)
|
# Publish registration event (non-blocking)
|
||||||
try:
|
try:
|
||||||
await publish_user_registered({
|
await publish_user_registered({
|
||||||
@@ -119,7 +145,8 @@ class EnhancedAuthService:
|
|||||||
"email": new_user.email,
|
"email": new_user.email,
|
||||||
"full_name": new_user.full_name,
|
"full_name": new_user.full_name,
|
||||||
"role": new_user.role,
|
"role": new_user.role,
|
||||||
"registered_at": datetime.now(timezone.utc).isoformat()
|
"registered_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"subscription_plan": user_data.subscription_plan or "starter"
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Failed to publish registration event", error=str(e))
|
logger.warning("Failed to publish registration event", error=str(e))
|
||||||
|
|||||||
@@ -84,15 +84,39 @@ class EnhancedTenantService:
|
|||||||
|
|
||||||
owner_membership = await member_repo.create_membership(membership_data)
|
owner_membership = await member_repo.create_membership(membership_data)
|
||||||
|
|
||||||
# Create starter subscription
|
# Get subscription plan from user's registration using standardized auth client
|
||||||
|
selected_plan = "starter" # Default fallback
|
||||||
|
try:
|
||||||
|
from shared.clients.auth_client import AuthServiceClient
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
auth_client = AuthServiceClient(settings)
|
||||||
|
selected_plan = await auth_client.get_subscription_plan_from_registration(owner_id)
|
||||||
|
|
||||||
|
logger.info("Retrieved subscription plan from registration",
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
owner_id=owner_id,
|
||||||
|
plan=selected_plan)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Could not retrieve subscription plan from auth service, using default",
|
||||||
|
error=str(e),
|
||||||
|
owner_id=owner_id,
|
||||||
|
default_plan=selected_plan)
|
||||||
|
|
||||||
|
# Create subscription with selected or default plan
|
||||||
subscription_data = {
|
subscription_data = {
|
||||||
"tenant_id": str(tenant.id),
|
"tenant_id": str(tenant.id),
|
||||||
"plan": "starter",
|
"plan": selected_plan,
|
||||||
"status": "active"
|
"status": "active"
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription = await subscription_repo.create_subscription(subscription_data)
|
subscription = await subscription_repo.create_subscription(subscription_data)
|
||||||
|
|
||||||
|
logger.info("Subscription created",
|
||||||
|
tenant_id=tenant.id,
|
||||||
|
plan=selected_plan)
|
||||||
|
|
||||||
# Commit the transaction
|
# Commit the transaction
|
||||||
await uow.commit()
|
await uow.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Provides easy access to all service clients
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .base_service_client import BaseServiceClient, ServiceAuthenticator
|
from .base_service_client import BaseServiceClient, ServiceAuthenticator
|
||||||
|
from .auth_client import AuthServiceClient
|
||||||
from .training_client import TrainingServiceClient
|
from .training_client import TrainingServiceClient
|
||||||
from .sales_client import SalesServiceClient
|
from .sales_client import SalesServiceClient
|
||||||
from .external_client import ExternalServiceClient
|
from .external_client import ExternalServiceClient
|
||||||
@@ -210,6 +211,7 @@ def get_service_clients(config: BaseServiceSettings = None, service_name: str =
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'BaseServiceClient',
|
'BaseServiceClient',
|
||||||
'ServiceAuthenticator',
|
'ServiceAuthenticator',
|
||||||
|
'AuthServiceClient',
|
||||||
'TrainingServiceClient',
|
'TrainingServiceClient',
|
||||||
'SalesServiceClient',
|
'SalesServiceClient',
|
||||||
'ExternalServiceClient',
|
'ExternalServiceClient',
|
||||||
|
|||||||
134
shared/clients/auth_client.py
Normal file
134
shared/clients/auth_client.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# shared/clients/auth_client.py
|
||||||
|
"""
|
||||||
|
Auth Service Client for Inter-Service Communication
|
||||||
|
Provides methods to interact with the authentication/onboarding service
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
import structlog
|
||||||
|
|
||||||
|
from shared.clients.base_service_client import BaseServiceClient
|
||||||
|
from shared.config.base import BaseServiceSettings
|
||||||
|
|
||||||
|
logger = structlog.get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class AuthServiceClient(BaseServiceClient):
|
||||||
|
"""Client for interacting with the Auth Service"""
|
||||||
|
|
||||||
|
def __init__(self, config: BaseServiceSettings):
|
||||||
|
super().__init__("auth", config)
|
||||||
|
|
||||||
|
def get_service_base_path(self) -> str:
|
||||||
|
"""Return the base path for auth service APIs"""
|
||||||
|
return "/api/v1/users"
|
||||||
|
|
||||||
|
async def get_user_onboarding_progress(self, user_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get user's onboarding progress including step data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: User ID to fetch progress for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with user progress including steps with data, or None if failed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Use the service endpoint that accepts user_id as parameter
|
||||||
|
result = await self.get(f"/{user_id}/onboarding/progress")
|
||||||
|
|
||||||
|
if result:
|
||||||
|
logger.info("Retrieved user onboarding progress",
|
||||||
|
user_id=user_id,
|
||||||
|
current_step=result.get("current_step"))
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
logger.warning("No onboarding progress found",
|
||||||
|
user_id=user_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to get user onboarding progress",
|
||||||
|
user_id=user_id,
|
||||||
|
error=str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_user_step_data(self, user_id: str, step_name: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get data for a specific onboarding step
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: User ID
|
||||||
|
step_name: Name of the step (e.g., "user_registered")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Step data dictionary or None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
progress = await self.get_user_onboarding_progress(user_id)
|
||||||
|
|
||||||
|
if not progress:
|
||||||
|
logger.warning("No progress data returned",
|
||||||
|
user_id=user_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.debug("Retrieved progress data",
|
||||||
|
user_id=user_id,
|
||||||
|
steps_count=len(progress.get("steps", [])),
|
||||||
|
current_step=progress.get("current_step"))
|
||||||
|
|
||||||
|
# Find the specific step
|
||||||
|
for step in progress.get("steps", []):
|
||||||
|
if step.get("step_name") == step_name:
|
||||||
|
step_data = step.get("data", {})
|
||||||
|
logger.info("Found step data",
|
||||||
|
user_id=user_id,
|
||||||
|
step_name=step_name,
|
||||||
|
data_keys=list(step_data.keys()) if step_data else [],
|
||||||
|
has_subscription_plan="subscription_plan" in step_data)
|
||||||
|
return step_data
|
||||||
|
|
||||||
|
logger.warning("Step not found in progress",
|
||||||
|
user_id=user_id,
|
||||||
|
step_name=step_name,
|
||||||
|
available_steps=[s.get("step_name") for s in progress.get("steps", [])])
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to get step data",
|
||||||
|
user_id=user_id,
|
||||||
|
step_name=step_name,
|
||||||
|
error=str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_subscription_plan_from_registration(self, user_id: str) -> str:
|
||||||
|
"""
|
||||||
|
Get the subscription plan selected during user registration
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: User ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plan name (e.g., "starter", "professional", "enterprise") or "starter" as default
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
step_data = await self.get_user_step_data(user_id, "user_registered")
|
||||||
|
|
||||||
|
if step_data and "subscription_plan" in step_data:
|
||||||
|
plan = step_data["subscription_plan"]
|
||||||
|
logger.info("Retrieved subscription plan from registration",
|
||||||
|
user_id=user_id,
|
||||||
|
plan=plan)
|
||||||
|
return plan
|
||||||
|
else:
|
||||||
|
logger.info("No subscription plan in registration data, using default",
|
||||||
|
user_id=user_id,
|
||||||
|
default_plan="starter")
|
||||||
|
return "starter"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Failed to retrieve subscription plan, using default",
|
||||||
|
user_id=user_id,
|
||||||
|
error=str(e),
|
||||||
|
default_plan="starter")
|
||||||
|
return "starter"
|
||||||
Reference in New Issue
Block a user