Add subcription feature 2
This commit is contained in:
@@ -121,7 +121,7 @@ class SubscriptionOrchestrationService:
|
||||
# Step 4: Create local subscription record
|
||||
logger.info("Creating local subscription record",
|
||||
tenant_id=tenant_id,
|
||||
stripe_subscription_id=stripe_subscription.id)
|
||||
subscription_id=stripe_subscription.id)
|
||||
|
||||
subscription_record = await self.subscription_service.create_subscription_record(
|
||||
tenant_id,
|
||||
@@ -141,7 +141,7 @@ class SubscriptionOrchestrationService:
|
||||
tenant_id=tenant_id)
|
||||
|
||||
tenant_update_data = {
|
||||
'stripe_customer_id': customer.id,
|
||||
'customer_id': customer.id,
|
||||
'subscription_status': stripe_subscription.status,
|
||||
'subscription_plan': plan_id,
|
||||
'subscription_tier': plan_id,
|
||||
@@ -265,13 +265,13 @@ class SubscriptionOrchestrationService:
|
||||
coupon_code=coupon_code,
|
||||
error=error)
|
||||
|
||||
# Step 3: Create subscription in payment provider
|
||||
# Step 3: Create subscription in payment provider (or get SetupIntent for 3DS)
|
||||
logger.info("Creating subscription in payment provider",
|
||||
customer_id=customer.id,
|
||||
plan_id=plan_id,
|
||||
trial_period_days=trial_period_days)
|
||||
|
||||
stripe_subscription = await self.payment_service.create_payment_subscription(
|
||||
subscription_result = await self.payment_service.create_payment_subscription(
|
||||
customer.id,
|
||||
plan_id,
|
||||
payment_method_id,
|
||||
@@ -279,6 +279,35 @@ class SubscriptionOrchestrationService:
|
||||
billing_interval
|
||||
)
|
||||
|
||||
# Check if result requires 3DS authentication (SetupIntent confirmation)
|
||||
if isinstance(subscription_result, dict) and subscription_result.get('requires_action'):
|
||||
logger.info("Subscription creation requires SetupIntent confirmation",
|
||||
customer_id=customer.id,
|
||||
action_type=subscription_result.get('action_type'),
|
||||
setup_intent_id=subscription_result.get('setup_intent_id'))
|
||||
|
||||
# Return the SetupIntent data for frontend to handle 3DS
|
||||
return {
|
||||
"requires_action": True,
|
||||
"action_type": subscription_result.get('action_type'),
|
||||
"client_secret": subscription_result.get('client_secret'),
|
||||
"setup_intent_id": subscription_result.get('setup_intent_id'),
|
||||
"customer_id": customer.id,
|
||||
"payment_method_id": payment_method_id,
|
||||
"plan_id": plan_id,
|
||||
"trial_period_days": trial_period_days,
|
||||
"billing_interval": billing_interval,
|
||||
"message": subscription_result.get('message'),
|
||||
"user_id": user_data.get('user_id')
|
||||
}
|
||||
|
||||
# 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']
|
||||
else:
|
||||
stripe_subscription = subscription_result
|
||||
|
||||
logger.info("Subscription created in payment provider",
|
||||
subscription_id=stripe_subscription.id,
|
||||
status=stripe_subscription.status)
|
||||
@@ -286,7 +315,7 @@ class SubscriptionOrchestrationService:
|
||||
# Step 4: Create local subscription record WITHOUT tenant_id
|
||||
logger.info("Creating tenant-independent subscription record",
|
||||
user_id=user_data.get('user_id'),
|
||||
stripe_subscription_id=stripe_subscription.id)
|
||||
subscription_id=stripe_subscription.id)
|
||||
|
||||
subscription_record = await self.subscription_service.create_tenant_independent_subscription_record(
|
||||
stripe_subscription.id,
|
||||
@@ -345,6 +374,100 @@ class SubscriptionOrchestrationService:
|
||||
error=str(e), user_id=user_data.get('user_id'))
|
||||
raise DatabaseError(f"Failed to create tenant-independent subscription: {str(e)}")
|
||||
|
||||
async def complete_subscription_after_setup_intent(
|
||||
self,
|
||||
setup_intent_id: str,
|
||||
customer_id: str,
|
||||
plan_id: str,
|
||||
payment_method_id: str,
|
||||
trial_period_days: Optional[int],
|
||||
user_id: str,
|
||||
billing_interval: str = "monthly"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Complete subscription creation after SetupIntent has been confirmed
|
||||
|
||||
This method is called after the frontend successfully confirms a SetupIntent
|
||||
(with or without 3DS). It creates the subscription with the verified payment method
|
||||
and creates a database record.
|
||||
|
||||
Args:
|
||||
setup_intent_id: The confirmed SetupIntent ID
|
||||
customer_id: Stripe customer ID
|
||||
plan_id: Subscription plan ID
|
||||
payment_method_id: Verified payment method ID
|
||||
trial_period_days: Optional trial period
|
||||
user_id: User ID for linking
|
||||
billing_interval: Billing interval
|
||||
|
||||
Returns:
|
||||
Dictionary with subscription details
|
||||
"""
|
||||
try:
|
||||
logger.info("Completing subscription after SetupIntent confirmation",
|
||||
setup_intent_id=setup_intent_id,
|
||||
user_id=user_id,
|
||||
plan_id=plan_id)
|
||||
|
||||
# Call payment service to complete subscription creation
|
||||
result = await self.payment_service.complete_subscription_after_setup_intent(
|
||||
setup_intent_id,
|
||||
customer_id,
|
||||
plan_id,
|
||||
payment_method_id,
|
||||
trial_period_days
|
||||
)
|
||||
|
||||
stripe_subscription = result['subscription']
|
||||
|
||||
logger.info("Subscription created in payment provider after SetupIntent",
|
||||
subscription_id=stripe_subscription.id,
|
||||
status=stripe_subscription.status)
|
||||
|
||||
# Create local subscription record WITHOUT tenant_id (tenant-independent)
|
||||
subscription_record = await self.subscription_service.create_tenant_independent_subscription_record(
|
||||
stripe_subscription.id,
|
||||
customer_id,
|
||||
plan_id,
|
||||
stripe_subscription.status,
|
||||
trial_period_days,
|
||||
billing_interval,
|
||||
user_id
|
||||
)
|
||||
|
||||
logger.info("Tenant-independent subscription record created after SetupIntent",
|
||||
subscription_id=stripe_subscription.id,
|
||||
user_id=user_id)
|
||||
|
||||
# Convert current_period_end to ISO format
|
||||
current_period_end = stripe_subscription.current_period_end
|
||||
if isinstance(current_period_end, int):
|
||||
current_period_end = datetime.fromtimestamp(current_period_end, tz=timezone.utc).isoformat()
|
||||
elif hasattr(current_period_end, 'isoformat'):
|
||||
current_period_end = current_period_end.isoformat()
|
||||
else:
|
||||
current_period_end = str(current_period_end)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"customer_id": customer_id,
|
||||
"subscription_id": stripe_subscription.id,
|
||||
"status": stripe_subscription.status,
|
||||
"plan": plan_id,
|
||||
"billing_cycle": billing_interval,
|
||||
"trial_period_days": trial_period_days,
|
||||
"current_period_end": current_period_end,
|
||||
"user_id": user_id,
|
||||
"setup_intent_id": setup_intent_id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to complete subscription after SetupIntent",
|
||||
error=str(e),
|
||||
setup_intent_id=setup_intent_id,
|
||||
user_id=user_id)
|
||||
raise DatabaseError(f"Failed to complete subscription: {str(e)}")
|
||||
|
||||
async def orchestrate_subscription_cancellation(
|
||||
self,
|
||||
tenant_id: str,
|
||||
@@ -383,7 +506,7 @@ class SubscriptionOrchestrationService:
|
||||
)
|
||||
|
||||
logger.info("Subscription cancelled in payment provider",
|
||||
stripe_subscription_id=stripe_subscription.id,
|
||||
subscription_id=stripe_subscription.id,
|
||||
stripe_status=stripe_subscription.status)
|
||||
|
||||
# Step 4: Sync status back to database
|
||||
@@ -536,7 +659,7 @@ class SubscriptionOrchestrationService:
|
||||
)
|
||||
|
||||
logger.info("Plan updated in payment provider",
|
||||
stripe_subscription_id=updated_stripe_subscription.id,
|
||||
subscription_id=updated_stripe_subscription.id,
|
||||
new_status=updated_stripe_subscription.status)
|
||||
|
||||
# Step 5: Update local subscription record
|
||||
@@ -622,7 +745,7 @@ class SubscriptionOrchestrationService:
|
||||
)
|
||||
|
||||
logger.info("Billing cycle changed in payment provider",
|
||||
stripe_subscription_id=updated_stripe_subscription.id,
|
||||
subscription_id=updated_stripe_subscription.id,
|
||||
new_billing_cycle=new_billing_cycle)
|
||||
|
||||
# Step 3: Get proration details (if available)
|
||||
@@ -771,6 +894,26 @@ class SubscriptionOrchestrationService:
|
||||
await self._handle_subscription_resumed(event_data)
|
||||
result["actions_taken"].append("subscription_resumed")
|
||||
|
||||
elif event_type == 'payment_intent.succeeded':
|
||||
await self._handle_payment_intent_succeeded(event_data)
|
||||
result["actions_taken"].append("payment_intent_succeeded")
|
||||
|
||||
elif event_type == 'payment_intent.payment_failed':
|
||||
await self._handle_payment_intent_failed(event_data)
|
||||
result["actions_taken"].append("payment_intent_failed")
|
||||
|
||||
elif event_type == 'payment_intent.requires_action':
|
||||
await self._handle_payment_intent_requires_action(event_data)
|
||||
result["actions_taken"].append("payment_intent_requires_action")
|
||||
|
||||
elif event_type == 'setup_intent.succeeded':
|
||||
await self._handle_setup_intent_succeeded(event_data)
|
||||
result["actions_taken"].append("setup_intent_succeeded")
|
||||
|
||||
elif event_type == 'setup_intent.requires_action':
|
||||
await self._handle_setup_intent_requires_action(event_data)
|
||||
result["actions_taken"].append("setup_intent_requires_action")
|
||||
|
||||
else:
|
||||
logger.info("Unhandled webhook event type", event_type=event_type)
|
||||
result["processed"] = False
|
||||
@@ -800,7 +943,7 @@ class SubscriptionOrchestrationService:
|
||||
status=status)
|
||||
|
||||
# Find tenant by customer ID
|
||||
tenant = await self.tenant_service.get_tenant_by_stripe_customer_id(customer_id)
|
||||
tenant = await self.tenant_service.get_tenant_by_customer_id(customer_id)
|
||||
|
||||
if tenant:
|
||||
# Update subscription status
|
||||
@@ -896,7 +1039,7 @@ class SubscriptionOrchestrationService:
|
||||
customer_id=customer_id)
|
||||
|
||||
# Find tenant and update payment status
|
||||
tenant = await self.tenant_service.get_tenant_by_stripe_customer_id(customer_id)
|
||||
tenant = await self.tenant_service.get_tenant_by_customer_id(customer_id)
|
||||
|
||||
if tenant:
|
||||
tenant_update_data = {
|
||||
@@ -924,7 +1067,7 @@ class SubscriptionOrchestrationService:
|
||||
customer_id=customer_id)
|
||||
|
||||
# Find tenant and update payment status
|
||||
tenant = await self.tenant_service.get_tenant_by_stripe_customer_id(customer_id)
|
||||
tenant = await self.tenant_service.get_tenant_by_customer_id(customer_id)
|
||||
|
||||
if tenant:
|
||||
tenant_update_data = {
|
||||
@@ -951,7 +1094,7 @@ class SubscriptionOrchestrationService:
|
||||
customer_id=customer_id,
|
||||
trial_end=trial_end)
|
||||
|
||||
tenant = await self.tenant_service.get_tenant_by_stripe_customer_id(customer_id)
|
||||
tenant = await self.tenant_service.get_tenant_by_customer_id(customer_id)
|
||||
|
||||
if tenant:
|
||||
tenant_update_data = {
|
||||
@@ -978,7 +1121,7 @@ class SubscriptionOrchestrationService:
|
||||
customer_id=customer_id,
|
||||
subscription_id=subscription_id)
|
||||
|
||||
tenant = await self.tenant_service.get_tenant_by_stripe_customer_id(customer_id)
|
||||
tenant = await self.tenant_service.get_tenant_by_customer_id(customer_id)
|
||||
|
||||
if tenant:
|
||||
tenant_update_data = {
|
||||
@@ -1004,7 +1147,7 @@ class SubscriptionOrchestrationService:
|
||||
subscription_id=subscription_id,
|
||||
customer_id=customer_id)
|
||||
|
||||
tenant = await self.tenant_service.get_tenant_by_stripe_customer_id(customer_id)
|
||||
tenant = await self.tenant_service.get_tenant_by_customer_id(customer_id)
|
||||
|
||||
if tenant:
|
||||
await self.subscription_service.update_subscription_status(
|
||||
@@ -1038,7 +1181,7 @@ class SubscriptionOrchestrationService:
|
||||
subscription_id=subscription_id,
|
||||
customer_id=customer_id)
|
||||
|
||||
tenant = await self.tenant_service.get_tenant_by_stripe_customer_id(customer_id)
|
||||
tenant = await self.tenant_service.get_tenant_by_customer_id(customer_id)
|
||||
|
||||
if tenant:
|
||||
await self.subscription_service.update_subscription_status(
|
||||
@@ -1062,6 +1205,155 @@ class SubscriptionOrchestrationService:
|
||||
tenant_id=str(tenant.id),
|
||||
subscription_id=subscription_id)
|
||||
|
||||
async def _handle_payment_intent_succeeded(self, event_data: Dict[str, Any]):
|
||||
"""Handle payment intent succeeded event (including 3DS authenticated payments)"""
|
||||
payment_intent_id = event_data['id']
|
||||
customer_id = event_data.get('customer')
|
||||
amount = event_data.get('amount', 0) / 100.0
|
||||
currency = event_data.get('currency', 'eur').upper()
|
||||
|
||||
logger.info("Handling payment intent succeeded event",
|
||||
payment_intent_id=payment_intent_id,
|
||||
customer_id=customer_id,
|
||||
amount=amount)
|
||||
|
||||
if customer_id:
|
||||
tenant = await self.tenant_service.get_tenant_by_customer_id(customer_id)
|
||||
|
||||
if tenant:
|
||||
tenant_update_data = {
|
||||
'payment_action_required': False,
|
||||
'last_successful_payment_at': datetime.now(timezone.utc)
|
||||
}
|
||||
|
||||
await self.tenant_service.update_tenant_subscription_info(
|
||||
str(tenant.id), tenant_update_data
|
||||
)
|
||||
|
||||
logger.info("Payment intent succeeded event handled",
|
||||
tenant_id=str(tenant.id),
|
||||
payment_intent_id=payment_intent_id)
|
||||
|
||||
async def _handle_payment_intent_failed(self, event_data: Dict[str, Any]):
|
||||
"""Handle payment intent failed event (including 3DS authentication failures)"""
|
||||
payment_intent_id = event_data['id']
|
||||
customer_id = event_data.get('customer')
|
||||
last_payment_error = event_data.get('last_payment_error', {})
|
||||
error_message = last_payment_error.get('message', 'Payment failed')
|
||||
|
||||
logger.warning("Handling payment intent failed event",
|
||||
payment_intent_id=payment_intent_id,
|
||||
customer_id=customer_id,
|
||||
error_message=error_message)
|
||||
|
||||
if customer_id:
|
||||
tenant = await self.tenant_service.get_tenant_by_customer_id(customer_id)
|
||||
|
||||
if tenant:
|
||||
tenant_update_data = {
|
||||
'payment_action_required': False,
|
||||
'last_payment_failure_at': datetime.now(timezone.utc),
|
||||
'last_payment_error': error_message
|
||||
}
|
||||
|
||||
await self.tenant_service.update_tenant_subscription_info(
|
||||
str(tenant.id), tenant_update_data
|
||||
)
|
||||
|
||||
logger.info("Payment intent failed event handled",
|
||||
tenant_id=str(tenant.id),
|
||||
payment_intent_id=payment_intent_id)
|
||||
|
||||
async def _handle_payment_intent_requires_action(self, event_data: Dict[str, Any]):
|
||||
"""Handle payment intent requires action event (3DS authentication needed)"""
|
||||
payment_intent_id = event_data['id']
|
||||
customer_id = event_data.get('customer')
|
||||
next_action = event_data.get('next_action', {})
|
||||
action_type = next_action.get('type', 'unknown')
|
||||
|
||||
logger.info("Handling payment intent requires action event",
|
||||
payment_intent_id=payment_intent_id,
|
||||
customer_id=customer_id,
|
||||
action_type=action_type)
|
||||
|
||||
if customer_id:
|
||||
tenant = await self.tenant_service.get_tenant_by_customer_id(customer_id)
|
||||
|
||||
if tenant:
|
||||
tenant_update_data = {
|
||||
'payment_action_required': True,
|
||||
'payment_action_type': action_type,
|
||||
'last_payment_action_required_at': datetime.now(timezone.utc)
|
||||
}
|
||||
|
||||
await self.tenant_service.update_tenant_subscription_info(
|
||||
str(tenant.id), tenant_update_data
|
||||
)
|
||||
|
||||
logger.info("Payment intent requires action event handled",
|
||||
tenant_id=str(tenant.id),
|
||||
payment_intent_id=payment_intent_id,
|
||||
action_type=action_type)
|
||||
|
||||
async def _handle_setup_intent_succeeded(self, event_data: Dict[str, Any]):
|
||||
"""Handle setup intent succeeded event (3DS authentication completed)"""
|
||||
setup_intent_id = event_data['id']
|
||||
customer_id = event_data.get('customer')
|
||||
|
||||
logger.info("Handling setup intent succeeded event (3DS authentication completed)",
|
||||
setup_intent_id=setup_intent_id,
|
||||
customer_id=customer_id)
|
||||
|
||||
if customer_id:
|
||||
tenant = await self.tenant_service.get_tenant_by_customer_id(customer_id)
|
||||
|
||||
if tenant:
|
||||
tenant_update_data = {
|
||||
'threeds_authentication_completed': True,
|
||||
'threeds_authentication_completed_at': datetime.now(timezone.utc),
|
||||
'last_threeds_setup_intent_id': setup_intent_id
|
||||
}
|
||||
|
||||
await self.tenant_service.update_tenant_subscription_info(
|
||||
str(tenant.id), tenant_update_data
|
||||
)
|
||||
|
||||
logger.info("Setup intent succeeded event handled (3DS authentication completed)",
|
||||
tenant_id=str(tenant.id),
|
||||
setup_intent_id=setup_intent_id)
|
||||
|
||||
async def _handle_setup_intent_requires_action(self, event_data: Dict[str, Any]):
|
||||
"""Handle setup intent requires action event (3DS authentication needed)"""
|
||||
setup_intent_id = event_data['id']
|
||||
customer_id = event_data.get('customer')
|
||||
next_action = event_data.get('next_action', {})
|
||||
action_type = next_action.get('type', 'unknown')
|
||||
|
||||
logger.info("Handling setup intent requires action event (3DS authentication needed)",
|
||||
setup_intent_id=setup_intent_id,
|
||||
customer_id=customer_id,
|
||||
action_type=action_type)
|
||||
|
||||
if customer_id:
|
||||
tenant = await self.tenant_service.get_tenant_by_customer_id(customer_id)
|
||||
|
||||
if tenant:
|
||||
tenant_update_data = {
|
||||
'threeds_authentication_required': True,
|
||||
'threeds_authentication_required_at': datetime.now(timezone.utc),
|
||||
'last_threeds_setup_intent_id': setup_intent_id,
|
||||
'threeds_action_type': action_type
|
||||
}
|
||||
|
||||
await self.tenant_service.update_tenant_subscription_info(
|
||||
str(tenant.id), tenant_update_data
|
||||
)
|
||||
|
||||
logger.info("Setup intent requires action event handled (3DS authentication needed)",
|
||||
tenant_id=str(tenant.id),
|
||||
setup_intent_id=setup_intent_id,
|
||||
action_type=action_type)
|
||||
|
||||
async def orchestrate_subscription_creation_with_default_payment(
|
||||
self,
|
||||
tenant_id: str,
|
||||
@@ -1165,3 +1457,310 @@ class SubscriptionOrchestrationService:
|
||||
error=str(e))
|
||||
# Don't fail the subscription creation if we can't get the default payment method
|
||||
return None
|
||||
|
||||
async def get_payment_method(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get the current payment method for a tenant's subscription
|
||||
|
||||
This is an orchestration method that coordinates between:
|
||||
1. SubscriptionService (to get subscription data)
|
||||
2. PaymentService (to get payment method from provider)
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID
|
||||
|
||||
Returns:
|
||||
Dictionary with payment method details or None
|
||||
"""
|
||||
try:
|
||||
# Get subscription from database
|
||||
subscription = await self.subscription_service.get_subscription_by_tenant_id(tenant_id)
|
||||
|
||||
if not subscription:
|
||||
logger.warning("get_payment_method_no_subscription",
|
||||
tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
# Check if subscription has a customer ID
|
||||
if not subscription.customer_id:
|
||||
logger.warning("get_payment_method_no_customer_id",
|
||||
tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
# Get payment method from payment provider
|
||||
payment_method = await self.payment_service.get_customer_payment_method(subscription.customer_id)
|
||||
|
||||
if not payment_method:
|
||||
logger.info("get_payment_method_not_found",
|
||||
tenant_id=tenant_id,
|
||||
customer_id=subscription.customer_id)
|
||||
return None
|
||||
|
||||
logger.info("payment_method_retrieved",
|
||||
tenant_id=tenant_id,
|
||||
payment_method_type=payment_method.type,
|
||||
last4=payment_method.last4)
|
||||
|
||||
return {
|
||||
"brand": payment_method.brand,
|
||||
"last4": payment_method.last4,
|
||||
"exp_month": payment_method.exp_month,
|
||||
"exp_year": payment_method.exp_year
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("get_payment_method_failed",
|
||||
error=str(e),
|
||||
tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
async def update_payment_method(
|
||||
self,
|
||||
tenant_id: str,
|
||||
payment_method_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Update the default payment method for a tenant's subscription
|
||||
|
||||
This is an orchestration method that coordinates between:
|
||||
1. SubscriptionService (to get subscription data)
|
||||
2. PaymentService (to update payment method with provider)
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID
|
||||
payment_method_id: New payment method ID from frontend
|
||||
|
||||
Returns:
|
||||
Dictionary with updated payment method details
|
||||
|
||||
Raises:
|
||||
ValidationError: If subscription or customer_id not found
|
||||
"""
|
||||
try:
|
||||
# Get subscription from database
|
||||
subscription = await self.subscription_service.get_subscription_by_tenant_id(tenant_id)
|
||||
|
||||
if not subscription:
|
||||
raise ValidationError(f"Subscription not found for tenant {tenant_id}")
|
||||
|
||||
if not subscription.customer_id:
|
||||
raise ValidationError(f"Tenant {tenant_id} does not have a payment customer ID")
|
||||
|
||||
# Update payment method via payment provider
|
||||
payment_result = await self.payment_service.update_payment_method(
|
||||
subscription.customer_id,
|
||||
payment_method_id
|
||||
)
|
||||
|
||||
logger.info("payment_method_updated",
|
||||
tenant_id=tenant_id,
|
||||
payment_method_id=payment_method_id,
|
||||
requires_action=payment_result.get('requires_action', False))
|
||||
|
||||
pm_details = payment_result.get('payment_method', {})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Payment method updated successfully",
|
||||
"payment_method_id": pm_details.get('id'),
|
||||
"brand": pm_details.get('brand', 'unknown'),
|
||||
"last4": pm_details.get('last4', '0000'),
|
||||
"exp_month": pm_details.get('exp_month'),
|
||||
"exp_year": pm_details.get('exp_year'),
|
||||
"requires_action": payment_result.get('requires_action', False),
|
||||
"client_secret": payment_result.get('client_secret'),
|
||||
"payment_intent_status": payment_result.get('payment_intent_status')
|
||||
}
|
||||
|
||||
except ValidationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("update_payment_method_failed",
|
||||
error=str(e),
|
||||
tenant_id=tenant_id)
|
||||
raise DatabaseError(f"Failed to update payment method: {str(e)}")
|
||||
|
||||
async def create_registration_payment_setup(
|
||||
self,
|
||||
user_data: Dict[str, Any],
|
||||
plan_id: str,
|
||||
payment_method_id: str,
|
||||
billing_interval: str = "monthly",
|
||||
coupon_code: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create payment customer and SetupIntent for registration (pre-user-creation)
|
||||
|
||||
This method supports the secure architecture where users are only created
|
||||
after payment verification. It creates a payment customer and SetupIntent
|
||||
without requiring a user_id.
|
||||
|
||||
Args:
|
||||
user_data: User data (email, full_name, etc.) - NO user_id required
|
||||
plan_id: Subscription plan ID
|
||||
payment_method_id: Payment method ID from frontend
|
||||
billing_interval: Billing interval (monthly/yearly)
|
||||
coupon_code: Optional coupon code
|
||||
|
||||
Returns:
|
||||
Dictionary with payment setup results including SetupIntent if required
|
||||
|
||||
Raises:
|
||||
Exception: If payment setup fails
|
||||
"""
|
||||
try:
|
||||
logger.info("Starting registration payment setup (pre-user-creation)",
|
||||
email=user_data.get('email'),
|
||||
plan_id=plan_id)
|
||||
|
||||
# Step 1: Create payment customer (without user_id)
|
||||
logger.info("Creating payment customer for registration",
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
customer = await self.payment_service.create_customer(customer_data)
|
||||
logger.info("Payment customer created for registration",
|
||||
customer_id=customer.id,
|
||||
email=user_data.get('email'))
|
||||
|
||||
# Step 2: Handle coupon logic (if provided)
|
||||
trial_period_days = 0
|
||||
coupon_discount = None
|
||||
|
||||
if coupon_code:
|
||||
logger.info("Validating and redeeming coupon code for registration",
|
||||
coupon_code=coupon_code,
|
||||
email=user_data.get('email'))
|
||||
|
||||
coupon_service = CouponService(self.db_session)
|
||||
success, discount_applied, error = await coupon_service.redeem_coupon(
|
||||
coupon_code,
|
||||
None, # No tenant_id yet
|
||||
base_trial_days=0
|
||||
)
|
||||
|
||||
if success and discount_applied:
|
||||
coupon_discount = discount_applied
|
||||
trial_period_days = discount_applied.get("total_trial_days", 0)
|
||||
logger.info("Coupon redeemed successfully for registration",
|
||||
coupon_code=coupon_code,
|
||||
trial_period_days=trial_period_days)
|
||||
else:
|
||||
logger.warning("Failed to redeem coupon for registration, continuing without it",
|
||||
coupon_code=coupon_code,
|
||||
error=error)
|
||||
|
||||
# Step 3: Create subscription/SetupIntent
|
||||
logger.info("Creating subscription/SetupIntent for registration",
|
||||
customer_id=customer.id,
|
||||
plan_id=plan_id,
|
||||
payment_method_id=payment_method_id)
|
||||
|
||||
subscription_result = await self.payment_service.create_payment_subscription(
|
||||
customer.id,
|
||||
plan_id,
|
||||
payment_method_id,
|
||||
trial_period_days if trial_period_days > 0 else None,
|
||||
billing_interval
|
||||
)
|
||||
|
||||
# Check if result requires 3DS authentication (SetupIntent confirmation)
|
||||
if isinstance(subscription_result, dict) and subscription_result.get('requires_action'):
|
||||
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'))
|
||||
|
||||
# Return the SetupIntent data for frontend to handle 3DS
|
||||
return {
|
||||
"requires_action": True,
|
||||
"action_type": subscription_result.get('action_type'),
|
||||
"client_secret": subscription_result.get('client_secret'),
|
||||
"setup_intent_id": subscription_result.get('setup_intent_id'),
|
||||
"customer_id": customer.id,
|
||||
"payment_customer_id": customer.id,
|
||||
"plan_id": plan_id,
|
||||
"payment_method_id": payment_method_id,
|
||||
"trial_period_days": trial_period_days,
|
||||
"billing_interval": billing_interval,
|
||||
"coupon_applied": coupon_code is not None,
|
||||
"email": user_data.get('email'),
|
||||
"full_name": user_data.get('full_name'),
|
||||
"message": subscription_result.get('message') or "Payment verification required before account creation"
|
||||
}
|
||||
else:
|
||||
# No 3DS required - subscription created successfully
|
||||
logger.info("Registration payment setup completed without 3DS",
|
||||
customer_id=customer.id,
|
||||
subscription_id=subscription_result.get('subscription_id'))
|
||||
|
||||
return {
|
||||
"requires_action": False,
|
||||
"subscription_id": subscription_result.get('subscription_id'),
|
||||
"customer_id": customer.id,
|
||||
"payment_customer_id": customer.id,
|
||||
"plan_id": plan_id,
|
||||
"payment_method_id": payment_method_id,
|
||||
"trial_period_days": trial_period_days,
|
||||
"billing_interval": billing_interval,
|
||||
"coupon_applied": coupon_code is not None,
|
||||
"email": user_data.get('email'),
|
||||
"full_name": user_data.get('full_name'),
|
||||
"message": "Payment setup completed successfully"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Registration payment setup failed",
|
||||
email=user_data.get('email'),
|
||||
error=str(e),
|
||||
exc_info=True)
|
||||
raise
|
||||
|
||||
async def verify_setup_intent_for_registration(
|
||||
self,
|
||||
setup_intent_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Verify SetupIntent status for registration completion
|
||||
|
||||
This method checks if a SetupIntent has been successfully confirmed
|
||||
(either automatically or via 3DS authentication) before proceeding
|
||||
with user creation.
|
||||
|
||||
Args:
|
||||
setup_intent_id: SetupIntent ID to verify
|
||||
|
||||
Returns:
|
||||
Dictionary with SetupIntent verification result
|
||||
|
||||
Raises:
|
||||
Exception: If verification fails
|
||||
"""
|
||||
try:
|
||||
logger.info("Verifying SetupIntent for registration completion",
|
||||
setup_intent_id=setup_intent_id)
|
||||
|
||||
# Use payment service to verify SetupIntent
|
||||
verification_result = await self.payment_service.verify_setup_intent(setup_intent_id)
|
||||
|
||||
logger.info("SetupIntent verification result for registration",
|
||||
setup_intent_id=setup_intent_id,
|
||||
status=verification_result.get('status'))
|
||||
|
||||
return verification_result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("SetupIntent verification failed for registration",
|
||||
setup_intent_id=setup_intent_id,
|
||||
error=str(e),
|
||||
exc_info=True)
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user