# 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"" @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