Files
orion/app/modules/marketplace/models/onboarding.py
Samir Boulahtit 0c63f387aa refactor: migrate models to canonical module locations
- Move Product/ProductTranslation to app/modules/catalog/models/
- Move VendorOnboarding to app/modules/marketplace/models/
- Delete legacy re-export files for marketplace models:
  - letzshop.py, marketplace.py, marketplace_product.py
  - marketplace_product_translation.py, marketplace_import_job.py
- Delete legacy product.py, product_translation.py, onboarding.py
- Update all imports across services, tasks, tests to use module locations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 14:45:32 +01:00

228 lines
7.6 KiB
Python

# app/modules/marketplace/models/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.
The onboarding flow guides vendors through Letzshop marketplace integration:
1. Company Profile setup
2. Letzshop API configuration
3. Product import from CSV feed
4. Historical order sync
"""
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 models.database.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