Files
orion/models/database/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

223 lines
7.4 KiB
Python

# models/database/onboarding.py
"""
Vendor onboarding progress tracking.
Tracks completion status of mandatory onboarding steps for new vendors.
Onboarding must be completed before accessing the vendor dashboard.
"""
import enum
from datetime import datetime
from sqlalchemy import (
Boolean,
Column,
DateTime,
ForeignKey,
Index,
Integer,
String,
Text,
)
from sqlalchemy.dialects.sqlite import JSON
from sqlalchemy.orm import relationship
from app.core.database import Base
from .base import TimestampMixin
class OnboardingStep(str, enum.Enum):
"""Onboarding step identifiers."""
COMPANY_PROFILE = "company_profile"
LETZSHOP_API = "letzshop_api"
PRODUCT_IMPORT = "product_import"
ORDER_SYNC = "order_sync"
class OnboardingStatus(str, enum.Enum):
"""Overall onboarding status."""
NOT_STARTED = "not_started"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
SKIPPED = "skipped" # For admin override capability
# Step order for validation
STEP_ORDER = [
OnboardingStep.COMPANY_PROFILE,
OnboardingStep.LETZSHOP_API,
OnboardingStep.PRODUCT_IMPORT,
OnboardingStep.ORDER_SYNC,
]
class VendorOnboarding(Base, TimestampMixin):
"""
Per-vendor onboarding progress tracking.
Created automatically when vendor is created during signup.
Blocks dashboard access until status = 'completed' or skipped_by_admin = True.
"""
__tablename__ = "vendor_onboarding"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(
Integer,
ForeignKey("vendors.id", ondelete="CASCADE"),
unique=True,
nullable=False,
index=True,
)
# Overall status
status = Column(
String(20),
default=OnboardingStatus.NOT_STARTED.value,
nullable=False,
index=True,
)
current_step = Column(
String(30),
default=OnboardingStep.COMPANY_PROFILE.value,
nullable=False,
)
# Step 1: Company Profile
step_company_profile_completed = Column(Boolean, default=False, nullable=False)
step_company_profile_completed_at = Column(DateTime(timezone=True), nullable=True)
step_company_profile_data = Column(JSON, nullable=True) # Store what was entered
# Step 2: Letzshop API Configuration
step_letzshop_api_completed = Column(Boolean, default=False, nullable=False)
step_letzshop_api_completed_at = Column(DateTime(timezone=True), nullable=True)
step_letzshop_api_connection_verified = Column(Boolean, default=False, nullable=False)
# Step 3: Product & Order Import (CSV feed URL + historical import)
step_product_import_completed = Column(Boolean, default=False, nullable=False)
step_product_import_completed_at = Column(DateTime(timezone=True), nullable=True)
step_product_import_csv_url_set = Column(Boolean, default=False, nullable=False)
# Step 4: Order Sync
step_order_sync_completed = Column(Boolean, default=False, nullable=False)
step_order_sync_completed_at = Column(DateTime(timezone=True), nullable=True)
step_order_sync_job_id = Column(Integer, nullable=True) # FK to LetzshopHistoricalImportJob
# Completion tracking
started_at = Column(DateTime(timezone=True), nullable=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
# Admin override (for support cases)
skipped_by_admin = Column(Boolean, default=False, nullable=False)
skipped_at = Column(DateTime(timezone=True), nullable=True)
skipped_reason = Column(Text, nullable=True)
skipped_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
# Relationships
vendor = relationship("Vendor", back_populates="onboarding")
__table_args__ = (
Index("idx_onboarding_vendor_status", "vendor_id", "status"),
{"sqlite_autoincrement": True},
)
def __repr__(self):
return f"<VendorOnboarding(vendor_id={self.vendor_id}, status='{self.status}', step='{self.current_step}')>"
@property
def is_completed(self) -> bool:
"""Check if onboarding is fully completed or skipped."""
return (
self.status == OnboardingStatus.COMPLETED.value or self.skipped_by_admin
)
@property
def completion_percentage(self) -> int:
"""Calculate completion percentage (0-100)."""
completed_steps = sum(
[
self.step_company_profile_completed,
self.step_letzshop_api_completed,
self.step_product_import_completed,
self.step_order_sync_completed,
]
)
return int((completed_steps / 4) * 100)
@property
def completed_steps_count(self) -> int:
"""Get number of completed steps."""
return sum(
[
self.step_company_profile_completed,
self.step_letzshop_api_completed,
self.step_product_import_completed,
self.step_order_sync_completed,
]
)
def is_step_completed(self, step: str) -> bool:
"""Check if a specific step is completed."""
step_mapping = {
OnboardingStep.COMPANY_PROFILE.value: self.step_company_profile_completed,
OnboardingStep.LETZSHOP_API.value: self.step_letzshop_api_completed,
OnboardingStep.PRODUCT_IMPORT.value: self.step_product_import_completed,
OnboardingStep.ORDER_SYNC.value: self.step_order_sync_completed,
}
return step_mapping.get(step, False)
def can_proceed_to_step(self, step: str) -> bool:
"""Check if user can proceed to a specific step (no skipping)."""
target_index = None
for i, s in enumerate(STEP_ORDER):
if s.value == step:
target_index = i
break
if target_index is None:
return False
# Check all previous steps are completed
for i in range(target_index):
if not self.is_step_completed(STEP_ORDER[i].value):
return False
return True
def get_next_step(self) -> str | None:
"""Get the next incomplete step."""
for step in STEP_ORDER:
if not self.is_step_completed(step.value):
return step.value
return None
def mark_step_complete(self, step: str, timestamp: datetime | None = None) -> None:
"""Mark a step as complete and update current step."""
if timestamp is None:
timestamp = datetime.utcnow()
if step == OnboardingStep.COMPANY_PROFILE.value:
self.step_company_profile_completed = True
self.step_company_profile_completed_at = timestamp
elif step == OnboardingStep.LETZSHOP_API.value:
self.step_letzshop_api_completed = True
self.step_letzshop_api_completed_at = timestamp
elif step == OnboardingStep.PRODUCT_IMPORT.value:
self.step_product_import_completed = True
self.step_product_import_completed_at = timestamp
elif step == OnboardingStep.ORDER_SYNC.value:
self.step_order_sync_completed = True
self.step_order_sync_completed_at = timestamp
# Update current step to next incomplete step
next_step = self.get_next_step()
if next_step:
self.current_step = next_step
else:
# All steps complete
self.status = OnboardingStatus.COMPLETED.value
self.completed_at = timestamp