feat: add mandatory vendor onboarding wizard
Implement 4-step onboarding flow for new vendors after signup: - Step 1: Company profile setup - Step 2: Letzshop API configuration with connection testing - Step 3: Product & order import CSV URL configuration - Step 4: Historical order sync with progress bar Key features: - Blocks dashboard access until completed - Step indicators with visual progress - Resume capability (progress persisted in DB) - Admin skip capability for support cases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2
app/api/v1/vendor/__init__.py
vendored
2
app/api/v1/vendor/__init__.py
vendored
@@ -28,6 +28,7 @@ from . import (
|
||||
media,
|
||||
messages,
|
||||
notifications,
|
||||
onboarding,
|
||||
order_item_exceptions,
|
||||
orders,
|
||||
payments,
|
||||
@@ -56,6 +57,7 @@ router.include_router(auth.router, tags=["vendor-auth"])
|
||||
router.include_router(dashboard.router, tags=["vendor-dashboard"])
|
||||
router.include_router(profile.router, tags=["vendor-profile"])
|
||||
router.include_router(settings.router, tags=["vendor-settings"])
|
||||
router.include_router(onboarding.router, tags=["vendor-onboarding"])
|
||||
|
||||
# Business operations (with prefixes: /products/*, /orders/*, etc.)
|
||||
router.include_router(products.router, tags=["vendor-products"])
|
||||
|
||||
294
app/api/v1/vendor/onboarding.py
vendored
Normal file
294
app/api/v1/vendor/onboarding.py
vendored
Normal file
@@ -0,0 +1,294 @@
|
||||
# app/api/v1/vendor/onboarding.py
|
||||
"""
|
||||
Vendor onboarding API endpoints.
|
||||
|
||||
Provides endpoints for the 4-step mandatory onboarding wizard:
|
||||
1. Company Profile Setup
|
||||
2. Letzshop API Configuration
|
||||
3. Product & Order Import Configuration
|
||||
4. Order Sync (historical import)
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.services.onboarding_service import (
|
||||
OnboardingError,
|
||||
OnboardingService,
|
||||
OnboardingStepError,
|
||||
)
|
||||
from models.database.user import User
|
||||
from models.schema.onboarding import (
|
||||
CompanyProfileRequest,
|
||||
CompanyProfileResponse,
|
||||
LetzshopApiConfigRequest,
|
||||
LetzshopApiConfigResponse,
|
||||
LetzshopApiTestRequest,
|
||||
LetzshopApiTestResponse,
|
||||
OnboardingStatusResponse,
|
||||
OrderSyncCompleteRequest,
|
||||
OrderSyncCompleteResponse,
|
||||
OrderSyncProgressResponse,
|
||||
OrderSyncTriggerRequest,
|
||||
OrderSyncTriggerResponse,
|
||||
ProductImportConfigRequest,
|
||||
ProductImportConfigResponse,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/onboarding")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Status Endpoint
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("/status", response_model=OnboardingStatusResponse)
|
||||
def get_onboarding_status(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get current onboarding status.
|
||||
|
||||
Returns full status including all step completion states and progress.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
status = service.get_status_response(current_user.token_vendor_id)
|
||||
return status
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Step 1: Company Profile
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("/step/company-profile")
|
||||
def get_company_profile(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get current company profile data for editing.
|
||||
|
||||
Returns pre-filled data from vendor and company records.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
return service.get_company_profile_data(current_user.token_vendor_id)
|
||||
|
||||
|
||||
@router.post("/step/company-profile", response_model=CompanyProfileResponse)
|
||||
def save_company_profile(
|
||||
request: CompanyProfileRequest,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Save company profile and complete Step 1.
|
||||
|
||||
Updates vendor and company records with provided data.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
|
||||
try:
|
||||
result = service.complete_company_profile(
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
company_name=request.company_name,
|
||||
brand_name=request.brand_name,
|
||||
description=request.description,
|
||||
contact_email=request.contact_email,
|
||||
contact_phone=request.contact_phone,
|
||||
website=request.website,
|
||||
business_address=request.business_address,
|
||||
tax_number=request.tax_number,
|
||||
default_language=request.default_language,
|
||||
dashboard_language=request.dashboard_language,
|
||||
)
|
||||
return result
|
||||
except OnboardingError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Step 2: Letzshop API Configuration
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.post("/step/letzshop-api/test", response_model=LetzshopApiTestResponse)
|
||||
def test_letzshop_api(
|
||||
request: LetzshopApiTestRequest,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Test Letzshop API connection without saving.
|
||||
|
||||
Use this to validate API key before saving credentials.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
return service.test_letzshop_api(
|
||||
api_key=request.api_key,
|
||||
shop_slug=request.shop_slug,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/step/letzshop-api", response_model=LetzshopApiConfigResponse)
|
||||
def save_letzshop_api(
|
||||
request: LetzshopApiConfigRequest,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Save Letzshop API credentials and complete Step 2.
|
||||
|
||||
Tests connection first, only saves if successful.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
|
||||
try:
|
||||
result = service.complete_letzshop_api(
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
api_key=request.api_key,
|
||||
shop_slug=request.shop_slug,
|
||||
letzshop_vendor_id=request.vendor_id,
|
||||
)
|
||||
return result
|
||||
except OnboardingStepError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except OnboardingError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Step 3: Product & Order Import Configuration
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get("/step/product-import")
|
||||
def get_product_import_config(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get current product import configuration.
|
||||
|
||||
Returns pre-filled CSV URLs and Letzshop feed settings.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
return service.get_product_import_config(current_user.token_vendor_id)
|
||||
|
||||
|
||||
@router.post("/step/product-import", response_model=ProductImportConfigResponse)
|
||||
def save_product_import_config(
|
||||
request: ProductImportConfigRequest,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Save product import configuration and complete Step 3.
|
||||
|
||||
At least one CSV URL must be provided.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
|
||||
try:
|
||||
result = service.complete_product_import(
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
csv_url_fr=request.csv_url_fr,
|
||||
csv_url_en=request.csv_url_en,
|
||||
csv_url_de=request.csv_url_de,
|
||||
default_tax_rate=request.default_tax_rate,
|
||||
delivery_method=request.delivery_method,
|
||||
preorder_days=request.preorder_days,
|
||||
)
|
||||
return result
|
||||
except OnboardingStepError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except OnboardingError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Step 4: Order Sync
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.post("/step/order-sync/trigger", response_model=OrderSyncTriggerResponse)
|
||||
def trigger_order_sync(
|
||||
request: OrderSyncTriggerRequest,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Trigger historical order import.
|
||||
|
||||
Creates a background job that imports orders from Letzshop.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
|
||||
try:
|
||||
result = service.trigger_order_sync(
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
user_id=current_user.id,
|
||||
days_back=request.days_back,
|
||||
include_products=request.include_products,
|
||||
)
|
||||
return result
|
||||
except OnboardingStepError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except OnboardingError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/step/order-sync/progress/{job_id}",
|
||||
response_model=OrderSyncProgressResponse,
|
||||
)
|
||||
def get_order_sync_progress(
|
||||
job_id: int,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get order sync job progress.
|
||||
|
||||
Poll this endpoint to show progress bar during import.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
return service.get_order_sync_progress(
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
job_id=job_id,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/step/order-sync/complete", response_model=OrderSyncCompleteResponse)
|
||||
def complete_order_sync(
|
||||
request: OrderSyncCompleteRequest,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Mark order sync step as complete.
|
||||
|
||||
Called after the import job finishes (success or failure).
|
||||
This also marks the entire onboarding as complete.
|
||||
"""
|
||||
service = OnboardingService(db)
|
||||
|
||||
try:
|
||||
result = service.complete_order_sync(
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
job_id=request.job_id,
|
||||
)
|
||||
return result
|
||||
except OnboardingStepError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except OnboardingError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
Reference in New Issue
Block a user