refactor: migrate remaining routes to modules and enforce auto-discovery

MIGRATION:
- Delete app/api/v1/vendor/analytics.py (duplicate - analytics module already auto-discovered)
- Move usage routes from app/api/v1/vendor/usage.py to billing module
- Move onboarding routes from app/api/v1/vendor/onboarding.py to marketplace module
- Move features routes to billing module (admin + vendor)
- Move inventory routes to inventory module (admin + vendor)
- Move marketplace/letzshop routes to marketplace module
- Move orders routes to orders module
- Delete legacy letzshop service files (moved to marketplace module)

DOCUMENTATION:
- Add docs/development/migration/module-autodiscovery-migration.md with full migration history
- Update docs/architecture/module-system.md with Entity Auto-Discovery Reference section
- Add detailed sections for each entity type: routes, services, models, schemas, tasks,
  exceptions, templates, static files, locales, configuration

ARCHITECTURE VALIDATION:
- Add MOD-016: Routes must be in modules, not app/api/v1/
- Add MOD-017: Services must be in modules, not app/services/
- Add MOD-018: Tasks must be in modules, not app/tasks/
- Add MOD-019: Schemas must be in modules, not models/schema/
- Update scripts/validate_architecture.py with _validate_legacy_locations method
- Update .architecture-rules/module.yaml with legacy location rules

These rules enforce that all entities must be in self-contained modules.
Legacy locations now trigger ERROR severity violations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 14:25:59 +01:00
parent e2cecff014
commit 401db56258
52 changed files with 1160 additions and 4968 deletions

View File

@@ -1,53 +0,0 @@
# app/services/letzshop/__init__.py
"""
LEGACY LOCATION - Re-exports from module for backwards compatibility.
The canonical implementation is now in:
app/modules/marketplace/services/letzshop/
This file exists to maintain backwards compatibility with code that
imports from the old location. All new code should import directly
from the module:
from app.modules.marketplace.services.letzshop import LetzshopClient
"""
from app.modules.marketplace.services.letzshop import (
# Client
LetzshopClient,
LetzshopClientError,
LetzshopAuthError,
LetzshopAPIError,
LetzshopConnectionError,
# Credentials
LetzshopCredentialsService,
CredentialsError,
CredentialsNotFoundError,
# Order Service
LetzshopOrderService,
OrderNotFoundError,
VendorNotFoundError,
# Vendor Sync Service
LetzshopVendorSyncService,
get_vendor_sync_service,
)
__all__ = [
# Client
"LetzshopClient",
"LetzshopClientError",
"LetzshopAuthError",
"LetzshopAPIError",
"LetzshopConnectionError",
# Credentials
"LetzshopCredentialsService",
"CredentialsError",
"CredentialsNotFoundError",
# Order Service
"LetzshopOrderService",
"OrderNotFoundError",
"VendorNotFoundError",
# Vendor Sync Service
"LetzshopVendorSyncService",
"get_vendor_sync_service",
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,400 +0,0 @@
# app/services/letzshop/credentials_service.py
"""
Letzshop credentials management service.
Handles secure storage and retrieval of per-vendor Letzshop API credentials.
"""
import logging
from datetime import UTC, datetime
from sqlalchemy.orm import Session
from app.utils.encryption import decrypt_value, encrypt_value, mask_api_key
from app.modules.marketplace.models import VendorLetzshopCredentials
from .client_service import LetzshopClient
logger = logging.getLogger(__name__)
# Default Letzshop GraphQL endpoint
DEFAULT_ENDPOINT = "https://letzshop.lu/graphql"
class CredentialsError(Exception):
"""Base exception for credentials errors."""
class CredentialsNotFoundError(CredentialsError):
"""Raised when credentials are not found for a vendor."""
class LetzshopCredentialsService:
"""
Service for managing Letzshop API credentials.
Provides secure storage and retrieval of encrypted API keys,
connection testing, and sync status updates.
"""
def __init__(self, db: Session):
"""
Initialize the credentials service.
Args:
db: SQLAlchemy database session.
"""
self.db = db
# ========================================================================
# CRUD Operations
# ========================================================================
def get_credentials(self, vendor_id: int) -> VendorLetzshopCredentials | None:
"""
Get Letzshop credentials for a vendor.
Args:
vendor_id: The vendor ID.
Returns:
VendorLetzshopCredentials or None if not found.
"""
return (
self.db.query(VendorLetzshopCredentials)
.filter(VendorLetzshopCredentials.vendor_id == vendor_id)
.first()
)
def get_credentials_or_raise(self, vendor_id: int) -> VendorLetzshopCredentials:
"""
Get Letzshop credentials for a vendor or raise an exception.
Args:
vendor_id: The vendor ID.
Returns:
VendorLetzshopCredentials.
Raises:
CredentialsNotFoundError: If credentials are not found.
"""
credentials = self.get_credentials(vendor_id)
if credentials is None:
raise CredentialsNotFoundError(
f"Letzshop credentials not found for vendor {vendor_id}"
)
return credentials
def create_credentials(
self,
vendor_id: int,
api_key: str,
api_endpoint: str | None = None,
auto_sync_enabled: bool = False,
sync_interval_minutes: int = 15,
) -> VendorLetzshopCredentials:
"""
Create Letzshop credentials for a vendor.
Args:
vendor_id: The vendor ID.
api_key: The Letzshop API key (will be encrypted).
api_endpoint: Custom API endpoint (optional).
auto_sync_enabled: Whether to enable automatic sync.
sync_interval_minutes: Sync interval in minutes.
Returns:
Created VendorLetzshopCredentials.
"""
# Encrypt the API key
encrypted_key = encrypt_value(api_key)
credentials = VendorLetzshopCredentials(
vendor_id=vendor_id,
api_key_encrypted=encrypted_key,
api_endpoint=api_endpoint or DEFAULT_ENDPOINT,
auto_sync_enabled=auto_sync_enabled,
sync_interval_minutes=sync_interval_minutes,
)
self.db.add(credentials)
self.db.flush()
logger.info(f"Created Letzshop credentials for vendor {vendor_id}")
return credentials
def update_credentials(
self,
vendor_id: int,
api_key: str | None = None,
api_endpoint: str | None = None,
auto_sync_enabled: bool | None = None,
sync_interval_minutes: int | None = None,
) -> VendorLetzshopCredentials:
"""
Update Letzshop credentials for a vendor.
Args:
vendor_id: The vendor ID.
api_key: New API key (optional, will be encrypted if provided).
api_endpoint: New API endpoint (optional).
auto_sync_enabled: New auto-sync setting (optional).
sync_interval_minutes: New sync interval (optional).
Returns:
Updated VendorLetzshopCredentials.
Raises:
CredentialsNotFoundError: If credentials are not found.
"""
credentials = self.get_credentials_or_raise(vendor_id)
if api_key is not None:
credentials.api_key_encrypted = encrypt_value(api_key)
if api_endpoint is not None:
credentials.api_endpoint = api_endpoint
if auto_sync_enabled is not None:
credentials.auto_sync_enabled = auto_sync_enabled
if sync_interval_minutes is not None:
credentials.sync_interval_minutes = sync_interval_minutes
self.db.flush()
logger.info(f"Updated Letzshop credentials for vendor {vendor_id}")
return credentials
def delete_credentials(self, vendor_id: int) -> bool:
"""
Delete Letzshop credentials for a vendor.
Args:
vendor_id: The vendor ID.
Returns:
True if deleted, False if not found.
"""
credentials = self.get_credentials(vendor_id)
if credentials is None:
return False
self.db.delete(credentials)
self.db.flush()
logger.info(f"Deleted Letzshop credentials for vendor {vendor_id}")
return True
def upsert_credentials(
self,
vendor_id: int,
api_key: str,
api_endpoint: str | None = None,
auto_sync_enabled: bool = False,
sync_interval_minutes: int = 15,
) -> VendorLetzshopCredentials:
"""
Create or update Letzshop credentials for a vendor.
Args:
vendor_id: The vendor ID.
api_key: The Letzshop API key (will be encrypted).
api_endpoint: Custom API endpoint (optional).
auto_sync_enabled: Whether to enable automatic sync.
sync_interval_minutes: Sync interval in minutes.
Returns:
Created or updated VendorLetzshopCredentials.
"""
existing = self.get_credentials(vendor_id)
if existing:
return self.update_credentials(
vendor_id=vendor_id,
api_key=api_key,
api_endpoint=api_endpoint,
auto_sync_enabled=auto_sync_enabled,
sync_interval_minutes=sync_interval_minutes,
)
return self.create_credentials(
vendor_id=vendor_id,
api_key=api_key,
api_endpoint=api_endpoint,
auto_sync_enabled=auto_sync_enabled,
sync_interval_minutes=sync_interval_minutes,
)
# ========================================================================
# Key Decryption and Client Creation
# ========================================================================
def get_decrypted_api_key(self, vendor_id: int) -> str:
"""
Get the decrypted API key for a vendor.
Args:
vendor_id: The vendor ID.
Returns:
Decrypted API key.
Raises:
CredentialsNotFoundError: If credentials are not found.
"""
credentials = self.get_credentials_or_raise(vendor_id)
return decrypt_value(credentials.api_key_encrypted)
def get_masked_api_key(self, vendor_id: int) -> str:
"""
Get a masked version of the API key for display.
Args:
vendor_id: The vendor ID.
Returns:
Masked API key (e.g., "sk-a***************").
Raises:
CredentialsNotFoundError: If credentials are not found.
"""
api_key = self.get_decrypted_api_key(vendor_id)
return mask_api_key(api_key)
def create_client(self, vendor_id: int) -> LetzshopClient:
"""
Create a Letzshop client for a vendor.
Args:
vendor_id: The vendor ID.
Returns:
Configured LetzshopClient.
Raises:
CredentialsNotFoundError: If credentials are not found.
"""
credentials = self.get_credentials_or_raise(vendor_id)
api_key = decrypt_value(credentials.api_key_encrypted)
return LetzshopClient(
api_key=api_key,
endpoint=credentials.api_endpoint,
)
# ========================================================================
# Connection Testing
# ========================================================================
def test_connection(self, vendor_id: int) -> tuple[bool, float | None, str | None]:
"""
Test the connection for a vendor's credentials.
Args:
vendor_id: The vendor ID.
Returns:
Tuple of (success, response_time_ms, error_message).
"""
try:
with self.create_client(vendor_id) as client:
return client.test_connection()
except CredentialsNotFoundError:
return False, None, "Letzshop credentials not configured"
except Exception as e:
logger.error(f"Connection test failed for vendor {vendor_id}: {e}")
return False, None, str(e)
def test_api_key(
self,
api_key: str,
api_endpoint: str | None = None,
) -> tuple[bool, float | None, str | None]:
"""
Test an API key without saving it.
Args:
api_key: The API key to test.
api_endpoint: Optional custom endpoint.
Returns:
Tuple of (success, response_time_ms, error_message).
"""
try:
with LetzshopClient(
api_key=api_key,
endpoint=api_endpoint or DEFAULT_ENDPOINT,
) as client:
return client.test_connection()
except Exception as e:
logger.error(f"API key test failed: {e}")
return False, None, str(e)
# ========================================================================
# Sync Status Updates
# ========================================================================
def update_sync_status(
self,
vendor_id: int,
status: str,
error: str | None = None,
) -> VendorLetzshopCredentials | None:
"""
Update the last sync status for a vendor.
Args:
vendor_id: The vendor ID.
status: Sync status (success, failed, partial).
error: Error message if sync failed.
Returns:
Updated credentials or None if not found.
"""
credentials = self.get_credentials(vendor_id)
if credentials is None:
return None
credentials.last_sync_at = datetime.now(UTC)
credentials.last_sync_status = status
credentials.last_sync_error = error
self.db.flush()
return credentials
# ========================================================================
# Status Helpers
# ========================================================================
def is_configured(self, vendor_id: int) -> bool:
"""Check if Letzshop is configured for a vendor."""
return self.get_credentials(vendor_id) is not None
def get_status(self, vendor_id: int) -> dict:
"""
Get the Letzshop integration status for a vendor.
Args:
vendor_id: The vendor ID.
Returns:
Status dictionary with configuration and sync info.
"""
credentials = self.get_credentials(vendor_id)
if credentials is None:
return {
"is_configured": False,
"is_connected": False,
"last_sync_at": None,
"last_sync_status": None,
"auto_sync_enabled": False,
}
return {
"is_configured": True,
"is_connected": credentials.last_sync_status == "success",
"last_sync_at": credentials.last_sync_at,
"last_sync_status": credentials.last_sync_status,
"auto_sync_enabled": credentials.auto_sync_enabled,
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,521 +0,0 @@
# app/services/letzshop/vendor_sync_service.py
"""
Service for syncing Letzshop vendor directory to local cache.
Fetches vendor data from Letzshop's public GraphQL API and stores it
in the letzshop_vendor_cache table for fast lookups during signup.
"""
import logging
from datetime import UTC, datetime
from typing import Any, Callable
from sqlalchemy import func
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.orm import Session
from app.services.letzshop.client_service import LetzshopClient
from app.modules.marketplace.models import LetzshopVendorCache
logger = logging.getLogger(__name__)
class LetzshopVendorSyncService:
"""
Service for syncing Letzshop vendor directory.
Usage:
service = LetzshopVendorSyncService(db)
stats = service.sync_all_vendors()
"""
def __init__(self, db: Session):
"""Initialize the sync service."""
self.db = db
def sync_all_vendors(
self,
progress_callback: Callable[[int, int, int], None] | None = None,
max_pages: int | None = None,
) -> dict[str, Any]:
"""
Sync all vendors from Letzshop to local cache.
Args:
progress_callback: Optional callback(page, fetched, total) for progress.
Returns:
Dictionary with sync statistics.
"""
stats = {
"started_at": datetime.now(UTC),
"total_fetched": 0,
"created": 0,
"updated": 0,
"errors": 0,
"error_details": [],
}
logger.info("Starting Letzshop vendor directory sync...")
# Create client (no API key needed for public vendor data)
client = LetzshopClient(api_key="")
try:
# Fetch all vendors
vendors = client.get_all_vendors_paginated(
page_size=50,
max_pages=max_pages,
progress_callback=progress_callback,
)
stats["total_fetched"] = len(vendors)
logger.info(f"Fetched {len(vendors)} vendors from Letzshop")
# Process each vendor
for vendor_data in vendors:
try:
result = self._upsert_vendor(vendor_data)
if result == "created":
stats["created"] += 1
elif result == "updated":
stats["updated"] += 1
except Exception as e:
stats["errors"] += 1
error_info = {
"vendor_id": vendor_data.get("id"),
"slug": vendor_data.get("slug"),
"error": str(e),
}
stats["error_details"].append(error_info)
logger.error(f"Error processing vendor {vendor_data.get('slug')}: {e}")
# Commit all changes
self.db.commit()
logger.info(
f"Sync complete: {stats['created']} created, "
f"{stats['updated']} updated, {stats['errors']} errors"
)
except Exception as e:
self.db.rollback()
logger.error(f"Vendor sync failed: {e}")
stats["error"] = str(e)
raise
finally:
client.close()
stats["completed_at"] = datetime.now(UTC)
stats["duration_seconds"] = (
stats["completed_at"] - stats["started_at"]
).total_seconds()
return stats
def _upsert_vendor(self, vendor_data: dict[str, Any]) -> str:
"""
Insert or update a vendor in the cache.
Args:
vendor_data: Raw vendor data from Letzshop API.
Returns:
"created" or "updated" indicating the operation performed.
"""
letzshop_id = vendor_data.get("id")
slug = vendor_data.get("slug")
if not letzshop_id or not slug:
raise ValueError("Vendor missing required id or slug")
# Parse the vendor data
parsed = self._parse_vendor_data(vendor_data)
# Check if exists
existing = (
self.db.query(LetzshopVendorCache)
.filter(LetzshopVendorCache.letzshop_id == letzshop_id)
.first()
)
if existing:
# Update existing record (preserve claimed status)
for key, value in parsed.items():
if key not in ("claimed_by_vendor_id", "claimed_at"):
setattr(existing, key, value)
existing.last_synced_at = datetime.now(UTC)
return "updated"
else:
# Create new record
cache_entry = LetzshopVendorCache(
**parsed,
last_synced_at=datetime.now(UTC),
)
self.db.add(cache_entry)
return "created"
def _parse_vendor_data(self, data: dict[str, Any]) -> dict[str, Any]:
"""
Parse raw Letzshop vendor data into cache model fields.
Args:
data: Raw vendor data from Letzshop API.
Returns:
Dictionary of parsed fields for LetzshopVendorCache.
"""
# Extract location
location = data.get("location") or {}
country = location.get("country") or {}
# Extract descriptions
description = data.get("description") or {}
# Extract opening hours
opening_hours = data.get("openingHours") or {}
# Extract categories (list of translated name objects)
categories = []
for cat in data.get("vendorCategories") or []:
cat_name = cat.get("name") or {}
# Prefer English, fallback to French or German
name = cat_name.get("en") or cat_name.get("fr") or cat_name.get("de")
if name:
categories.append(name)
# Extract social media URLs
social_links = []
for link in data.get("socialMediaLinks") or []:
url = link.get("url")
if url:
social_links.append(url)
# Extract background image
bg_image = data.get("backgroundImage") or {}
return {
"letzshop_id": data.get("id"),
"slug": data.get("slug"),
"name": data.get("name"),
"company_name": data.get("companyName") or data.get("legalName"),
"is_active": data.get("active", True),
# Descriptions
"description_en": description.get("en"),
"description_fr": description.get("fr"),
"description_de": description.get("de"),
# Contact
"email": data.get("email"),
"phone": data.get("phone"),
"fax": data.get("fax"),
"website": data.get("homepage"),
# Location
"street": location.get("street"),
"street_number": location.get("number"),
"city": location.get("city"),
"zipcode": location.get("zipcode"),
"country_iso": country.get("iso", "LU"),
"latitude": str(data.get("lat")) if data.get("lat") else None,
"longitude": str(data.get("lng")) if data.get("lng") else None,
# Categories and media
"categories": categories,
"background_image_url": bg_image.get("url"),
"social_media_links": social_links,
# Opening hours
"opening_hours_en": opening_hours.get("en"),
"opening_hours_fr": opening_hours.get("fr"),
"opening_hours_de": opening_hours.get("de"),
# Representative
"representative_name": data.get("representative"),
"representative_title": data.get("representativeTitle"),
# Raw data for reference
"raw_data": data,
}
def sync_single_vendor(self, slug: str) -> LetzshopVendorCache | None:
"""
Sync a single vendor by slug.
Useful for on-demand refresh when a user looks up a vendor.
Args:
slug: The vendor's URL slug.
Returns:
The updated/created cache entry, or None if not found.
"""
client = LetzshopClient(api_key="")
try:
vendor_data = client.get_vendor_by_slug(slug)
if not vendor_data:
logger.warning(f"Vendor not found on Letzshop: {slug}")
return None
result = self._upsert_vendor(vendor_data)
self.db.commit()
logger.info(f"Single vendor sync: {slug} ({result})")
return (
self.db.query(LetzshopVendorCache)
.filter(LetzshopVendorCache.slug == slug)
.first()
)
finally:
client.close()
def get_cached_vendor(self, slug: str) -> LetzshopVendorCache | None:
"""
Get a vendor from cache by slug.
Args:
slug: The vendor's URL slug.
Returns:
Cache entry or None if not found.
"""
return (
self.db.query(LetzshopVendorCache)
.filter(LetzshopVendorCache.slug == slug.lower())
.first()
)
def search_cached_vendors(
self,
search: str | None = None,
city: str | None = None,
category: str | None = None,
only_unclaimed: bool = False,
page: int = 1,
limit: int = 20,
) -> tuple[list[LetzshopVendorCache], int]:
"""
Search cached vendors with filters.
Args:
search: Search term for name.
city: Filter by city.
category: Filter by category.
only_unclaimed: Only return vendors not yet claimed.
page: Page number (1-indexed).
limit: Items per page.
Returns:
Tuple of (vendors list, total count).
"""
query = self.db.query(LetzshopVendorCache).filter(
LetzshopVendorCache.is_active == True # noqa: E712
)
if search:
search_term = f"%{search.lower()}%"
query = query.filter(
func.lower(LetzshopVendorCache.name).like(search_term)
)
if city:
query = query.filter(
func.lower(LetzshopVendorCache.city) == city.lower()
)
if category:
# Search in JSON array
query = query.filter(
LetzshopVendorCache.categories.contains([category])
)
if only_unclaimed:
query = query.filter(
LetzshopVendorCache.claimed_by_vendor_id.is_(None)
)
# Get total count
total = query.count()
# Apply pagination
offset = (page - 1) * limit
vendors = (
query.order_by(LetzshopVendorCache.name)
.offset(offset)
.limit(limit)
.all()
)
return vendors, total
def get_sync_stats(self) -> dict[str, Any]:
"""
Get statistics about the vendor cache.
Returns:
Dictionary with cache statistics.
"""
total = self.db.query(LetzshopVendorCache).count()
active = (
self.db.query(LetzshopVendorCache)
.filter(LetzshopVendorCache.is_active == True) # noqa: E712
.count()
)
claimed = (
self.db.query(LetzshopVendorCache)
.filter(LetzshopVendorCache.claimed_by_vendor_id.isnot(None))
.count()
)
# Get last sync time
last_synced = (
self.db.query(func.max(LetzshopVendorCache.last_synced_at)).scalar()
)
# Get unique cities
cities = (
self.db.query(LetzshopVendorCache.city)
.filter(LetzshopVendorCache.city.isnot(None))
.distinct()
.count()
)
return {
"total_vendors": total,
"active_vendors": active,
"claimed_vendors": claimed,
"unclaimed_vendors": active - claimed,
"unique_cities": cities,
"last_synced_at": last_synced.isoformat() if last_synced else None,
}
def mark_vendor_claimed(
self,
letzshop_slug: str,
vendor_id: int,
) -> bool:
"""
Mark a Letzshop vendor as claimed by a platform vendor.
Args:
letzshop_slug: The Letzshop vendor slug.
vendor_id: The platform vendor ID that claimed it.
Returns:
True if successful, False if vendor not found.
"""
cache_entry = self.get_cached_vendor(letzshop_slug)
if not cache_entry:
return False
cache_entry.claimed_by_vendor_id = vendor_id
cache_entry.claimed_at = datetime.now(UTC)
self.db.commit()
logger.info(f"Vendor {letzshop_slug} claimed by vendor_id={vendor_id}")
return True
def create_vendor_from_cache(
self,
letzshop_slug: str,
company_id: int,
) -> dict[str, Any]:
"""
Create a platform vendor from a cached Letzshop vendor.
Args:
letzshop_slug: The Letzshop vendor slug.
company_id: The company ID to create the vendor under.
Returns:
Dictionary with created vendor info.
Raises:
ValueError: If vendor not found, already claimed, or company not found.
"""
import random
from sqlalchemy import func
from app.services.admin_service import admin_service
from models.database.company import Company
from models.database.vendor import Vendor
from models.schema.vendor import VendorCreate
# Get cache entry
cache_entry = self.get_cached_vendor(letzshop_slug)
if not cache_entry:
raise ValueError(f"Letzshop vendor '{letzshop_slug}' not found in cache")
if cache_entry.is_claimed:
raise ValueError(
f"Letzshop vendor '{cache_entry.name}' is already claimed "
f"by vendor ID {cache_entry.claimed_by_vendor_id}"
)
# Verify company exists
company = self.db.query(Company).filter(Company.id == company_id).first()
if not company:
raise ValueError(f"Company with ID {company_id} not found")
# Generate vendor code from slug
vendor_code = letzshop_slug.upper().replace("-", "_")[:20]
# Check if vendor code already exists
existing = (
self.db.query(Vendor)
.filter(func.upper(Vendor.vendor_code) == vendor_code)
.first()
)
if existing:
vendor_code = f"{vendor_code[:16]}_{random.randint(100, 999)}"
# Generate subdomain from slug
subdomain = letzshop_slug.lower().replace("_", "-")[:30]
existing_subdomain = (
self.db.query(Vendor)
.filter(func.lower(Vendor.subdomain) == subdomain)
.first()
)
if existing_subdomain:
subdomain = f"{subdomain[:26]}-{random.randint(100, 999)}"
# Create vendor data from cache
address = f"{cache_entry.street or ''} {cache_entry.street_number or ''}".strip()
vendor_data = VendorCreate(
name=cache_entry.name,
vendor_code=vendor_code,
subdomain=subdomain,
company_id=company_id,
email=cache_entry.email or company.email,
phone=cache_entry.phone,
description=cache_entry.description_en or cache_entry.description_fr or "",
city=cache_entry.city,
country=cache_entry.country_iso or "LU",
website=cache_entry.website,
address_line_1=address or None,
postal_code=cache_entry.zipcode,
)
# Create vendor
vendor = admin_service.create_vendor(self.db, vendor_data)
# Mark the Letzshop vendor as claimed (commits internally) # noqa: SVC-006
self.mark_vendor_claimed(letzshop_slug, vendor.id)
logger.info(
f"Created vendor {vendor.vendor_code} from Letzshop vendor {letzshop_slug}"
)
return {
"id": vendor.id,
"vendor_code": vendor.vendor_code,
"name": vendor.name,
"subdomain": vendor.subdomain,
"company_id": vendor.company_id,
}
# Singleton-style function for easy access
def get_vendor_sync_service(db: Session) -> LetzshopVendorSyncService:
"""Get a vendor sync service instance."""
return LetzshopVendorSyncService(db)

View File

@@ -1,25 +0,0 @@
# app/services/letzshop_export_service.py
"""
LEGACY LOCATION - Re-exports from module for backwards compatibility.
The canonical implementation is now in:
app/modules/marketplace/services/letzshop_export_service.py
This file exists to maintain backwards compatibility with code that
imports from the old location. All new code should import directly
from the module:
from app.modules.marketplace.services import letzshop_export_service
"""
from app.modules.marketplace.services.letzshop_export_service import (
LetzshopExportService,
letzshop_export_service,
LETZSHOP_CSV_COLUMNS,
)
__all__ = [
"LetzshopExportService",
"letzshop_export_service",
"LETZSHOP_CSV_COLUMNS",
]

View File

@@ -1,23 +0,0 @@
# app/services/marketplace_import_job_service.py
"""
LEGACY LOCATION - Re-exports from module for backwards compatibility.
The canonical implementation is now in:
app/modules/marketplace/services/marketplace_import_job_service.py
This file exists to maintain backwards compatibility with code that
imports from the old location. All new code should import directly
from the module:
from app.modules.marketplace.services import marketplace_import_job_service
"""
from app.modules.marketplace.services.marketplace_import_job_service import (
MarketplaceImportJobService,
marketplace_import_job_service,
)
__all__ = [
"MarketplaceImportJobService",
"marketplace_import_job_service",
]

View File

@@ -1,23 +0,0 @@
# app/services/marketplace_product_service.py
"""
LEGACY LOCATION - Re-exports from module for backwards compatibility.
The canonical implementation is now in:
app/modules/marketplace/services/marketplace_product_service.py
This file exists to maintain backwards compatibility with code that
imports from the old location. All new code should import directly
from the module:
from app.modules.marketplace.services import marketplace_product_service
"""
from app.modules.marketplace.services.marketplace_product_service import (
MarketplaceProductService,
marketplace_product_service,
)
__all__ = [
"MarketplaceProductService",
"marketplace_product_service",
]

View File

@@ -1 +0,0 @@
# Marketplace import services (MarketplaceProduct

View File

@@ -22,8 +22,10 @@ from app.exceptions import (
OnboardingSyncNotCompleteException,
VendorNotFoundException,
)
from app.services.letzshop.credentials_service import LetzshopCredentialsService
from app.services.letzshop.order_service import LetzshopOrderService
from app.modules.marketplace.services.letzshop import (
LetzshopCredentialsService,
LetzshopOrderService,
)
from app.modules.marketplace.models import (
OnboardingStatus,
OnboardingStep,