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

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