# app/api/v1/platform/signup.py """ Platform signup API endpoints. Handles the multi-step signup flow: 1. Start signup (select tier + platform) 2. Create account (user + merchant) 3. Create store 4. Setup payment (collect card via SetupIntent) 5. Complete signup (create Stripe subscription with trial) Platform-specific steps (e.g., OMS Letzshop claiming) are handled by their respective modules and call into this core flow. All endpoints are public (no authentication required). """ import logging from fastapi import APIRouter, Depends, Response from pydantic import BaseModel, EmailStr from sqlalchemy.orm import Session from app.core.database import get_db from app.core.environment import should_use_secure_cookies from app.modules.billing.services.signup_service import signup_service router = APIRouter() logger = logging.getLogger(__name__) # ============================================================================= # Request/Response Schemas # ============================================================================= class SignupStartRequest(BaseModel): """Start signup - select tier and platform.""" tier_code: str is_annual: bool = False platform_code: str language: str = "fr" class SignupStartResponse(BaseModel): """Response from signup start.""" session_id: str tier_code: str is_annual: bool platform_code: str class CreateAccountRequest(BaseModel): """Create account (user + merchant).""" session_id: str email: EmailStr password: str first_name: str last_name: str merchant_name: str phone: str | None = None class CreateAccountResponse(BaseModel): """Response from account creation (includes auto-created store).""" session_id: str user_id: int merchant_id: int stripe_customer_id: str store_id: int store_code: str class CreateStoreRequest(BaseModel): """Create store for the merchant.""" session_id: str store_name: str | None = None language: str | None = None class CreateStoreResponse(BaseModel): """Response from store creation.""" session_id: str store_id: int store_code: str class SetupPaymentRequest(BaseModel): """Request payment setup.""" session_id: str class SetupPaymentResponse(BaseModel): """Response with Stripe SetupIntent client secret.""" session_id: str client_secret: str stripe_customer_id: str class CompleteSignupRequest(BaseModel): """Complete signup after card collection.""" session_id: str setup_intent_id: str class CompleteSignupResponse(BaseModel): """Response from signup completion.""" success: bool store_code: str store_id: int redirect_url: str trial_ends_at: str access_token: str | None = None # JWT token for automatic login # ============================================================================= # Endpoints # ============================================================================= @router.post("/signup/start", response_model=SignupStartResponse) # public async def start_signup(request: SignupStartRequest) -> SignupStartResponse: """ Start the signup process. Step 1: User selects a tier, billing period, and platform. Creates a signup session to track the flow. """ session_id = signup_service.create_session( tier_code=request.tier_code, is_annual=request.is_annual, platform_code=request.platform_code, language=request.language, ) return SignupStartResponse( session_id=session_id, tier_code=request.tier_code, is_annual=request.is_annual, platform_code=request.platform_code, ) @router.post("/signup/create-account", response_model=CreateAccountResponse) # public async def create_account( request: CreateAccountRequest, db: Session = Depends(get_db), ) -> CreateAccountResponse: """ Create user and merchant accounts. Step 2: User provides account details. Creates User, Merchant, and Stripe Customer. Store creation is a separate step. """ result = signup_service.create_account( db=db, session_id=request.session_id, email=request.email, password=request.password, first_name=request.first_name, last_name=request.last_name, merchant_name=request.merchant_name, phone=request.phone, ) return CreateAccountResponse( session_id=request.session_id, user_id=result.user_id, merchant_id=result.merchant_id, stripe_customer_id=result.stripe_customer_id, store_id=result.store_id, store_code=result.store_code, ) @router.post("/signup/create-store", response_model=CreateStoreResponse) # public async def create_store( request: CreateStoreRequest, db: Session = Depends(get_db), ) -> CreateStoreResponse: """ Create the first store for the merchant. Step 3: User names their store (defaults to merchant name). Creates Store, StorePlatform, and MerchantSubscription. """ result = signup_service.create_store( db=db, session_id=request.session_id, store_name=request.store_name, language=request.language, ) return CreateStoreResponse( session_id=request.session_id, store_id=result.store_id, store_code=result.store_code, ) @router.post("/signup/setup-payment", response_model=SetupPaymentResponse) # public async def setup_payment(request: SetupPaymentRequest) -> SetupPaymentResponse: """ Create Stripe SetupIntent for card collection. Step 4: Collect card details without charging. The card will be charged after the trial period ends. """ client_secret, stripe_customer_id = signup_service.setup_payment( session_id=request.session_id, ) return SetupPaymentResponse( session_id=request.session_id, client_secret=client_secret, stripe_customer_id=stripe_customer_id, ) @router.post("/signup/complete", response_model=CompleteSignupResponse) # public async def complete_signup( request: CompleteSignupRequest, response: Response, db: Session = Depends(get_db), ) -> CompleteSignupResponse: """ Complete signup after card collection. Step 5: Verify SetupIntent, attach payment method, create subscription. Also sets HTTP-only cookie for page navigation and returns token for localStorage. """ result = signup_service.complete_signup( db=db, session_id=request.session_id, setup_intent_id=request.setup_intent_id, ) # Set HTTP-only cookie for page navigation (same as login does) # This enables the user to access store pages immediately after signup if result.access_token: response.set_cookie( key="store_token", value=result.access_token, httponly=True, # JavaScript cannot access (XSS protection) secure=should_use_secure_cookies(), # HTTPS only in production/staging samesite="lax", # CSRF protection max_age=3600 * 24, # 24 hours path="/store", # RESTRICTED TO STORE ROUTES ONLY ) logger.info(f"Set store_token cookie for new store {result.store_code}") return CompleteSignupResponse( success=result.success, store_code=result.store_code, store_id=result.store_id, redirect_url=result.redirect_url, trial_ends_at=result.trial_ends_at, access_token=result.access_token, ) @router.get("/signup/session/{session_id}") # public async def get_signup_session(session_id: str) -> dict: """ Get signup session status. Useful for resuming an incomplete signup. """ session = signup_service.get_session_or_raise(session_id) # Return safe subset of session data return { "session_id": session_id, "step": session.get("step"), "tier_code": session.get("tier_code"), "is_annual": session.get("is_annual"), "letzshop_slug": session.get("letzshop_slug"), "store_name": session.get("store_name"), "created_at": session.get("created_at"), }