feat(billing): end-to-end Stripe subscription signup with platform enforcement
Move core signup service from marketplace to billing module, add automatic Stripe product/price sync for tiers, create loyalty-specific signup wizard, and enforce that platform is always explicitly known (no silent defaulting to primary/hardcoded ID). Key changes: - New billing SignupService with separated account/store creation steps - Stripe auto-sync on tier create/update (new prices, archive old) - Loyalty signup template (Plan → Account → Store → Payment) - platform_code is now required throughout the signup flow - Pricing/signup pages return 404 if platform not detected - OMS-specific logic (Letzshop claiming) stays in marketplace module - Bootstrap script: scripts/seed/sync_stripe_products.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,14 @@
|
||||
Platform signup API endpoints.
|
||||
|
||||
Handles the multi-step signup flow:
|
||||
1. Start signup (select tier)
|
||||
2. Claim Letzshop store (optional)
|
||||
3. Create account
|
||||
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 subscription with trial)
|
||||
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).
|
||||
"""
|
||||
@@ -20,9 +23,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.environment import should_use_secure_cookies
|
||||
from app.modules.marketplace.services.platform_signup_service import (
|
||||
platform_signup_service,
|
||||
)
|
||||
from app.modules.billing.services.signup_service import signup_service
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -34,10 +35,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SignupStartRequest(BaseModel):
|
||||
"""Start signup - select tier."""
|
||||
"""Start signup - select tier and platform."""
|
||||
|
||||
tier_code: str
|
||||
is_annual: bool = False
|
||||
platform_code: str
|
||||
|
||||
|
||||
class SignupStartResponse(BaseModel):
|
||||
@@ -46,26 +48,11 @@ class SignupStartResponse(BaseModel):
|
||||
session_id: str
|
||||
tier_code: str
|
||||
is_annual: bool
|
||||
|
||||
|
||||
class ClaimStoreRequest(BaseModel):
|
||||
"""Claim Letzshop store."""
|
||||
|
||||
session_id: str
|
||||
letzshop_slug: str
|
||||
letzshop_store_id: str | None = None
|
||||
|
||||
|
||||
class ClaimStoreResponse(BaseModel):
|
||||
"""Response from store claim."""
|
||||
|
||||
session_id: str
|
||||
letzshop_slug: str
|
||||
store_name: str | None
|
||||
platform_code: str
|
||||
|
||||
|
||||
class CreateAccountRequest(BaseModel):
|
||||
"""Create account."""
|
||||
"""Create account (user + merchant)."""
|
||||
|
||||
session_id: str
|
||||
email: EmailStr
|
||||
@@ -81,10 +68,26 @@ class CreateAccountResponse(BaseModel):
|
||||
|
||||
session_id: str
|
||||
user_id: int
|
||||
store_id: int
|
||||
merchant_id: int
|
||||
stripe_customer_id: 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."""
|
||||
|
||||
@@ -127,43 +130,20 @@ async def start_signup(request: SignupStartRequest) -> SignupStartResponse:
|
||||
"""
|
||||
Start the signup process.
|
||||
|
||||
Step 1: User selects a tier and billing period.
|
||||
Step 1: User selects a tier, billing period, and platform.
|
||||
Creates a signup session to track the flow.
|
||||
"""
|
||||
session_id = platform_signup_service.create_session(
|
||||
session_id = signup_service.create_session(
|
||||
tier_code=request.tier_code,
|
||||
is_annual=request.is_annual,
|
||||
platform_code=request.platform_code,
|
||||
)
|
||||
|
||||
return SignupStartResponse(
|
||||
session_id=session_id,
|
||||
tier_code=request.tier_code,
|
||||
is_annual=request.is_annual,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/signup/claim-store", response_model=ClaimStoreResponse) # public
|
||||
async def claim_letzshop_store(
|
||||
request: ClaimStoreRequest,
|
||||
db: Session = Depends(get_db),
|
||||
) -> ClaimStoreResponse:
|
||||
"""
|
||||
Claim a Letzshop store.
|
||||
|
||||
Step 2 (optional): User claims their Letzshop shop.
|
||||
This pre-fills store info during account creation.
|
||||
"""
|
||||
store_name = platform_signup_service.claim_store(
|
||||
db=db,
|
||||
session_id=request.session_id,
|
||||
letzshop_slug=request.letzshop_slug,
|
||||
letzshop_store_id=request.letzshop_store_id,
|
||||
)
|
||||
|
||||
return ClaimStoreResponse(
|
||||
session_id=request.session_id,
|
||||
letzshop_slug=request.letzshop_slug,
|
||||
store_name=store_name,
|
||||
platform_code=request.platform_code,
|
||||
)
|
||||
|
||||
|
||||
@@ -173,12 +153,13 @@ async def create_account(
|
||||
db: Session = Depends(get_db),
|
||||
) -> CreateAccountResponse:
|
||||
"""
|
||||
Create user and store accounts.
|
||||
Create user and merchant accounts.
|
||||
|
||||
Step 3: User provides account details.
|
||||
Creates User, Merchant, Store, and Stripe Customer.
|
||||
Step 2: User provides account details.
|
||||
Creates User, Merchant, and Stripe Customer.
|
||||
Store creation is a separate step.
|
||||
"""
|
||||
result = platform_signup_service.create_account(
|
||||
result = signup_service.create_account(
|
||||
db=db,
|
||||
session_id=request.session_id,
|
||||
email=request.email,
|
||||
@@ -192,11 +173,36 @@ async def create_account(
|
||||
return CreateAccountResponse(
|
||||
session_id=request.session_id,
|
||||
user_id=result.user_id,
|
||||
store_id=result.store_id,
|
||||
merchant_id=result.merchant_id,
|
||||
stripe_customer_id=result.stripe_customer_id,
|
||||
)
|
||||
|
||||
|
||||
@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:
|
||||
"""
|
||||
@@ -205,7 +211,7 @@ async def setup_payment(request: SetupPaymentRequest) -> SetupPaymentResponse:
|
||||
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(
|
||||
client_secret, stripe_customer_id = signup_service.setup_payment(
|
||||
session_id=request.session_id,
|
||||
)
|
||||
|
||||
@@ -228,7 +234,7 @@ async def complete_signup(
|
||||
Step 5: Verify SetupIntent, attach payment method, create subscription.
|
||||
Also sets HTTP-only cookie for page navigation and returns token for localStorage.
|
||||
"""
|
||||
result = platform_signup_service.complete_signup(
|
||||
result = signup_service.complete_signup(
|
||||
db=db,
|
||||
session_id=request.session_id,
|
||||
setup_intent_id=request.setup_intent_id,
|
||||
@@ -265,7 +271,7 @@ async def get_signup_session(session_id: str) -> dict:
|
||||
|
||||
Useful for resuming an incomplete signup.
|
||||
"""
|
||||
session = platform_signup_service.get_session_or_raise(session_id)
|
||||
session = signup_service.get_session_or_raise(session_id)
|
||||
|
||||
# Return safe subset of session data
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user