Add subcription feature 2
This commit is contained in:
@@ -200,8 +200,8 @@ async def clone_demo_data(
|
||||
session_time,
|
||||
"next_billing_date"
|
||||
),
|
||||
stripe_subscription_id=subscription_data.get('stripe_subscription_id'),
|
||||
stripe_customer_id=subscription_data.get('stripe_customer_id'),
|
||||
subscription_id=subscription_data.get('stripe_subscription_id'),
|
||||
customer_id=subscription_data.get('stripe_customer_id'),
|
||||
cancelled_at=parse_date_field(
|
||||
subscription_data.get('cancelled_at'),
|
||||
session_time,
|
||||
|
||||
@@ -867,7 +867,7 @@ async def register_with_subscription(
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Registration and subscription created successfully",
|
||||
"data": result
|
||||
**result
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to register with subscription", error=str(e))
|
||||
@@ -924,7 +924,7 @@ async def create_subscription_endpoint(
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Subscription created successfully",
|
||||
"data": result
|
||||
**result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -975,16 +975,30 @@ async def create_subscription_for_registration(
|
||||
request.billing_interval,
|
||||
request.coupon_code
|
||||
)
|
||||
|
||||
|
||||
# Check if result requires SetupIntent confirmation (3DS)
|
||||
if result.get('requires_action'):
|
||||
logger.info("Subscription creation requires SetupIntent confirmation",
|
||||
user_id=request.user_data.get('user_id'),
|
||||
action_type=result.get('action_type'),
|
||||
setup_intent_id=result.get('setup_intent_id'))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Payment method verification required",
|
||||
**result # Spread all result fields to top level for frontend compatibility
|
||||
}
|
||||
|
||||
# Normal subscription creation (no 3DS)
|
||||
logger.info("Tenant-independent subscription created successfully",
|
||||
user_id=request.user_data.get('user_id'),
|
||||
subscription_id=result["subscription_id"],
|
||||
subscription_id=result.get("subscription_id"),
|
||||
plan_id=request.plan_id)
|
||||
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Tenant-independent subscription created successfully",
|
||||
"data": result
|
||||
**result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -998,6 +1012,136 @@ async def create_subscription_for_registration(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/v1/subscriptions/complete-after-setup-intent")
|
||||
async def complete_subscription_after_setup_intent(
|
||||
request: dict = Body(..., description="Completion request with setup_intent_id"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Complete subscription creation after SetupIntent confirmation
|
||||
|
||||
This endpoint is called by the frontend after successfully confirming a SetupIntent
|
||||
(with or without 3DS). It verifies the SetupIntent and creates the subscription.
|
||||
|
||||
Request body should contain:
|
||||
- setup_intent_id: The SetupIntent ID that was confirmed
|
||||
- customer_id: Stripe customer ID (from initial response)
|
||||
- plan_id: Subscription plan ID
|
||||
- payment_method_id: Payment method ID
|
||||
- trial_period_days: Optional trial period
|
||||
- user_id: User ID (for linking subscription to user)
|
||||
"""
|
||||
try:
|
||||
setup_intent_id = request.get('setup_intent_id')
|
||||
customer_id = request.get('customer_id')
|
||||
plan_id = request.get('plan_id')
|
||||
payment_method_id = request.get('payment_method_id')
|
||||
trial_period_days = request.get('trial_period_days')
|
||||
user_id = request.get('user_id')
|
||||
billing_interval = request.get('billing_interval', 'monthly')
|
||||
|
||||
if not all([setup_intent_id, customer_id, plan_id, payment_method_id, user_id]):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Missing required fields: setup_intent_id, customer_id, plan_id, payment_method_id, user_id"
|
||||
)
|
||||
|
||||
logger.info("Completing subscription after SetupIntent confirmation",
|
||||
setup_intent_id=setup_intent_id,
|
||||
user_id=user_id,
|
||||
plan_id=plan_id)
|
||||
|
||||
# Use orchestration service to complete subscription
|
||||
orchestration_service = SubscriptionOrchestrationService(db)
|
||||
|
||||
result = await orchestration_service.complete_subscription_after_setup_intent(
|
||||
setup_intent_id=setup_intent_id,
|
||||
customer_id=customer_id,
|
||||
plan_id=plan_id,
|
||||
payment_method_id=payment_method_id,
|
||||
trial_period_days=trial_period_days,
|
||||
user_id=user_id,
|
||||
billing_interval=billing_interval
|
||||
)
|
||||
|
||||
logger.info("Subscription completed successfully after SetupIntent",
|
||||
setup_intent_id=setup_intent_id,
|
||||
subscription_id=result.get('subscription_id'),
|
||||
user_id=user_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Subscription created successfully after SetupIntent confirmation",
|
||||
**result
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Failed to complete subscription after SetupIntent",
|
||||
error=str(e),
|
||||
setup_intent_id=request.get('setup_intent_id'))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to complete subscription: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/subscriptions/{tenant_id}/payment-method")
|
||||
async def get_payment_method(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get the current payment method for a subscription
|
||||
|
||||
This endpoint retrieves the current payment method details from the payment provider
|
||||
for display in the UI, including brand, last4 digits, and expiration date.
|
||||
"""
|
||||
try:
|
||||
# Use SubscriptionOrchestrationService to get payment method
|
||||
orchestration_service = SubscriptionOrchestrationService(db)
|
||||
|
||||
payment_method = await orchestration_service.get_payment_method(tenant_id)
|
||||
|
||||
if not payment_method:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="No payment method found for this subscription"
|
||||
)
|
||||
|
||||
logger.info("payment_method_retrieved_via_api",
|
||||
tenant_id=tenant_id,
|
||||
user_id=current_user.get("user_id"))
|
||||
|
||||
return payment_method
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValidationError as ve:
|
||||
logger.error("get_payment_method_validation_failed",
|
||||
error=str(ve), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(ve)
|
||||
)
|
||||
except DatabaseError as de:
|
||||
logger.error("get_payment_method_failed",
|
||||
error=str(de), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve payment method"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("get_payment_method_unexpected_error",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="An unexpected error occurred while retrieving payment method"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/v1/subscriptions/{tenant_id}/update-payment-method")
|
||||
async def update_payment_method(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
@@ -1007,60 +1151,43 @@ async def update_payment_method(
|
||||
):
|
||||
"""
|
||||
Update the default payment method for a subscription
|
||||
|
||||
|
||||
This endpoint allows users to change their payment method through the UI.
|
||||
It updates the default payment method in Stripe and returns the updated
|
||||
payment method information.
|
||||
It updates the default payment method with the payment provider and returns
|
||||
the updated payment method information.
|
||||
"""
|
||||
try:
|
||||
# Use SubscriptionService to get subscription and update payment method
|
||||
subscription_service = SubscriptionService(db)
|
||||
|
||||
# Get current subscription
|
||||
subscription = await 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.stripe_customer_id:
|
||||
raise ValidationError(f"Tenant {tenant_id} does not have a Stripe customer ID")
|
||||
|
||||
# Update payment method via PaymentService
|
||||
payment_result = await subscription_service.payment_service.update_payment_method(
|
||||
subscription.stripe_customer_id,
|
||||
# Use SubscriptionOrchestrationService to update payment method
|
||||
orchestration_service = SubscriptionOrchestrationService(db)
|
||||
|
||||
result = await orchestration_service.update_payment_method(
|
||||
tenant_id,
|
||||
payment_method_id
|
||||
)
|
||||
|
||||
|
||||
logger.info("Payment method updated successfully",
|
||||
tenant_id=tenant_id,
|
||||
payment_method_id=payment_method_id,
|
||||
user_id=current_user.get("user_id"))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Payment method updated successfully",
|
||||
"payment_method_id": payment_result.id,
|
||||
"brand": getattr(payment_result, 'brand', 'unknown'),
|
||||
"last4": getattr(payment_result, 'last4', '0000'),
|
||||
"exp_month": getattr(payment_result, 'exp_month', None),
|
||||
"exp_year": getattr(payment_result, 'exp_year', None)
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
|
||||
except ValidationError as ve:
|
||||
logger.error("update_payment_method_validation_failed",
|
||||
logger.error("update_payment_method_validation_failed",
|
||||
error=str(ve), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(ve)
|
||||
)
|
||||
except DatabaseError as de:
|
||||
logger.error("update_payment_method_failed",
|
||||
logger.error("update_payment_method_failed",
|
||||
error=str(de), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update payment method"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("update_payment_method_unexpected_error",
|
||||
logger.error("update_payment_method_unexpected_error",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -1377,3 +1504,118 @@ async def redeem_coupon(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="An unexpected error occurred while redeeming coupon"
|
||||
)
|
||||
|
||||
|
||||
# NEW ENDPOINTS FOR SECURE REGISTRATION ARCHITECTURE
|
||||
|
||||
class PaymentCustomerCreationRequest(BaseModel):
|
||||
"""Request model for payment customer creation (pre-user-creation)"""
|
||||
user_data: Dict[str, Any]
|
||||
payment_method_id: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/payment-customers/create")
|
||||
async def create_payment_customer_for_registration(
|
||||
request: PaymentCustomerCreationRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Create payment customer (supports pre-user-creation flow)
|
||||
|
||||
This endpoint creates a payment customer without requiring a user_id,
|
||||
supporting the secure architecture where users are only created after
|
||||
payment verification.
|
||||
|
||||
Uses SubscriptionOrchestrationService for proper workflow coordination.
|
||||
|
||||
Args:
|
||||
request: Payment customer creation request
|
||||
|
||||
Returns:
|
||||
Dictionary with payment customer creation result
|
||||
"""
|
||||
try:
|
||||
logger.info("Creating payment customer for registration (pre-user creation)",
|
||||
email=request.user_data.get('email'),
|
||||
payment_method_id=request.payment_method_id)
|
||||
|
||||
# Use orchestration service for proper workflow coordination
|
||||
orchestration_service = SubscriptionOrchestrationService(db)
|
||||
|
||||
result = await orchestration_service.create_registration_payment_setup(
|
||||
user_data=request.user_data,
|
||||
plan_id=request.plan_id if hasattr(request, 'plan_id') else "professional",
|
||||
payment_method_id=request.payment_method_id,
|
||||
billing_interval="monthly", # Default for registration
|
||||
coupon_code=request.user_data.get('coupon_code')
|
||||
)
|
||||
|
||||
logger.info("Payment setup completed for registration",
|
||||
email=request.user_data.get('email'),
|
||||
requires_action=result.get('requires_action'),
|
||||
setup_intent_id=result.get('setup_intent_id'))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
**result # Include all orchestration service results
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to create payment customer for registration",
|
||||
email=request.user_data.get('email'),
|
||||
error=str(e),
|
||||
exc_info=True)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to create payment customer: " + str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/setup-intents/{setup_intent_id}/verify")
|
||||
async def verify_setup_intent(
|
||||
setup_intent_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Verify SetupIntent status with payment provider
|
||||
|
||||
This endpoint checks if a SetupIntent has been successfully confirmed
|
||||
(either automatically or via 3DS authentication).
|
||||
|
||||
Uses SubscriptionOrchestrationService for proper workflow coordination.
|
||||
|
||||
Args:
|
||||
setup_intent_id: SetupIntent ID to verify
|
||||
|
||||
Returns:
|
||||
Dictionary with SetupIntent verification result
|
||||
"""
|
||||
try:
|
||||
logger.info("Verifying SetupIntent status",
|
||||
setup_intent_id=setup_intent_id)
|
||||
|
||||
# Use orchestration service for proper workflow coordination
|
||||
orchestration_service = SubscriptionOrchestrationService(db)
|
||||
|
||||
# Verify SetupIntent using orchestration service
|
||||
result = await orchestration_service.verify_setup_intent_for_registration(
|
||||
setup_intent_id
|
||||
)
|
||||
|
||||
logger.info("SetupIntent verification result",
|
||||
setup_intent_id=setup_intent_id,
|
||||
status=result.get('status'))
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to verify SetupIntent",
|
||||
setup_intent_id=setup_intent_id,
|
||||
error=str(e),
|
||||
exc_info=True)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to verify SetupIntent: " + str(e)
|
||||
)
|
||||
|
||||
@@ -1138,7 +1138,7 @@ async def register_with_subscription(
|
||||
):
|
||||
"""Process user registration with subscription creation"""
|
||||
|
||||
@router.post(route_builder.build_base_route("payment-customers/create", include_tenant_prefix=False))
|
||||
@router.post("/api/v1/payment-customers/create")
|
||||
async def create_payment_customer(
|
||||
user_data: Dict[str, Any],
|
||||
payment_method_id: Optional[str] = Query(None, description="Optional payment method ID"),
|
||||
@@ -1146,7 +1146,7 @@ async def create_payment_customer(
|
||||
):
|
||||
"""
|
||||
Create a payment customer in the payment provider
|
||||
|
||||
|
||||
This endpoint is designed for service-to-service communication from auth service
|
||||
during user registration. It creates a payment customer that can be used later
|
||||
for subscription creation.
|
||||
@@ -1241,7 +1241,7 @@ async def register_with_subscription(
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Registration and subscription created successfully",
|
||||
"data": result
|
||||
**result
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to register with subscription", error=str(e))
|
||||
@@ -1291,7 +1291,7 @@ async def link_subscription_to_tenant(
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Subscription linked to tenant successfully",
|
||||
"data": result
|
||||
**result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user