Files
orion/app/api/v1/platform/signup.py
Samir Boulahtit 95987d0c1c refactor: move platform API database queries to service layer
- Create platform_signup_service.py for signup flow operations
- Create platform_pricing_service.py for pricing data operations
- Refactor signup.py, pricing.py, letzshop_vendors.py to use services
- Add # public markers to all platform endpoints
- Update tests for correct mock paths and status codes

Fixes architecture validation errors (API-002, API-003, SVC-006).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 13:30:52 +01:00

259 lines
6.7 KiB
Python

# app/api/v1/platform/signup.py
"""
Platform signup API endpoints.
Handles the multi-step signup flow:
1. Start signup (select tier)
2. Claim Letzshop vendor (optional)
3. Create account
4. Setup payment (collect card via SetupIntent)
5. Complete signup (create subscription with trial)
All endpoints are public (no authentication required).
"""
import logging
from fastapi import APIRouter, Depends
from pydantic import BaseModel, EmailStr
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.services.platform_signup_service import platform_signup_service
router = APIRouter()
logger = logging.getLogger(__name__)
# =============================================================================
# Request/Response Schemas
# =============================================================================
class SignupStartRequest(BaseModel):
"""Start signup - select tier."""
tier_code: str
is_annual: bool = False
class SignupStartResponse(BaseModel):
"""Response from signup start."""
session_id: str
tier_code: str
is_annual: bool
class ClaimVendorRequest(BaseModel):
"""Claim Letzshop vendor."""
session_id: str
letzshop_slug: str
letzshop_vendor_id: str | None = None
class ClaimVendorResponse(BaseModel):
"""Response from vendor claim."""
session_id: str
letzshop_slug: str
vendor_name: str | None
class CreateAccountRequest(BaseModel):
"""Create account."""
session_id: str
email: EmailStr
password: str
first_name: str
last_name: str
company_name: str
phone: str | None = None
class CreateAccountResponse(BaseModel):
"""Response from account creation."""
session_id: str
user_id: int
vendor_id: int
stripe_customer_id: 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
vendor_code: str
vendor_id: int
redirect_url: str
trial_ends_at: str
# =============================================================================
# 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 and billing period.
Creates a signup session to track the flow.
"""
session_id = platform_signup_service.create_session(
tier_code=request.tier_code,
is_annual=request.is_annual,
)
return SignupStartResponse(
session_id=session_id,
tier_code=request.tier_code,
is_annual=request.is_annual,
)
@router.post("/signup/claim-vendor", response_model=ClaimVendorResponse) # public
async def claim_letzshop_vendor(
request: ClaimVendorRequest,
db: Session = Depends(get_db),
) -> ClaimVendorResponse:
"""
Claim a Letzshop vendor.
Step 2 (optional): User claims their Letzshop shop.
This pre-fills vendor info during account creation.
"""
vendor_name = platform_signup_service.claim_vendor(
db=db,
session_id=request.session_id,
letzshop_slug=request.letzshop_slug,
letzshop_vendor_id=request.letzshop_vendor_id,
)
return ClaimVendorResponse(
session_id=request.session_id,
letzshop_slug=request.letzshop_slug,
vendor_name=vendor_name,
)
@router.post("/signup/create-account", response_model=CreateAccountResponse) # public
async def create_account(
request: CreateAccountRequest,
db: Session = Depends(get_db),
) -> CreateAccountResponse:
"""
Create user and vendor accounts.
Step 3: User provides account details.
Creates User, Company, Vendor, and Stripe Customer.
"""
result = platform_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,
company_name=request.company_name,
phone=request.phone,
)
return CreateAccountResponse(
session_id=request.session_id,
user_id=result.user_id,
vendor_id=result.vendor_id,
stripe_customer_id=result.stripe_customer_id,
)
@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 = platform_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,
db: Session = Depends(get_db),
) -> CompleteSignupResponse:
"""
Complete signup after card collection.
Step 5: Verify SetupIntent, attach payment method, create subscription.
"""
result = platform_signup_service.complete_signup(
db=db,
session_id=request.session_id,
setup_intent_id=request.setup_intent_id,
)
return CompleteSignupResponse(
success=result.success,
vendor_code=result.vendor_code,
vendor_id=result.vendor_id,
redirect_url=result.redirect_url,
trial_ends_at=result.trial_ends_at,
)
@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 = platform_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"),
"vendor_name": session.get("vendor_name"),
"created_at": session.get("created_at"),
}