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

@@ -21,10 +21,9 @@ from sqlalchemy.orm import Session
from app.exceptions import AdminOperationException, VendorNotFoundException
from app.modules.customers.models.customer import Customer
from app.modules.inventory.models import Inventory
from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.marketplace_product import MarketplaceProduct
from app.modules.marketplace.models import MarketplaceImportJob, MarketplaceProduct
from app.modules.orders.models import Order
from models.database.product import Product
from app.modules.catalog.models import Product
from models.database.user import User
from models.database.vendor import Vendor

View File

@@ -16,7 +16,7 @@ from dataclasses import dataclass
from sqlalchemy import func
from sqlalchemy.orm import Session
from models.database.product import Product
from app.modules.catalog.models import Product
from models.database.subscription import SubscriptionTier, VendorSubscription
from models.database.vendor import VendorUser

View File

@@ -27,7 +27,7 @@ from app.modules.billing.models import (
SubscriptionTier,
VendorSubscription,
)
from models.database.product import Product
from app.modules.catalog.models import Product
from models.database.vendor import Vendor, VendorUser
logger = logging.getLogger(__name__)

View File

@@ -45,7 +45,7 @@ from app.modules.billing.schemas import (
TierLimits,
UsageSummary,
)
from models.database.product import Product
from app.modules.catalog.models import Product
from models.database.vendor import Vendor, VendorUser
logger = logging.getLogger(__name__)

View File

@@ -24,7 +24,7 @@ from app.exceptions import (
)
from app.utils.money import cents_to_euros
from app.modules.cart.models.cart import CartItem
from models.database.product import Product
from app.modules.catalog.models import Product
logger = logging.getLogger(__name__)

View File

@@ -2,6 +2,16 @@
"""
Catalog module models.
Note: The catalog module uses the Product model from the products module.
This file exists for consistency with the module structure.
This is the canonical location for product models.
Usage:
from app.modules.catalog.models import Product, ProductTranslation
"""
from app.modules.catalog.models.product import Product
from app.modules.catalog.models.product_translation import ProductTranslation
__all__ = [
"Product",
"ProductTranslation",
]

View File

@@ -0,0 +1,347 @@
# app/modules/catalog/models/product.py
"""Vendor Product model - independent copy pattern.
This model represents a vendor's product. Products can be:
1. Created from a marketplace import (has marketplace_product_id)
2. Created directly by the vendor (no marketplace_product_id)
When created from marketplace, the marketplace_product_id FK provides
"view original source" comparison feature.
Money values are stored as integer cents (e.g., 105.91 = 10591).
See docs/architecture/money-handling.md for details.
"""
from sqlalchemy import (
Boolean,
Column,
ForeignKey,
Index,
Integer,
String,
UniqueConstraint,
)
from sqlalchemy.dialects.sqlite import JSON
from sqlalchemy.orm import relationship
from app.core.database import Base
from app.utils.money import cents_to_euros, euros_to_cents
from models.database.base import TimestampMixin
class Product(Base, TimestampMixin):
"""Vendor-specific product.
Products can be created from marketplace imports or directly by vendors.
When from marketplace, marketplace_product_id provides source comparison.
Price fields use integer cents for precision (19.99 = 1999 cents).
"""
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
marketplace_product_id = Column(
Integer, ForeignKey("marketplace_products.id"), nullable=True
)
# === VENDOR REFERENCE ===
vendor_sku = Column(String, index=True) # Vendor's internal SKU
# === PRODUCT IDENTIFIERS ===
# GTIN (Global Trade Item Number) - barcode for EAN matching with orders
# Populated from MarketplaceProduct.gtin during product import
gtin = Column(String(50), index=True) # EAN/UPC barcode number
gtin_type = Column(String(20)) # Format: gtin13, gtin14, gtin12, gtin8, isbn13, isbn10
# === PRODUCT FIELDS (copied from marketplace at creation) ===
# Pricing - stored as integer cents (19.99 = 1999)
price_cents = Column(Integer) # Price in cents
sale_price_cents = Column(Integer) # Sale price in cents
currency = Column(String(3), default="EUR")
# Product Info
brand = Column(String)
condition = Column(String)
availability = Column(String)
# Media
primary_image_url = Column(String)
additional_images = Column(JSON)
# Digital Product Fields
download_url = Column(String)
license_type = Column(String(50))
# === TAX / VAT ===
# Luxembourg VAT rates: 0 (exempt), 3 (super-reduced), 8 (reduced), 14 (intermediate), 17 (standard)
# Prices are stored as gross (VAT-inclusive). Tax rate is used for profit calculation.
tax_rate_percent = Column(Integer, default=17, nullable=False)
# === SUPPLIER TRACKING & COST ===
supplier = Column(String(50)) # 'codeswholesale', 'internal', etc.
supplier_product_id = Column(String) # Supplier's product reference
cost_cents = Column(Integer) # What vendor pays to acquire (in cents) - for profit calculation
margin_percent_x100 = Column(Integer) # Markup percentage * 100 (e.g., 25.5% = 2550)
# === PRODUCT TYPE ===
is_digital = Column(Boolean, default=False, index=True)
product_type = Column(String(20), default="physical") # physical, digital, service, subscription
# === VENDOR-SPECIFIC ===
is_featured = Column(Boolean, default=False)
is_active = Column(Boolean, default=True)
display_order = Column(Integer, default=0)
# Inventory Settings
min_quantity = Column(Integer, default=1)
max_quantity = Column(Integer)
# Digital Fulfillment
fulfillment_email_template = Column(String) # Template name for digital delivery
# === RELATIONSHIPS ===
vendor = relationship("Vendor", back_populates="products")
marketplace_product = relationship(
"MarketplaceProduct", back_populates="vendor_products"
)
translations = relationship(
"ProductTranslation",
back_populates="product",
cascade="all, delete-orphan",
)
inventory_entries = relationship(
"Inventory", back_populates="product", cascade="all, delete-orphan"
)
# === CONSTRAINTS & INDEXES ===
__table_args__ = (
UniqueConstraint(
"vendor_id", "marketplace_product_id", name="uq_vendor_marketplace_product"
),
Index("idx_product_vendor_active", "vendor_id", "is_active"),
Index("idx_product_vendor_featured", "vendor_id", "is_featured"),
Index("idx_product_vendor_sku", "vendor_id", "vendor_sku"),
Index("idx_product_supplier", "supplier", "supplier_product_id"),
)
def __repr__(self):
return (
f"<Product(id={self.id}, vendor_id={self.vendor_id}, "
f"vendor_sku='{self.vendor_sku}')>"
)
# === PRICE PROPERTIES (Euro convenience accessors) ===
@property
def price(self) -> float | None:
"""Get price in euros (for API/display)."""
if self.price_cents is not None:
return cents_to_euros(self.price_cents)
return None
@price.setter
def price(self, value: float | None):
"""Set price from euros."""
self.price_cents = euros_to_cents(value) if value is not None else None
@property
def sale_price(self) -> float | None:
"""Get sale price in euros (for API/display)."""
if self.sale_price_cents is not None:
return cents_to_euros(self.sale_price_cents)
return None
@sale_price.setter
def sale_price(self, value: float | None):
"""Set sale price from euros."""
self.sale_price_cents = euros_to_cents(value) if value is not None else None
@property
def cost(self) -> float | None:
"""Get cost in euros (what vendor pays to acquire)."""
if self.cost_cents is not None:
return cents_to_euros(self.cost_cents)
return None
@cost.setter
def cost(self, value: float | None):
"""Set cost from euros."""
self.cost_cents = euros_to_cents(value) if value is not None else None
@property
def margin_percent(self) -> float | None:
"""Get margin percent (e.g., 25.5)."""
if self.margin_percent_x100 is not None:
return self.margin_percent_x100 / 100.0
return None
@margin_percent.setter
def margin_percent(self, value: float | None):
"""Set margin percent."""
self.margin_percent_x100 = int(value * 100) if value is not None else None
# === TAX / PROFIT CALCULATION PROPERTIES ===
@property
def net_price_cents(self) -> int | None:
"""Calculate net price (excluding VAT) from gross price.
Formula: Net = Gross / (1 + rate/100)
Example: 119 gross at 17% VAT = 119 / 1.17 = 101.71 net
"""
if self.price_cents is None:
return None
# Use integer math to avoid floating point issues
# Net = Gross * 100 / (100 + rate)
return int(self.price_cents * 100 / (100 + self.tax_rate_percent))
@property
def net_price(self) -> float | None:
"""Get net price in euros."""
cents = self.net_price_cents
return cents_to_euros(cents) if cents is not None else None
@property
def vat_amount_cents(self) -> int | None:
"""Calculate VAT amount in cents.
Formula: VAT = Gross - Net
"""
if self.price_cents is None:
return None
net = self.net_price_cents
if net is None:
return None
return self.price_cents - net
@property
def vat_amount(self) -> float | None:
"""Get VAT amount in euros."""
cents = self.vat_amount_cents
return cents_to_euros(cents) if cents is not None else None
@property
def profit_cents(self) -> int | None:
"""Calculate profit in cents.
Formula: Profit = Net Revenue - Cost
Returns None if cost is not set.
"""
net = self.net_price_cents
if net is None or self.cost_cents is None:
return None
return net - self.cost_cents
@property
def profit(self) -> float | None:
"""Get profit in euros."""
cents = self.profit_cents
return cents_to_euros(cents) if cents is not None else None
@property
def profit_margin_percent(self) -> float | None:
"""Calculate profit margin as percentage of net revenue.
Formula: Margin% = (Profit / Net) * 100
Example: 41.71 profit on 101.71 net = 41.0% margin
"""
net = self.net_price_cents
profit = self.profit_cents
if net is None or profit is None or net == 0:
return None
return round((profit / net) * 100, 2)
# === INVENTORY PROPERTIES ===
# Constant for unlimited inventory (digital products)
UNLIMITED_INVENTORY = 999999
@property
def has_unlimited_inventory(self) -> bool:
"""Check if product has unlimited inventory.
Digital products have unlimited inventory by default.
They don't require physical stock tracking.
"""
return self.is_digital
@property
def total_inventory(self) -> int:
"""Calculate total inventory across all locations.
Digital products return unlimited inventory.
"""
if self.has_unlimited_inventory:
return self.UNLIMITED_INVENTORY
return sum(inv.quantity for inv in self.inventory_entries)
@property
def available_inventory(self) -> int:
"""Calculate available inventory (total - reserved).
Digital products return unlimited inventory since they
don't have physical stock constraints.
"""
if self.has_unlimited_inventory:
return self.UNLIMITED_INVENTORY
return sum(inv.available_quantity for inv in self.inventory_entries)
# === SOURCE COMPARISON METHOD ===
def get_source_comparison_info(self) -> dict:
"""Get current values with source values for comparison.
Returns a dict with current field values and original source values
from the marketplace product. Used for "view original source" feature.
Only populated when product was created from a marketplace source.
"""
mp = self.marketplace_product
return {
# Price
"price": self.price,
"price_cents": self.price_cents,
"price_source": cents_to_euros(mp.price_cents) if mp and mp.price_cents else None,
# Sale Price
"sale_price": self.sale_price,
"sale_price_cents": self.sale_price_cents,
"sale_price_source": cents_to_euros(mp.sale_price_cents) if mp and mp.sale_price_cents else None,
# Currency
"currency": self.currency,
"currency_source": mp.currency if mp else None,
# Brand
"brand": self.brand,
"brand_source": mp.brand if mp else None,
# Condition
"condition": self.condition,
"condition_source": mp.condition if mp else None,
# Availability
"availability": self.availability,
"availability_source": mp.availability if mp else None,
# Images
"primary_image_url": self.primary_image_url,
"primary_image_url_source": mp.image_link if mp else None,
# Product type (independent fields, no source comparison)
"is_digital": self.is_digital,
"product_type": self.product_type,
}
# === TRANSLATION HELPERS ===
def get_translation(self, language: str) -> "ProductTranslation | None":
"""Get translation for a specific language."""
for t in self.translations:
if t.language == language:
return t
return None
def get_title(self, language: str = "en") -> str | None:
"""Get title for a specific language."""
translation = self.get_translation(language)
return translation.title if translation else None
def get_description(self, language: str = "en") -> str | None:
"""Get description for a specific language."""
translation = self.get_translation(language)
return translation.description if translation else None

View File

@@ -0,0 +1,115 @@
# app/modules/catalog/models/product_translation.py
"""Product Translation model for vendor-specific localized content.
This model stores vendor-specific translations. Translations are independent
entities with all fields populated at creation time from the source
marketplace product translation.
The marketplace product translation can be accessed via the product's
marketplace_product relationship for "view original source" comparison.
"""
from sqlalchemy import (
Column,
ForeignKey,
Index,
Integer,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import relationship
from app.core.database import Base
from models.database.base import TimestampMixin
class ProductTranslation(Base, TimestampMixin):
"""Vendor-specific localized content - independent copy.
Each vendor has their own translations with all fields populated
at creation time. The source marketplace translation can be accessed
for comparison via the product's marketplace_product relationship.
"""
__tablename__ = "product_translations"
id = Column(Integer, primary_key=True, index=True)
product_id = Column(
Integer,
ForeignKey("products.id", ondelete="CASCADE"),
nullable=False,
)
language = Column(String(5), nullable=False) # 'en', 'fr', 'de', 'lb'
# === LOCALIZED FIELDS (copied from marketplace at creation) ===
title = Column(String)
description = Column(Text)
short_description = Column(String(500))
# SEO Fields
meta_title = Column(String(70))
meta_description = Column(String(160))
url_slug = Column(String(255))
# === RELATIONSHIPS ===
product = relationship("Product", back_populates="translations")
__table_args__ = (
UniqueConstraint("product_id", "language", name="uq_product_translation"),
Index("idx_pt_product_id", "product_id"),
Index("idx_pt_product_language", "product_id", "language"),
)
def __repr__(self):
return (
f"<ProductTranslation(id={self.id}, "
f"product_id={self.product_id}, "
f"language='{self.language}', "
f"title='{self.title[:30] if self.title else None}...')>"
)
# === SOURCE COMPARISON METHOD ===
def _find_marketplace_translation(self):
"""Get the corresponding marketplace translation for comparison."""
product = self.product
if product and product.marketplace_product:
mp = product.marketplace_product
for t in mp.translations:
if t.language == self.language:
return t
return None
def get_source_comparison_info(self) -> dict:
"""Get current values with source values for comparison.
Returns a dict with current field values and original source values
from the marketplace product translation. Used for "view original source" feature.
"""
mp_translation = self._find_marketplace_translation()
return {
# Title
"title": self.title,
"title_source": mp_translation.title if mp_translation else None,
# Description
"description": self.description,
"description_source": mp_translation.description if mp_translation else None,
# Short Description
"short_description": self.short_description,
"short_description_source": (
mp_translation.short_description if mp_translation else None
),
# Meta Title
"meta_title": self.meta_title,
"meta_title_source": mp_translation.meta_title if mp_translation else None,
# Meta Description
"meta_description": self.meta_description,
"meta_description_source": (
mp_translation.meta_description if mp_translation else None
),
# URL Slug
"url_slug": self.url_slug,
"url_slug_source": mp_translation.url_slug if mp_translation else None,
}

View File

@@ -18,8 +18,7 @@ from sqlalchemy import or_
from sqlalchemy.orm import Session, joinedload
from app.exceptions import ProductNotFoundException, ValidationException
from models.database.product import Product
from models.database.product_translation import ProductTranslation
from app.modules.catalog.models import Product, ProductTranslation
logger = logging.getLogger(__name__)

View File

@@ -24,7 +24,7 @@ from dataclasses import dataclass, field
from sqlalchemy.orm import Session
from app.modules.inventory.models.inventory import Inventory
from models.database.product import Product
from app.modules.catalog.models import Product
logger = logging.getLogger(__name__)

View File

@@ -30,7 +30,7 @@ from app.modules.inventory.schemas.inventory import (
InventoryUpdate,
ProductInventorySummary,
)
from models.database.product import Product
from app.modules.catalog.models import Product
from models.database.vendor import Vendor
logger = logging.getLogger(__name__)
@@ -603,8 +603,8 @@ class InventoryService:
query = query.filter(Inventory.quantity <= low_stock)
if search:
from models.database.marketplace_product import MarketplaceProduct
from models.database.marketplace_product_translation import (
from app.modules.marketplace.models import (
MarketplaceProduct,
MarketplaceProductTranslation,
)

View File

@@ -15,7 +15,7 @@ from app.exceptions import OrderNotFoundException, ProductNotFoundException
from app.modules.inventory.models.inventory import Inventory
from app.modules.inventory.models.inventory_transaction import InventoryTransaction
from app.modules.orders.models import Order
from models.database.product import Product
from app.modules.catalog.models import Product
logger = logging.getLogger(__name__)

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

View File

@@ -10,7 +10,7 @@ from sqlalchemy import case, desc, func
from sqlalchemy.orm import Session
from models.database.architecture_scan import ArchitectureScan
from models.database.marketplace_import_job import MarketplaceImportJob
from app.modules.marketplace.models import MarketplaceImportJob
from models.database.test_run import TestRun

View File

@@ -23,7 +23,7 @@ from app.exceptions import (
)
from app.modules.orders.models.order import Order, OrderItem
from app.modules.orders.models.order_item_exception import OrderItemException
from models.database.product import Product
from app.modules.catalog.models import Product
logger = logging.getLogger(__name__)

View File

@@ -49,9 +49,8 @@ from app.utils.vat import (
calculate_vat_amount,
determine_vat_regime,
)
from models.database.marketplace_product import MarketplaceProduct
from models.database.marketplace_product_translation import MarketplaceProductTranslation
from models.database.product import Product
from app.modules.marketplace.models import MarketplaceProduct, MarketplaceProductTranslation
from app.modules.catalog.models import Product
from models.database.vendor import Vendor
# Placeholder product constants