Add subcription feature

This commit is contained in:
Urtzi Alfaro
2026-01-13 22:22:38 +01:00
parent b931a5c45e
commit 6ddf608d37
61 changed files with 7915 additions and 1238 deletions

View File

@@ -102,6 +102,135 @@ async def register(
detail="Registration failed"
)
@router.post("/api/v1/auth/register-with-subscription", response_model=TokenResponse)
@track_execution_time("enhanced_registration_with_subscription_duration_seconds", "auth-service")
async def register_with_subscription(
user_data: UserRegistration,
request: Request,
auth_service: EnhancedAuthService = Depends(get_auth_service)
):
"""
Register new user and create subscription in one call
This endpoint implements the new registration flow where:
1. User is created
2. Payment customer is created via tenant service
3. Tenant-independent subscription is created via tenant service
4. Subscription data is stored in onboarding progress
5. User is authenticated and returned with tokens
The subscription will be linked to a tenant during the onboarding flow.
"""
metrics = get_metrics_collector(request)
logger.info("Registration with subscription attempt using new architecture",
email=user_data.email)
try:
# Enhanced input validation
if not user_data.email or not user_data.email.strip():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email is required"
)
if not user_data.password or len(user_data.password) < 8:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password must be at least 8 characters long"
)
if not user_data.full_name or not user_data.full_name.strip():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Full name is required"
)
# Step 1: Register user using enhanced service
logger.info("Step 1: Creating user", email=user_data.email)
result = await auth_service.register_user(user_data)
user_id = result.user.id
logger.info("User created successfully", user_id=user_id)
# Step 2: Create subscription via tenant service (if subscription data provided)
subscription_id = None
if user_data.subscription_plan and user_data.payment_method_id:
logger.info("Step 2: Creating tenant-independent subscription",
user_id=user_id,
plan=user_data.subscription_plan)
subscription_result = await auth_service.create_subscription_via_tenant_service(
user_id=user_id,
plan_id=user_data.subscription_plan,
payment_method_id=user_data.payment_method_id,
billing_cycle=user_data.billing_cycle or "monthly",
coupon_code=user_data.coupon_code
)
if subscription_result:
subscription_id = subscription_result.get("subscription_id")
logger.info("Tenant-independent subscription created successfully",
user_id=user_id,
subscription_id=subscription_id)
# Step 3: Store subscription data in onboarding progress
logger.info("Step 3: Storing subscription data in onboarding progress",
user_id=user_id)
# Update onboarding progress with subscription data
await auth_service.save_subscription_to_onboarding_progress(
user_id=user_id,
subscription_id=subscription_id,
registration_data=user_data
)
logger.info("Subscription data stored in onboarding progress",
user_id=user_id)
else:
logger.warning("Subscription creation failed, but user registration succeeded",
user_id=user_id)
else:
logger.info("No subscription data provided, skipping subscription creation",
user_id=user_id)
# Record successful registration
if metrics:
metrics.increment_counter("enhanced_registration_with_subscription_total", labels={"status": "success"})
logger.info("Registration with subscription completed successfully using new architecture",
user_id=user_id,
email=user_data.email,
subscription_id=subscription_id)
# Add subscription_id to the response
result.subscription_id = subscription_id
return result
except HTTPException as e:
if metrics:
error_type = "validation_error" if e.status_code == 400 else "conflict" if e.status_code == 409 else "failed"
metrics.increment_counter("enhanced_registration_with_subscription_total", labels={"status": error_type})
logger.warning("Registration with subscription failed using new architecture",
email=user_data.email,
error=e.detail)
raise
except Exception as e:
if metrics:
metrics.increment_counter("enhanced_registration_with_subscription_total", labels={"status": "error"})
logger.error("Registration with subscription system error using new architecture",
email=user_data.email,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Registration with subscription failed"
)
@router.post("/api/v1/auth/login", response_model=TokenResponse)
@track_execution_time("enhanced_login_duration_seconds", "auth-service")

View File

@@ -1044,4 +1044,110 @@ async def delete_step_draft(
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete step draft"
)
@router.get("/api/v1/auth/me/onboarding/subscription-parameters", response_model=Dict[str, Any])
async def get_subscription_parameters(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Get subscription parameters saved during onboarding for tenant creation
Returns all parameters needed for subscription processing: plan, billing cycle, coupon, etc.
"""
try:
user_id = current_user["user_id"]
is_demo = current_user.get("is_demo", False)
# DEMO FIX: Demo users get default subscription parameters
if is_demo or user_id.startswith("demo-user-"):
logger.info(f"Demo user {user_id} requesting subscription parameters - returning demo defaults")
return {
"subscription_plan": "professional",
"billing_cycle": "monthly",
"coupon_code": "DEMO2025",
"payment_method_id": "pm_demo_test_123",
"payment_customer_id": "cus_demo_test_123", # Demo payment customer ID
"saved_at": datetime.now(timezone.utc).isoformat(),
"demo_mode": True
}
# Get subscription parameters from onboarding progress
from app.repositories.onboarding_repository import OnboardingRepository
onboarding_repo = OnboardingRepository(db)
subscription_params = await onboarding_repo.get_subscription_parameters(user_id)
if not subscription_params:
logger.warning(f"No subscription parameters found for user {user_id} - returning defaults")
return {
"subscription_plan": "starter",
"billing_cycle": "monthly",
"coupon_code": None,
"payment_method_id": None,
"payment_customer_id": None,
"saved_at": datetime.now(timezone.utc).isoformat()
}
logger.info(f"Retrieved subscription parameters for user {user_id}",
subscription_plan=subscription_params["subscription_plan"],
billing_cycle=subscription_params["billing_cycle"],
coupon_code=subscription_params["coupon_code"])
return subscription_params
except Exception as e:
logger.error(f"Error getting subscription parameters for user {current_user.get('user_id')}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve subscription parameters"
)
@router.get("/api/v1/auth/users/{user_id}/onboarding/subscription-parameters", response_model=Dict[str, Any])
async def get_user_subscription_parameters(
user_id: str,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Get subscription parameters for a specific user (admin/service access)
"""
try:
# Check permissions - only admins and services can access other users' data
requester_id = current_user["user_id"]
requester_roles = current_user.get("roles", [])
is_service = current_user.get("is_service", False)
if not is_service and "super_admin" not in requester_roles and requester_id != user_id:
logger.warning(f"Unauthorized access attempt to user {user_id} subscription parameters by {requester_id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to access other users' subscription parameters"
)
# Get subscription parameters from onboarding progress
from app.repositories.onboarding_repository import OnboardingRepository
onboarding_repo = OnboardingRepository(db)
subscription_params = await onboarding_repo.get_subscription_parameters(user_id)
if not subscription_params:
logger.warning(f"No subscription parameters found for user {user_id} - returning defaults")
return {
"subscription_plan": "starter",
"billing_cycle": "monthly",
"coupon_code": None,
"payment_method_id": None,
"payment_customer_id": None,
"saved_at": datetime.now(timezone.utc).isoformat()
}
logger.info(f"Retrieved subscription parameters for user {user_id} by {requester_id}",
subscription_plan=subscription_params["subscription_plan"])
return subscription_params
except Exception as e:
logger.error(f"Error getting subscription parameters for user {user_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve subscription parameters"
)

View File

@@ -2,7 +2,7 @@
User management API routes
"""
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks, Path
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks, Path, Body
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Dict, Any
import structlog
@@ -223,7 +223,9 @@ async def get_user_by_id(
created_at=user.created_at,
last_login=user.last_login,
role=user.role,
tenant_id=None
tenant_id=None,
payment_customer_id=user.payment_customer_id,
default_payment_method_id=user.default_payment_method_id
)
except HTTPException:
@@ -481,3 +483,71 @@ async def get_user_activity(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get user activity information"
)
@router.patch("/api/v1/auth/users/{user_id}/tenant")
async def update_user_tenant(
user_id: str = Path(..., description="User ID"),
tenant_data: Dict[str, Any] = Body(..., description="Tenant data containing tenant_id"),
db: AsyncSession = Depends(get_db)
):
"""
Update user's tenant_id after tenant registration
This endpoint is called by the tenant service after a user creates their tenant.
It links the user to their newly created tenant.
"""
try:
# Log the incoming request data for debugging
logger.debug("Received tenant update request",
user_id=user_id,
tenant_data=tenant_data)
tenant_id = tenant_data.get("tenant_id")
if not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="tenant_id is required"
)
logger.info("Updating user tenant_id",
user_id=user_id,
tenant_id=tenant_id)
user_service = UserService(db)
user = await user_service.get_user_by_id(uuid.UUID(user_id))
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# Update user's tenant_id
user.tenant_id = uuid.UUID(tenant_id)
user.updated_at = datetime.now(timezone.utc)
await db.commit()
await db.refresh(user)
logger.info("Successfully updated user tenant_id",
user_id=user_id,
tenant_id=tenant_id)
return {
"success": True,
"user_id": str(user.id),
"tenant_id": str(user.tenant_id)
}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to update user tenant_id",
user_id=user_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update user tenant_id"
)