Files
orion/models/schema/onboarding.py
Samir Boulahtit 409a2eaa05 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>
2025-12-27 21:46:26 +01:00

292 lines
8.3 KiB
Python

# models/schema/onboarding.py
"""
Pydantic schemas for Vendor Onboarding operations.
Schemas include:
- OnboardingStatusResponse: Current onboarding status with all step states
- CompanyProfileRequest/Response: Step 1 - Company profile data
- LetzshopApiConfigRequest/Response: Step 2 - API configuration
- ProductImportConfigRequest/Response: Step 3 - CSV URL configuration
- OrderSyncTriggerResponse: Step 4 - Job trigger response
- OrderSyncProgressResponse: Step 4 - Progress polling response
"""
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
# =============================================================================
# STEP STATUS MODELS
# =============================================================================
class StepStatus(BaseModel):
"""Status for a single onboarding step."""
completed: bool = False
completed_at: datetime | None = None
class CompanyProfileStepStatus(StepStatus):
"""Step 1 status with saved data."""
data: dict | None = None
class LetzshopApiStepStatus(StepStatus):
"""Step 2 status with connection verification."""
connection_verified: bool = False
class ProductImportStepStatus(StepStatus):
"""Step 3 status with CSV URL flag."""
csv_url_set: bool = False
class OrderSyncStepStatus(StepStatus):
"""Step 4 status with job tracking."""
job_id: int | None = None
# =============================================================================
# ONBOARDING STATUS RESPONSE
# =============================================================================
class OnboardingStatusResponse(BaseModel):
"""Full onboarding status with all step information."""
model_config = ConfigDict(from_attributes=True)
id: int
vendor_id: int
status: str # not_started, in_progress, completed, skipped
current_step: str # company_profile, letzshop_api, product_import, order_sync
# Step statuses
company_profile: CompanyProfileStepStatus
letzshop_api: LetzshopApiStepStatus
product_import: ProductImportStepStatus
order_sync: OrderSyncStepStatus
# Progress tracking
completion_percentage: int
completed_steps_count: int
total_steps: int = 4
# Completion info
is_completed: bool
started_at: datetime | None = None
completed_at: datetime | None = None
# Admin override info
skipped_by_admin: bool = False
skipped_at: datetime | None = None
skipped_reason: str | None = None
# =============================================================================
# STEP 1: COMPANY PROFILE
# =============================================================================
class CompanyProfileRequest(BaseModel):
"""Request to save company profile during onboarding Step 1."""
# Company name is already set during signup, but can be updated
company_name: str | None = Field(None, min_length=2, max_length=255)
# Vendor/brand name
brand_name: str | None = Field(None, min_length=2, max_length=255)
description: str | None = Field(None, max_length=2000)
# Contact information
contact_email: str | None = Field(None, max_length=255)
contact_phone: str | None = Field(None, max_length=50)
website: str | None = Field(None, max_length=255)
business_address: str | None = Field(None, max_length=500)
tax_number: str | None = Field(None, max_length=100)
# Language preferences
default_language: str = Field("fr", pattern="^(en|fr|de|lb)$")
dashboard_language: str = Field("fr", pattern="^(en|fr|de|lb)$")
class CompanyProfileResponse(BaseModel):
"""Response after saving company profile."""
success: bool
step_completed: bool
next_step: str | None = None
message: str | None = None
# =============================================================================
# STEP 2: LETZSHOP API CONFIGURATION
# =============================================================================
class LetzshopApiConfigRequest(BaseModel):
"""Request to configure Letzshop API credentials."""
api_key: str = Field(..., min_length=10, description="Letzshop API key")
shop_slug: str = Field(
...,
min_length=2,
max_length=100,
description="Letzshop shop URL slug (e.g., 'my-shop')",
)
vendor_id: str | None = Field(
None,
max_length=100,
description="Letzshop vendor ID (optional, auto-detected if not provided)",
)
class LetzshopApiTestRequest(BaseModel):
"""Request to test Letzshop API connection."""
api_key: str = Field(..., min_length=10)
shop_slug: str = Field(..., min_length=2, max_length=100)
class LetzshopApiTestResponse(BaseModel):
"""Response from Letzshop API connection test."""
success: bool
message: str
vendor_name: str | None = None
vendor_id: str | None = None
shop_slug: str | None = None
class LetzshopApiConfigResponse(BaseModel):
"""Response after saving Letzshop API configuration."""
success: bool
step_completed: bool
next_step: str | None = None
message: str | None = None
connection_verified: bool = False
# =============================================================================
# STEP 3: PRODUCT & ORDER IMPORT CONFIGURATION
# =============================================================================
class ProductImportConfigRequest(BaseModel):
"""Request to configure product import settings."""
# CSV feed URLs for each language
csv_url_fr: str | None = Field(None, max_length=500)
csv_url_en: str | None = Field(None, max_length=500)
csv_url_de: str | None = Field(None, max_length=500)
# Letzshop feed settings
default_tax_rate: int = Field(
17,
ge=0,
le=17,
description="Default VAT rate: 0, 3, 8, 14, or 17",
)
delivery_method: str = Field(
"package_delivery",
description="Delivery method: nationwide, package_delivery, self_collect",
)
preorder_days: int = Field(1, ge=0, le=30)
class ProductImportConfigResponse(BaseModel):
"""Response after saving product import configuration."""
success: bool
step_completed: bool
next_step: str | None = None
message: str | None = None
csv_urls_configured: int = 0
# =============================================================================
# STEP 4: ORDER SYNC
# =============================================================================
class OrderSyncTriggerRequest(BaseModel):
"""Request to trigger historical order import."""
# How far back to import orders (days)
days_back: int = Field(90, ge=1, le=365, description="Days of order history to import")
include_products: bool = Field(True, description="Also import products from Letzshop")
class OrderSyncTriggerResponse(BaseModel):
"""Response after triggering order sync job."""
success: bool
message: str
job_id: int | None = None
estimated_duration_minutes: int | None = None
class OrderSyncProgressResponse(BaseModel):
"""Response for order sync progress polling."""
job_id: int
status: str # pending, running, completed, failed
progress_percentage: int = 0
current_phase: str | None = None # products, orders, finalizing
# Counts
orders_imported: int = 0
orders_total: int | None = None
products_imported: int = 0
# Timing
started_at: datetime | None = None
completed_at: datetime | None = None
estimated_remaining_seconds: int | None = None
# Error info if failed
error_message: str | None = None
class OrderSyncCompleteRequest(BaseModel):
"""Request to mark order sync as complete."""
job_id: int
class OrderSyncCompleteResponse(BaseModel):
"""Response after completing order sync step."""
success: bool
step_completed: bool
onboarding_completed: bool = False
message: str | None = None
redirect_url: str | None = None
# =============================================================================
# ADMIN SKIP
# =============================================================================
class OnboardingSkipRequest(BaseModel):
"""Request to skip onboarding (admin only)."""
reason: str = Field(..., min_length=10, max_length=500)
class OnboardingSkipResponse(BaseModel):
"""Response after skipping onboarding."""
success: bool
message: str
vendor_id: int
skipped_at: datetime