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>
This commit is contained in:
2026-01-30 14:45:32 +01:00
parent a123341aa8
commit 0c63f387aa
72 changed files with 176 additions and 276 deletions

View File

@@ -35,6 +35,12 @@ from app.modules.marketplace.models.letzshop import (
# Import jobs
LetzshopHistoricalImportJob,
)
from app.modules.marketplace.models.onboarding import (
OnboardingStatus,
OnboardingStep,
STEP_ORDER,
VendorOnboarding,
)
__all__ = [
# Marketplace products
@@ -51,4 +57,9 @@ __all__ = [
"LetzshopFulfillmentQueue",
"LetzshopVendorCache",
"LetzshopSyncLog",
# Onboarding
"OnboardingStatus",
"OnboardingStep",
"STEP_ORDER",
"VendorOnboarding",
]

View File

@@ -0,0 +1,227 @@
# 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

View File

@@ -11,7 +11,7 @@ from datetime import UTC, datetime
from sqlalchemy.orm import Session
from app.utils.encryption import decrypt_value, encrypt_value, mask_api_key
from models.database.letzshop import VendorLetzshopCredentials
from app.modules.marketplace.models import VendorLetzshopCredentials
from .client_service import LetzshopClient

View File

@@ -16,15 +16,15 @@ from sqlalchemy.orm import Session
from app.services.order_service import order_service as unified_order_service
from app.services.subscription_service import subscription_service
from models.database.letzshop import (
from app.modules.marketplace.models import (
LetzshopFulfillmentQueue,
LetzshopHistoricalImportJob,
LetzshopSyncLog,
MarketplaceImportJob,
VendorLetzshopCredentials,
)
from models.database.marketplace_import_job import MarketplaceImportJob
from app.modules.orders.models import Order, OrderItem
from models.database.product import Product
from app.modules.catalog.models import Product
from models.database.vendor import Vendor
logger = logging.getLogger(__name__)

View File

@@ -15,7 +15,7 @@ from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.orm import Session
from app.services.letzshop.client_service import LetzshopClient
from models.database.letzshop import LetzshopVendorCache
from app.modules.marketplace.models import LetzshopVendorCache
logger = logging.getLogger(__name__)

View File

@@ -12,9 +12,8 @@ from datetime import UTC, datetime
from sqlalchemy.orm import Session, joinedload
from models.database.letzshop import LetzshopSyncLog
from models.database.marketplace_product import MarketplaceProduct
from models.database.product import Product
from app.modules.marketplace.models import LetzshopSyncLog, MarketplaceProduct
from app.modules.catalog.models import Product
logger = logging.getLogger(__name__)

View File

@@ -8,7 +8,7 @@ from app.exceptions import (
ImportJobNotOwnedException,
ValidationException,
)
from models.database.marketplace_import_job import (
from app.modules.marketplace.models import (
MarketplaceImportError,
MarketplaceImportJob,
)

View File

@@ -31,8 +31,8 @@ from app.exceptions import (
)
from app.utils.data_processing import GTINProcessor, PriceProcessor
from app.modules.inventory.models import Inventory
from models.database.marketplace_product import MarketplaceProduct
from models.database.marketplace_product_translation import (
from app.modules.marketplace.models import (
MarketplaceProduct,
MarketplaceProductTranslation,
)
from app.modules.inventory.schemas import InventoryLocationResponse, InventorySummaryResponse
@@ -859,8 +859,8 @@ class MarketplaceProductService:
Returns:
Dict with copied, skipped, failed counts and details
"""
from models.database.product import Product
from models.database.product_translation import ProductTranslation
from app.modules.catalog.models import Product
from app.modules.catalog.models import ProductTranslation
from models.database.vendor import Vendor
vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first()

View File

@@ -13,8 +13,7 @@ from datetime import UTC, datetime
from typing import Callable
from app.core.celery_config import celery_app
from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.letzshop import LetzshopHistoricalImportJob
from app.modules.marketplace.models import MarketplaceImportJob, LetzshopHistoricalImportJob
from app.services.admin_notification_service import admin_notification_service
from app.services.letzshop import LetzshopClientError
from app.services.letzshop.credentials_service import LetzshopCredentialsService