Add subcription feature 3

This commit is contained in:
Urtzi Alfaro
2026-01-15 20:45:49 +01:00
parent a4c3b7da3f
commit b674708a4c
83 changed files with 9451 additions and 6828 deletions

View File

@@ -17,6 +17,8 @@ from app.services.tenant_service import EnhancedTenantService
from app.core.config import settings
from shared.database.exceptions import DatabaseError, ValidationError
from shared.database.base import create_database_manager
from shared.exceptions.payment_exceptions import SubscriptionUpdateFailed
from shared.exceptions.subscription_exceptions import SubscriptionNotFound
logger = structlog.get_logger()
@@ -69,7 +71,10 @@ class SubscriptionOrchestrationService:
logger.info("Creating customer in payment provider",
tenant_id=tenant_id, email=user_data.get('email'))
customer = await self.payment_service.create_customer(user_data)
email = user_data.get('email')
name = f"{user_data.get('first_name', '')} {user_data.get('last_name', '')}".strip()
metadata = None
customer = await self.payment_service.create_customer(email, name, metadata)
logger.info("Customer created successfully",
customer_id=customer.id, tenant_id=tenant_id)
@@ -106,9 +111,12 @@ class SubscriptionOrchestrationService:
plan_id=plan_id,
trial_period_days=trial_period_days)
stripe_subscription = await self.payment_service.create_payment_subscription(
# Get the Stripe price ID for this plan
price_id = self.payment_service._get_stripe_price_id(plan_id, billing_interval)
stripe_subscription = await self.payment_service.create_subscription_with_verified_payment(
customer.id,
plan_id,
price_id,
payment_method_id,
trial_period_days if trial_period_days > 0 else None,
billing_interval
@@ -232,7 +240,10 @@ class SubscriptionOrchestrationService:
user_id=user_data.get('user_id'),
email=user_data.get('email'))
customer = await self.payment_service.create_customer(user_data)
email = user_data.get('email')
name = f"{user_data.get('first_name', '')} {user_data.get('last_name', '')}".strip()
metadata = None
customer = await self.payment_service.create_customer(email, name, metadata)
logger.info("Customer created successfully",
customer_id=customer.id,
user_id=user_data.get('user_id'))
@@ -271,9 +282,12 @@ class SubscriptionOrchestrationService:
plan_id=plan_id,
trial_period_days=trial_period_days)
subscription_result = await self.payment_service.create_payment_subscription(
# Get the Stripe price ID for this plan
price_id = self.payment_service._get_stripe_price_id(plan_id, billing_interval)
subscription_result = await self.payment_service.create_subscription_with_verified_payment(
customer.id,
plan_id,
price_id,
payment_method_id,
trial_period_days if trial_period_days > 0 else None,
billing_interval
@@ -302,9 +316,25 @@ class SubscriptionOrchestrationService:
}
# Extract subscription object from result
# Result can be either a dict with 'subscription' key or the subscription object directly
if isinstance(subscription_result, dict) and 'subscription' in subscription_result:
stripe_subscription = subscription_result['subscription']
# Result can be either:
# 1. A dict with 'subscription' key containing an object
# 2. A dict with subscription fields directly (subscription_id, status, etc.)
# 3. A subscription object directly
if isinstance(subscription_result, dict):
if 'subscription' in subscription_result:
stripe_subscription = subscription_result['subscription']
elif 'subscription_id' in subscription_result:
# Create a simple object-like wrapper for dict results
class SubscriptionWrapper:
def __init__(self, data: dict):
self.id = data.get('subscription_id')
self.status = data.get('status')
self.current_period_start = data.get('current_period_start')
self.current_period_end = data.get('current_period_end')
self.customer = data.get('customer_id')
stripe_subscription = SubscriptionWrapper(subscription_result)
else:
stripe_subscription = subscription_result
else:
stripe_subscription = subscription_result
@@ -980,7 +1010,7 @@ class SubscriptionOrchestrationService:
status=status)
# Find tenant by subscription
subscription = await self.subscription_service.get_subscription_by_stripe_id(subscription_id)
subscription = await self.subscription_service.get_subscription_by_provider_id(subscription_id)
if subscription:
# Update subscription status
@@ -1014,7 +1044,7 @@ class SubscriptionOrchestrationService:
subscription_id=subscription_id)
# Find and update subscription
subscription = await self.subscription_service.get_subscription_by_stripe_id(subscription_id)
subscription = await self.subscription_service.get_subscription_by_provider_id(subscription_id)
if subscription:
# Cancel subscription in our system
@@ -1618,16 +1648,14 @@ class SubscriptionOrchestrationService:
email=user_data.get('email'))
# Create customer without user_id metadata
customer_data = {
'email': user_data.get('email'),
'name': user_data.get('full_name'),
'metadata': {
'registration_flow': 'pre_user_creation',
'timestamp': datetime.now(timezone.utc).isoformat()
}
email = user_data.get('email')
name = user_data.get('full_name')
metadata = {
'registration_flow': 'pre_user_creation',
'timestamp': datetime.now(timezone.utc).isoformat()
}
customer = await self.payment_service.create_customer(customer_data)
customer = await self.payment_service.create_customer(email, name, metadata)
logger.info("Payment customer created for registration",
customer_id=customer.id,
email=user_data.get('email'))
@@ -1665,9 +1693,12 @@ class SubscriptionOrchestrationService:
plan_id=plan_id,
payment_method_id=payment_method_id)
subscription_result = await self.payment_service.create_payment_subscription(
# Get the Stripe price ID for this plan
price_id = self.payment_service._get_stripe_price_id(plan_id, billing_interval)
subscription_result = await self.payment_service.create_subscription_with_verified_payment(
customer.id,
plan_id,
price_id,
payment_method_id,
trial_period_days if trial_period_days > 0 else None,
billing_interval
@@ -1678,14 +1709,19 @@ class SubscriptionOrchestrationService:
logger.info("Registration payment setup requires SetupIntent confirmation",
customer_id=customer.id,
action_type=subscription_result.get('action_type'),
setup_intent_id=subscription_result.get('setup_intent_id'))
setup_intent_id=subscription_result.get('setup_intent_id'),
subscription_id=subscription_result.get('subscription_id'))
# Return the SetupIntent data for frontend to handle 3DS
# Note: subscription_id is included because for trial subscriptions,
# the subscription is already created in 'trialing' status even though
# the SetupIntent requires 3DS verification for future payments
return {
"requires_action": True,
"action_type": subscription_result.get('action_type'),
"action_type": subscription_result.get('action_type') or 'use_stripe_sdk',
"client_secret": subscription_result.get('client_secret'),
"setup_intent_id": subscription_result.get('setup_intent_id'),
"subscription_id": subscription_result.get('subscription_id'),
"customer_id": customer.id,
"payment_customer_id": customer.id,
"plan_id": plan_id,
@@ -1764,3 +1800,154 @@ class SubscriptionOrchestrationService:
error=str(e),
exc_info=True)
raise
async def validate_plan_upgrade(
self,
tenant_id: str,
new_plan: str
) -> Dict[str, Any]:
"""
Validate if a tenant can upgrade to a new plan
Args:
tenant_id: Tenant ID
new_plan: New plan to validate upgrade to
Returns:
Dictionary with validation result
"""
try:
logger.info("Validating plan upgrade",
tenant_id=tenant_id,
new_plan=new_plan)
# Delegate to subscription service for validation
can_upgrade = await self.subscription_service.validate_subscription_change(
tenant_id,
new_plan
)
result = {
"can_upgrade": can_upgrade,
"tenant_id": tenant_id,
"current_plan": None, # Would need to fetch current plan if needed
"new_plan": new_plan
}
if not can_upgrade:
result["reason"] = "Subscription change not allowed based on current status"
logger.info("Plan upgrade validation completed",
tenant_id=tenant_id,
can_upgrade=can_upgrade)
return result
except Exception as e:
logger.error("Plan upgrade validation failed",
tenant_id=tenant_id,
new_plan=new_plan,
error=str(e),
exc_info=True)
raise DatabaseError(f"Failed to validate plan upgrade: {str(e)}")
async def get_subscriptions_by_customer_id(self, customer_id: str) -> List[Subscription]:
"""
Get all subscriptions for a given customer ID
Args:
customer_id: Stripe customer ID
Returns:
List of Subscription objects
"""
try:
return await self.subscription_service.get_subscriptions_by_customer_id(customer_id)
except Exception as e:
logger.error("Failed to get subscriptions by customer ID",
customer_id=customer_id,
error=str(e),
exc_info=True)
raise DatabaseError(f"Failed to get subscriptions: {str(e)}")
async def update_subscription_with_verified_payment(
self,
subscription_id: str,
customer_id: str,
payment_method_id: str,
trial_period_days: Optional[int] = None
) -> Dict[str, Any]:
"""
Update an existing subscription with a verified payment method
This is used when we already have a trial subscription and just need to
attach the verified payment method to it.
Args:
subscription_id: Stripe subscription ID
customer_id: Stripe customer ID
payment_method_id: Verified payment method ID
trial_period_days: Optional trial period (for validation)
Returns:
Dictionary with updated subscription details
"""
try:
logger.info("Updating existing subscription with verified payment method",
subscription_id=subscription_id,
customer_id=customer_id,
payment_method_id=payment_method_id)
# First, verify the subscription exists and get its current status
existing_subscription = await self.subscription_service.get_subscription_by_provider_id(subscription_id)
if not existing_subscription:
raise SubscriptionNotFound(f"Subscription {subscription_id} not found")
# Update the subscription in Stripe with the verified payment method
stripe_subscription = await self.payment_service.update_subscription_payment_method(
subscription_id,
payment_method_id
)
# Update our local subscription record
await self.subscription_service.update_subscription_status(
existing_subscription.tenant_id,
stripe_subscription.status,
{
'current_period_start': datetime.fromtimestamp(stripe_subscription.current_period_start),
'current_period_end': datetime.fromtimestamp(stripe_subscription.current_period_end)
}
)
# Create a mock subscription object-like dict for compatibility
class SubscriptionResult:
def __init__(self, data: Dict[str, Any]):
self.id = data.get('subscription_id')
self.status = data.get('status')
self.current_period_start = data.get('current_period_start')
self.current_period_end = data.get('current_period_end')
self.customer = data.get('customer_id')
return {
'subscription': SubscriptionResult({
'subscription_id': stripe_subscription.id,
'status': stripe_subscription.status,
'current_period_start': stripe_subscription.current_period_start,
'current_period_end': stripe_subscription.current_period_end,
'customer_id': customer_id
}),
'verification': {
'verified': True,
'customer_id': customer_id,
'payment_method_id': payment_method_id
}
}
except Exception as e:
logger.error("Failed to update subscription with verified payment",
subscription_id=subscription_id,
customer_id=customer_id,
error=str(e),
exc_info=True)
raise SubscriptionUpdateFailed(f"Failed to update subscription: {str(e)}")