From fa7b62bd6ca349e577f3462a5f9c6189d495d98f Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Fri, 16 Jan 2026 16:09:32 +0100 Subject: [PATCH] Add subcription feature 8 --- services/auth/app/api/auth_operations.py | 65 ++++++++++----- services/auth/app/services/auth_service.py | 97 ++++++++++++++++------ 2 files changed, 115 insertions(+), 47 deletions(-) diff --git a/services/auth/app/api/auth_operations.py b/services/auth/app/api/auth_operations.py index 9e7f0969..03e9e8e5 100644 --- a/services/auth/app/api/auth_operations.py +++ b/services/auth/app/api/auth_operations.py @@ -39,20 +39,30 @@ async def start_registration( """ Start secure registration flow with SetupIntent-first approach - This is the FIRST step in the new registration architecture: - 1. Creates payment customer - 2. Attaches payment method - 3. Creates SetupIntent for verification - 4. Returns SetupIntent to frontend for 3DS handling + This is the FIRST step in the atomic registration architecture: + 1. Creates Stripe customer via tenant service + 2. Creates SetupIntent with confirm=True + 3. Returns SetupIntent data to frontend - If 3DS is required, frontend must confirm SetupIntent and call complete-registration - If no 3DS required, user is created immediately and registration completes + IMPORTANT: NO subscription or user is created in this step! + + Two possible outcomes: + - requires_action=True: 3DS required, frontend must confirm SetupIntent then call complete-registration + - requires_action=False: No 3DS required, but frontend STILL must call complete-registration + + In BOTH cases, the frontend must call complete-registration to create the subscription and user. + This ensures consistent flow and prevents duplicate subscriptions. Args: user_data: User registration data with payment info Returns: - Registration result (may require 3DS) + SetupIntent result with: + - requires_action: True if 3DS required, False if not + - setup_intent_id: SetupIntent ID for verification + - client_secret: For 3DS authentication (when requires_action=True) + - customer_id: Stripe customer ID + - Other SetupIntent metadata Raises: HTTPException: 400 for validation errors, 500 for server errors @@ -124,6 +134,7 @@ async def start_registration( return { "requires_action": False, + "setup_intent_id": result.get('setup_intent_id'), "user": user_data_response, "subscription_id": result.get('subscription_id'), "payment_customer_id": result.get('payment_customer_id'), @@ -156,31 +167,45 @@ async def start_registration( @router.post("/complete-registration", response_model=Dict[str, Any], - summary="Complete registration after 3DS verification") + summary="Complete registration after SetupIntent verification") async def complete_registration( verification_data: Dict[str, Any], request: Request, auth_service: AuthService = Depends(get_auth_service) ) -> Dict[str, Any]: """ - Complete registration after frontend confirms SetupIntent (3DS handled) + Complete registration after frontend confirms SetupIntent - This is the SECOND step in the registration architecture: - 1. Called after frontend confirms SetupIntent - 2. Called after user completes 3DS authentication (if required) - 3. Verifies SetupIntent status - 4. Creates subscription with verified payment method - 5. Creates user record - 6. Saves onboarding progress + This is the SECOND step in the atomic registration architecture: + 1. Called after frontend confirms SetupIntent (with or without 3DS) + 2. Verifies SetupIntent status with Stripe + 3. Creates subscription with verified payment method (FIRST time subscription is created) + 4. Creates user record in auth database + 5. Saves onboarding progress + 6. Generates auth tokens for auto-login + + This endpoint is called in TWO scenarios: + 1. After user completes 3DS authentication (requires_action=True flow) + 2. Immediately after start-registration (requires_action=False flow) + + In BOTH cases, this is where the subscription and user are actually created. + This ensures consistent flow and prevents duplicate subscriptions. Args: - verification_data: SetupIntent verification data + verification_data: Must contain: + - setup_intent_id: Verified SetupIntent ID + - user_data: Original user registration data Returns: - Complete registration result + Complete registration result with: + - user: Created user data + - subscription_id: Created subscription ID + - payment_customer_id: Stripe customer ID + - access_token: JWT access token + - refresh_token: JWT refresh token Raises: - HTTPException: 400 for validation errors, 500 for server errors + HTTPException: 400 if setup_intent_id is missing, 500 for server errors """ try: # Validate required fields diff --git a/services/auth/app/services/auth_service.py b/services/auth/app/services/auth_service.py index bb48c201..24b86242 100644 --- a/services/auth/app/services/auth_service.py +++ b/services/auth/app/services/auth_service.py @@ -130,20 +130,37 @@ class AuthService: ) -> Dict[str, Any]: """ Complete registration after successful payment verification. - - NEW ARCHITECTURE: This calls tenant service to create subscription - AFTER SetupIntent verification. No subscription exists until this point. + + This is the CORE method that creates the subscription and user. + It is called by complete_registration_with_verified_payment in both scenarios: + 1. After 3DS authentication (requires_action=True flow) + 2. Immediately after start-registration (requires_action=False flow) + + CRITICAL: This is the FIRST and ONLY place where: + - Subscription is created via tenant service + - User record is created in auth database + - Onboarding progress is saved + - Auth tokens are generated + + This ensures that subscriptions are only created after SetupIntent verification, + preventing duplicate subscriptions and maintaining payment security. Args: - setup_intent_id: Verified SetupIntent ID + setup_intent_id: Verified SetupIntent ID (must not be None) user_data: User registration data payment_setup_result: Optional payment setup result with customer_id etc. Returns: - Complete registration result + Complete registration result with: + - user: Created user data + - subscription_id: Created subscription ID + - payment_customer_id: Stripe customer ID + - status: Subscription status + - access_token: JWT access token + - refresh_token: JWT refresh token Raises: - RegistrationError: If registration completion fails + RegistrationError: If registration completion fails (e.g., missing setup_intent_id) """ try: logger.info(f"Completing registration after verification, email={user_data.email}, setup_intent_id={setup_intent_id}") @@ -250,13 +267,28 @@ class AuthService: ) -> Dict[str, Any]: """ Start secure registration flow with SetupIntent-first approach - Main entry point for new registration architecture + + This is the FIRST step in the atomic registration flow: + 1. Creates Stripe customer + 2. Creates SetupIntent with confirm=True + 3. Returns SetupIntent data to frontend + + IMPORTANT: NO subscription is created in this step! + Subscription creation happens in complete_registration_after_payment_verification + + Two possible outcomes: + - requires_action=True: 3DS required, frontend must confirm SetupIntent + - requires_action=False: No 3DS required, but frontend STILL must call complete-registration Args: user_data: User registration data Returns: - Registration flow result (may require 3DS) + Registration flow result with SetupIntent data + - requires_action: True if 3DS required, False if not + - setup_intent_id: SetupIntent ID for verification + - client_secret: For 3DS authentication (when requires_action=True) + - Other SetupIntent metadata Raises: RegistrationError: If registration flow fails @@ -292,23 +324,20 @@ class AuthService: # No 3DS required - SetupIntent already succeeded logger.info(f"Registration SetupIntent succeeded without 3DS, email={user_data.email}, setup_intent_id={payment_setup_result.get('setup_intent_id')}") - # Complete registration - create subscription now - setup_intent_id = payment_setup_result.get('setup_intent_id') - registration_result = await self.complete_registration_after_payment_verification( - setup_intent_id, - user_data, - payment_setup_result - ) - + # Note: NO subscription created yet - frontend must call complete-registration + # This ensures consistent flow whether 3DS is required or not return { 'requires_action': False, - 'user': registration_result.get('user'), - 'subscription_id': registration_result.get('subscription_id'), - 'payment_customer_id': registration_result.get('payment_customer_id'), - 'status': registration_result.get('status'), - 'access_token': registration_result.get('access_token'), - 'refresh_token': registration_result.get('refresh_token'), - 'message': 'Registration completed successfully' + 'setup_intent_id': payment_setup_result.get('setup_intent_id'), + 'customer_id': payment_setup_result.get('customer_id'), + 'payment_customer_id': payment_setup_result.get('payment_customer_id'), + 'plan_id': payment_setup_result.get('plan_id'), + 'payment_method_id': payment_setup_result.get('payment_method_id'), + 'billing_cycle': payment_setup_result.get('billing_cycle'), + 'trial_period_days': payment_setup_result.get('trial_period_days', 0), + 'coupon_code': payment_setup_result.get('coupon_code'), + 'email': payment_setup_result.get('email'), + 'message': 'SetupIntent succeeded without 3DS. Frontend must call complete-registration to create subscription.' } except Exception as e: @@ -321,15 +350,29 @@ class AuthService: user_data: UserRegistration ) -> Dict[str, Any]: """ - Complete registration after frontend confirms SetupIntent (3DS handled) - This is called by frontend after user completes 3DS authentication + Complete registration after frontend confirms SetupIntent + + This is the SECOND step in the atomic registration flow and is called in TWO scenarios: + 1. After user completes 3DS authentication (when requires_action=True) + 2. Immediately after start-registration (when requires_action=False) + + In BOTH cases, this method: + - Verifies the SetupIntent status with Stripe + - Calls tenant service to create the subscription (first time subscription is created) + - Creates the user record in auth database + - Saves onboarding progress + - Generates auth tokens for auto-login + + This ensures a CONSISTENT flow whether 3DS is required or not: + Step 1: start-registration creates SetupIntent only (NO subscription yet) + Step 2: complete-registration verifies SetupIntent and creates subscription Args: - setup_intent_id: SetupIntent ID that was confirmed + setup_intent_id: SetupIntent ID that was confirmed (either by Stripe or frontend) user_data: User registration data Returns: - Complete registration result + Complete registration result with user data, tokens, and subscription info Raises: RegistrationError: If completion fails