Add subcription feature
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -33,6 +33,10 @@ class User(Base):
|
||||
timezone = Column(String(50), default="Europe/Madrid")
|
||||
role = Column(String(20), nullable=False)
|
||||
|
||||
# Payment integration fields
|
||||
payment_customer_id = Column(String(255), nullable=True, index=True)
|
||||
default_payment_method_id = Column(String(255), nullable=True)
|
||||
|
||||
# REMOVED: All tenant relationships - these are handled by tenant service
|
||||
# No tenant_memberships, tenants relationships
|
||||
|
||||
|
||||
@@ -199,9 +199,17 @@ class OnboardingRepository:
|
||||
self,
|
||||
user_id: str,
|
||||
step_name: str,
|
||||
step_data: Dict[str, Any]
|
||||
step_data: Dict[str, Any],
|
||||
auto_commit: bool = True
|
||||
) -> UserOnboardingProgress:
|
||||
"""Save data for a specific step without marking it as completed"""
|
||||
"""Save data for a specific step without marking it as completed
|
||||
|
||||
Args:
|
||||
user_id: User ID
|
||||
step_name: Name of the step
|
||||
step_data: Data to save
|
||||
auto_commit: Whether to auto-commit (set to False when used within UnitOfWork)
|
||||
"""
|
||||
try:
|
||||
# Get existing step or create new one
|
||||
existing_step = await self.get_user_step(user_id, step_name)
|
||||
@@ -221,7 +229,12 @@ class OnboardingRepository:
|
||||
).returning(UserOnboardingProgress)
|
||||
|
||||
result = await self.db.execute(stmt)
|
||||
await self.db.commit()
|
||||
|
||||
if auto_commit:
|
||||
await self.db.commit()
|
||||
else:
|
||||
await self.db.flush()
|
||||
|
||||
return result.scalars().first()
|
||||
else:
|
||||
# Create new step with data but not completed
|
||||
@@ -229,12 +242,14 @@ class OnboardingRepository:
|
||||
user_id=user_id,
|
||||
step_name=step_name,
|
||||
completed=False,
|
||||
step_data=step_data
|
||||
step_data=step_data,
|
||||
auto_commit=auto_commit
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving step data for {step_name}, user {user_id}: {e}")
|
||||
await self.db.rollback()
|
||||
if auto_commit:
|
||||
await self.db.rollback()
|
||||
raise
|
||||
|
||||
async def get_step_data(self, user_id: str, step_name: str) -> Optional[Dict[str, Any]]:
|
||||
@@ -246,6 +261,26 @@ class OnboardingRepository:
|
||||
logger.error(f"Error getting step data for {step_name}, user {user_id}: {e}")
|
||||
return None
|
||||
|
||||
async def get_subscription_parameters(self, user_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get subscription parameters saved during onboarding for tenant creation"""
|
||||
try:
|
||||
step_data = await self.get_step_data(user_id, "user_registered")
|
||||
if step_data:
|
||||
# Extract subscription-related parameters
|
||||
subscription_params = {
|
||||
"subscription_plan": step_data.get("subscription_plan", "starter"),
|
||||
"billing_cycle": step_data.get("billing_cycle", "monthly"),
|
||||
"coupon_code": step_data.get("coupon_code"),
|
||||
"payment_method_id": step_data.get("payment_method_id"),
|
||||
"payment_customer_id": step_data.get("payment_customer_id"),
|
||||
"saved_at": step_data.get("saved_at")
|
||||
}
|
||||
return subscription_params
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting subscription parameters for user {user_id}: {e}")
|
||||
return None
|
||||
|
||||
async def get_completion_stats(self) -> Dict[str, Any]:
|
||||
"""Get completion statistics across all users"""
|
||||
try:
|
||||
|
||||
@@ -20,7 +20,8 @@ class UserRegistration(BaseModel):
|
||||
tenant_name: Optional[str] = Field(None, max_length=255)
|
||||
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")
|
||||
billing_cycle: Optional[str] = Field("monthly", description="Billing cycle (monthly, yearly)")
|
||||
coupon_code: Optional[str] = Field(None, description="Discount coupon code")
|
||||
payment_method_id: Optional[str] = Field(None, description="Stripe payment method ID")
|
||||
# GDPR Consent fields
|
||||
terms_accepted: Optional[bool] = Field(True, description="Accept terms of service")
|
||||
@@ -76,6 +77,7 @@ class TokenResponse(BaseModel):
|
||||
token_type: str = "bearer"
|
||||
expires_in: int = 3600 # seconds
|
||||
user: Optional[UserData] = None
|
||||
subscription_id: Optional[str] = Field(None, description="Subscription ID if created during registration")
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
@@ -92,7 +94,8 @@ class TokenResponse(BaseModel):
|
||||
"is_verified": False,
|
||||
"created_at": "2025-07-22T10:00:00Z",
|
||||
"role": "user"
|
||||
}
|
||||
},
|
||||
"subscription_id": "sub_1234567890"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +113,8 @@ class UserResponse(BaseModel):
|
||||
timezone: Optional[str] = None # ✅ Added missing field
|
||||
tenant_id: Optional[str] = None
|
||||
role: Optional[str] = "admin"
|
||||
payment_customer_id: Optional[str] = None # ✅ Added payment integration field
|
||||
default_payment_method_id: Optional[str] = None # ✅ Added payment integration field
|
||||
|
||||
class Config:
|
||||
from_attributes = True # ✅ Enable ORM mode for SQLAlchemy objects
|
||||
|
||||
@@ -21,6 +21,7 @@ from shared.database.unit_of_work import UnitOfWork
|
||||
from shared.database.transactions import transactional
|
||||
from shared.database.exceptions import DatabaseError, ValidationError, DuplicateRecordError
|
||||
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
@@ -169,9 +170,62 @@ class EnhancedAuthService:
|
||||
# Re-raise to ensure registration fails if consent can't be recorded
|
||||
raise
|
||||
|
||||
# Payment customer creation via tenant service
|
||||
# The auth service calls the tenant service to create payment customer
|
||||
# This maintains proper separation of concerns while providing seamless user experience
|
||||
|
||||
try:
|
||||
# Call tenant service to create payment customer
|
||||
from shared.clients.tenant_client import TenantServiceClient
|
||||
from app.core.config import settings
|
||||
|
||||
tenant_client = TenantServiceClient(settings)
|
||||
|
||||
# Prepare user data for tenant service
|
||||
user_data_for_tenant = {
|
||||
"user_id": str(new_user.id),
|
||||
"email": user_data.email,
|
||||
"full_name": user_data.full_name,
|
||||
"name": user_data.full_name
|
||||
}
|
||||
|
||||
# Call tenant service to create payment customer
|
||||
payment_result = await tenant_client.create_payment_customer(
|
||||
user_data_for_tenant,
|
||||
user_data.payment_method_id
|
||||
)
|
||||
|
||||
if payment_result and payment_result.get("success"):
|
||||
# Store payment customer ID from tenant service response
|
||||
new_user.payment_customer_id = payment_result.get("payment_customer_id")
|
||||
|
||||
logger.info("Payment customer created successfully via tenant service",
|
||||
user_id=new_user.id,
|
||||
payment_customer_id=new_user.payment_customer_id,
|
||||
payment_method_id=user_data.payment_method_id)
|
||||
else:
|
||||
logger.warning("Payment customer creation via tenant service returned no success",
|
||||
user_id=new_user.id,
|
||||
result=payment_result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Payment customer creation via tenant service failed",
|
||||
user_id=new_user.id,
|
||||
error=str(e))
|
||||
# Don't fail registration if payment customer creation fails
|
||||
# This allows users to register even if payment system is temporarily unavailable
|
||||
new_user.payment_customer_id = None
|
||||
|
||||
# Store payment method ID if provided (will be used by tenant service)
|
||||
if user_data.payment_method_id:
|
||||
new_user.default_payment_method_id = user_data.payment_method_id
|
||||
logger.info("Payment method ID stored for later use by tenant service",
|
||||
user_id=new_user.id,
|
||||
payment_method_id=user_data.payment_method_id)
|
||||
|
||||
# Store subscription plan selection in onboarding progress BEFORE committing
|
||||
# This ensures it's part of the same transaction
|
||||
if user_data.subscription_plan or user_data.use_trial or user_data.payment_method_id:
|
||||
if user_data.subscription_plan or user_data.payment_method_id or user_data.billing_cycle or user_data.coupon_code:
|
||||
try:
|
||||
from app.repositories.onboarding_repository import OnboardingRepository
|
||||
from app.models.onboarding import UserOnboardingProgress
|
||||
@@ -181,8 +235,10 @@ class EnhancedAuthService:
|
||||
plan_data = {
|
||||
"subscription_plan": user_data.subscription_plan or "starter",
|
||||
"subscription_tier": user_data.subscription_plan or "starter", # Store tier for enterprise onboarding logic
|
||||
"use_trial": user_data.use_trial or False,
|
||||
"billing_cycle": user_data.billing_cycle or "monthly",
|
||||
"coupon_code": user_data.coupon_code,
|
||||
"payment_method_id": user_data.payment_method_id,
|
||||
"payment_customer_id": new_user.payment_customer_id, # Now created via tenant service
|
||||
"saved_at": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
|
||||
@@ -197,11 +253,15 @@ class EnhancedAuthService:
|
||||
auto_commit=False
|
||||
)
|
||||
|
||||
logger.info("Subscription plan saved to onboarding progress",
|
||||
logger.info("Subscription plan and parameters saved to onboarding progress",
|
||||
user_id=new_user.id,
|
||||
plan=user_data.subscription_plan)
|
||||
plan=user_data.subscription_plan,
|
||||
billing_cycle=user_data.billing_cycle,
|
||||
coupon_code=user_data.coupon_code,
|
||||
payment_method_id=user_data.payment_method_id,
|
||||
payment_customer_id=new_user.payment_customer_id)
|
||||
except Exception as e:
|
||||
logger.error("Failed to save subscription plan to onboarding progress",
|
||||
logger.error("Failed to save subscription plan and parameters to onboarding progress",
|
||||
user_id=new_user.id,
|
||||
error=str(e))
|
||||
# Re-raise to ensure registration fails if onboarding data can't be saved
|
||||
@@ -730,6 +790,177 @@ class EnhancedAuthService:
|
||||
)
|
||||
|
||||
|
||||
async def create_subscription_via_tenant_service(
|
||||
self,
|
||||
user_id: str,
|
||||
plan_id: str,
|
||||
payment_method_id: str,
|
||||
billing_cycle: str,
|
||||
coupon_code: Optional[str] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Create a tenant-independent subscription via tenant service
|
||||
|
||||
This method calls the tenant service to create a subscription during user registration
|
||||
that is not linked to any tenant. The subscription will be linked to a tenant
|
||||
during the onboarding flow.
|
||||
|
||||
Args:
|
||||
user_id: User ID
|
||||
plan_id: Subscription plan ID
|
||||
payment_method_id: Payment method ID
|
||||
billing_cycle: Billing cycle (monthly/yearly)
|
||||
coupon_code: Optional coupon code
|
||||
|
||||
Returns:
|
||||
Dict with subscription creation results including:
|
||||
- success: boolean
|
||||
- subscription_id: string
|
||||
- customer_id: string
|
||||
- status: string
|
||||
- plan: string
|
||||
- billing_cycle: string
|
||||
Returns None if creation fails
|
||||
"""
|
||||
try:
|
||||
from shared.clients.tenant_client import TenantServiceClient
|
||||
from shared.config.base import BaseServiceSettings
|
||||
|
||||
# Get the base settings to create tenant client
|
||||
tenant_client = TenantServiceClient(BaseServiceSettings())
|
||||
|
||||
# Get user data for tenant service
|
||||
user_data = await self.get_user_data_for_tenant_service(user_id)
|
||||
|
||||
logger.info("Creating tenant-independent subscription via tenant service",
|
||||
user_id=user_id,
|
||||
plan_id=plan_id)
|
||||
|
||||
# Call tenant service using the new dedicated method
|
||||
result = await tenant_client.create_subscription_for_registration(
|
||||
user_data=user_data,
|
||||
plan_id=plan_id,
|
||||
payment_method_id=payment_method_id,
|
||||
billing_cycle=billing_cycle,
|
||||
coupon_code=coupon_code
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info("Tenant-independent subscription created successfully via tenant service",
|
||||
user_id=user_id,
|
||||
subscription_id=result.get('subscription_id'))
|
||||
return result
|
||||
else:
|
||||
logger.error("Tenant-independent subscription creation failed via tenant service",
|
||||
user_id=user_id)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to create tenant-independent subscription via tenant service",
|
||||
user_id=user_id,
|
||||
error=str(e))
|
||||
return None
|
||||
|
||||
async def get_user_data_for_tenant_service(self, user_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get user data formatted for tenant service calls
|
||||
|
||||
Args:
|
||||
user_id: User ID
|
||||
|
||||
Returns:
|
||||
Dict with user data including email, name, etc.
|
||||
"""
|
||||
try:
|
||||
# Get user from database
|
||||
async with self.database_manager.get_session() as db_session:
|
||||
async with UnitOfWork(db_session) as uow:
|
||||
user_repo = uow.register_repository("users", UserRepository, User)
|
||||
|
||||
user = await user_repo.get_by_id(user_id)
|
||||
|
||||
if not user:
|
||||
raise ValueError(f"User {user_id} not found")
|
||||
|
||||
return {
|
||||
"user_id": str(user.id),
|
||||
"email": user.email,
|
||||
"full_name": user.full_name,
|
||||
"name": user.full_name
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to get user data for tenant service",
|
||||
user_id=user_id,
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
async def save_subscription_to_onboarding_progress(
|
||||
self,
|
||||
user_id: str,
|
||||
subscription_id: str,
|
||||
registration_data: UserRegistration
|
||||
) -> None:
|
||||
"""
|
||||
Save subscription data to the user's onboarding progress
|
||||
|
||||
This method stores subscription information in the onboarding progress
|
||||
so it can be retrieved later during the tenant creation step.
|
||||
|
||||
Args:
|
||||
user_id: User ID
|
||||
subscription_id: Subscription ID created by tenant service
|
||||
registration_data: Original registration data including plan, payment method, etc.
|
||||
"""
|
||||
try:
|
||||
from app.repositories.onboarding_repository import OnboardingRepository
|
||||
from app.models.onboarding import UserOnboardingProgress
|
||||
|
||||
# Prepare subscription data to store
|
||||
subscription_data = {
|
||||
"subscription_id": subscription_id,
|
||||
"plan_id": registration_data.subscription_plan,
|
||||
"payment_method_id": registration_data.payment_method_id,
|
||||
"billing_cycle": registration_data.billing_cycle or "monthly",
|
||||
"coupon_code": registration_data.coupon_code,
|
||||
"created_at": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
|
||||
logger.info("Saving subscription data to onboarding progress",
|
||||
user_id=user_id,
|
||||
subscription_id=subscription_id)
|
||||
|
||||
# Save to onboarding progress
|
||||
async with self.database_manager.get_session() as db_session:
|
||||
async with UnitOfWork(db_session) as uow:
|
||||
onboarding_repo = uow.register_repository(
|
||||
"onboarding",
|
||||
OnboardingRepository,
|
||||
UserOnboardingProgress
|
||||
)
|
||||
|
||||
# Save or update the subscription step data
|
||||
await onboarding_repo.save_step_data(
|
||||
user_id=user_id,
|
||||
step_name="subscription",
|
||||
step_data=subscription_data,
|
||||
auto_commit=False
|
||||
)
|
||||
|
||||
# Commit the transaction
|
||||
await uow.commit()
|
||||
|
||||
logger.info("Subscription data saved successfully to onboarding progress",
|
||||
user_id=user_id,
|
||||
subscription_id=subscription_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to save subscription data to onboarding progress",
|
||||
user_id=user_id,
|
||||
subscription_id=subscription_id,
|
||||
error=str(e))
|
||||
# Don't raise - we don't want to fail the registration if this fails
|
||||
# The subscription was already created, so the user can still proceed
|
||||
|
||||
# Legacy compatibility - alias EnhancedAuthService as AuthService
|
||||
AuthService = EnhancedAuthService
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""add_payment_columns_to_users
|
||||
|
||||
Revision ID: 20260113_add_payment_columns
|
||||
Revises: 510cf1184e0b
|
||||
Create Date: 2026-01-13 13:30:00.000000+00:00
|
||||
|
||||
Add payment_customer_id and default_payment_method_id columns to users table
|
||||
to support payment integration.
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '20260113_add_payment_columns'
|
||||
down_revision: Union[str, None] = '510cf1184e0b'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add payment_customer_id column
|
||||
op.add_column('users',
|
||||
sa.Column('payment_customer_id', sa.String(length=255), nullable=True))
|
||||
|
||||
# Add default_payment_method_id column
|
||||
op.add_column('users',
|
||||
sa.Column('default_payment_method_id', sa.String(length=255), nullable=True))
|
||||
|
||||
# Create index for payment_customer_id
|
||||
op.create_index(op.f('ix_users_payment_customer_id'), 'users', ['payment_customer_id'], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop index first
|
||||
op.drop_index(op.f('ix_users_payment_customer_id'), table_name='users')
|
||||
|
||||
# Drop columns
|
||||
op.drop_column('users', 'default_payment_method_id')
|
||||
op.drop_column('users', 'payment_customer_id')
|
||||
Reference in New Issue
Block a user