Files
orion/app/api/v1/vendor/onboarding.py
Samir Boulahtit cad862f469 refactor(api): introduce UserContext schema for API dependency injection
Replace direct User database model imports in API endpoints with UserContext
schema, following the architecture principle that API routes should not import
database models directly.

Changes:
- Create UserContext schema in models/schema/auth.py with from_user() factory
- Update app/api/deps.py to return UserContext from all auth dependencies
- Add _get_user_model() helper for functions needing User model access
- Update 58 API endpoint files to use UserContext instead of User
- Add noqa comments for 4 legitimate edge cases (enums, internal helpers)

Architecture validation: 0 errors (down from 61), 11 warnings remain

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 20:47:33 +01:00

290 lines
9.0 KiB
Python

# 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, BackgroundTasks, Depends
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 OnboardingService
from app.tasks.letzshop_tasks import process_historical_import
from models.schema.auth import UserContext
from app.modules.marketplace.schemas 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: UserContext = 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: UserContext = 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: UserContext = 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)
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,
)
db.commit() # Commit at API level for transaction control
return result
# =============================================================================
# Step 2: Letzshop API Configuration
# =============================================================================
@router.post("/step/letzshop-api/test", response_model=LetzshopApiTestResponse)
def test_letzshop_api(
request: LetzshopApiTestRequest,
current_user: UserContext = 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: UserContext = 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)
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,
)
db.commit() # Commit at API level for transaction control
return result
# =============================================================================
# Step 3: Product & Order Import Configuration
# =============================================================================
@router.get("/step/product-import")
def get_product_import_config(
current_user: UserContext = 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: UserContext = 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)
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,
)
db.commit() # Commit at API level for transaction control
return result
# =============================================================================
# Step 4: Order Sync
# =============================================================================
@router.post("/step/order-sync/trigger", response_model=OrderSyncTriggerResponse)
def trigger_order_sync(
request: OrderSyncTriggerRequest,
background_tasks: BackgroundTasks,
current_user: UserContext = 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)
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,
)
db.commit() # Commit at API level for transaction control
# Queue background task to process the import
if result.get("success") and result.get("job_id"):
from app.tasks.dispatcher import task_dispatcher
celery_task_id = task_dispatcher.dispatch_historical_import(
background_tasks=background_tasks,
job_id=result["job_id"],
vendor_id=current_user.token_vendor_id,
)
# Store Celery task ID if using Celery
if celery_task_id:
from app.services.letzshop import LetzshopOrderService
order_service = LetzshopOrderService(db)
order_service.update_job_celery_task_id(result["job_id"], celery_task_id)
logger.info(f"Queued historical import task for job {result['job_id']}")
return result
@router.get(
"/step/order-sync/progress/{job_id}",
response_model=OrderSyncProgressResponse,
)
def get_order_sync_progress(
job_id: int,
current_user: UserContext = 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: UserContext = 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)
result = service.complete_order_sync(
vendor_id=current_user.token_vendor_id,
job_id=request.job_id,
)
db.commit() # Commit at API level for transaction control
return result