Add subcription feature 3
This commit is contained in:
@@ -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)}")
|
||||
|
||||
Reference in New Issue
Block a user