feat: complete marketplace module self-containment
Migrate marketplace module to self-contained structure: - routes/api/admin.py - Admin API endpoints - routes/api/vendor.py - Vendor API endpoints - routes/pages/ - Page routes (placeholder) - models/letzshop.py - Letzshop model - models/marketplace_import_job.py - Import job model - models/marketplace_product.py - Product model - models/marketplace_product_translation.py - Translation model - schemas/marketplace_import_job.py - Import job schemas - schemas/marketplace_product.py - Product schemas - locales/ - Translations (en, de, fr, lu) Removed legacy route files replaced by api/ structure. Updated __init__.py files to use new structure. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,12 +27,21 @@ Usage:
|
|||||||
from app.modules.marketplace.exceptions import LetzshopClientError
|
from app.modules.marketplace.exceptions import LetzshopClientError
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from app.modules.marketplace.definition import (
|
# Lazy imports to avoid circular dependencies
|
||||||
marketplace_module,
|
# Routers and module definition are imported on-demand
|
||||||
get_marketplace_module_with_routers,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"marketplace_module",
|
"marketplace_module",
|
||||||
"get_marketplace_module_with_routers",
|
"get_marketplace_module_with_routers",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str):
|
||||||
|
"""Lazy import to avoid circular dependencies."""
|
||||||
|
if name == "marketplace_module":
|
||||||
|
from app.modules.marketplace.definition import marketplace_module
|
||||||
|
return marketplace_module
|
||||||
|
elif name == "get_marketplace_module_with_routers":
|
||||||
|
from app.modules.marketplace.definition import get_marketplace_module_with_routers
|
||||||
|
return get_marketplace_module_with_routers
|
||||||
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ from models.database.admin_menu_config import FrontendType
|
|||||||
|
|
||||||
def _get_admin_router():
|
def _get_admin_router():
|
||||||
"""Lazy import of admin router to avoid circular imports."""
|
"""Lazy import of admin router to avoid circular imports."""
|
||||||
from app.modules.marketplace.routes.admin import admin_router
|
from app.modules.marketplace.routes.api.admin import admin_router
|
||||||
|
|
||||||
return admin_router
|
return admin_router
|
||||||
|
|
||||||
|
|
||||||
def _get_vendor_router():
|
def _get_vendor_router():
|
||||||
"""Lazy import of vendor router to avoid circular imports."""
|
"""Lazy import of vendor router to avoid circular imports."""
|
||||||
from app.modules.marketplace.routes.vendor import vendor_router
|
from app.modules.marketplace.routes.api.vendor import vendor_router
|
||||||
|
|
||||||
return vendor_router
|
return vendor_router
|
||||||
|
|
||||||
|
|||||||
122
app/modules/marketplace/locales/de.json
Normal file
122
app/modules/marketplace/locales/de.json
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"title": "Marktplatz-Integration",
|
||||||
|
"description": "Letzshop Produkt- und Bestellsynchronisation",
|
||||||
|
"products": {
|
||||||
|
"title": "Marktplatz-Produkte",
|
||||||
|
"subtitle": "Von Marktplätzen importierte Produkte",
|
||||||
|
"empty": "Keine Produkte gefunden",
|
||||||
|
"empty_search": "Keine Produkte entsprechen Ihrer Suche",
|
||||||
|
"import": "Produkte importieren"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"title": "Produkte importieren",
|
||||||
|
"subtitle": "Produkte aus Marktplatz-Feeds importieren",
|
||||||
|
"source_url": "Feed-URL",
|
||||||
|
"source_url_help": "URL zum Marktplatz-CSV-Feed",
|
||||||
|
"marketplace": "Marktplatz",
|
||||||
|
"language": "Sprache",
|
||||||
|
"language_help": "Sprache für Produktübersetzungen",
|
||||||
|
"batch_size": "Batch-Größe",
|
||||||
|
"start_import": "Import starten",
|
||||||
|
"cancel": "Abbrechen"
|
||||||
|
},
|
||||||
|
"import_jobs": {
|
||||||
|
"title": "Import-Verlauf",
|
||||||
|
"subtitle": "Vergangene und aktuelle Import-Jobs",
|
||||||
|
"empty": "Keine Import-Jobs",
|
||||||
|
"job_id": "Job-ID",
|
||||||
|
"marketplace": "Marktplatz",
|
||||||
|
"vendor": "Verkäufer",
|
||||||
|
"status": "Status",
|
||||||
|
"imported": "Importiert",
|
||||||
|
"updated": "Aktualisiert",
|
||||||
|
"errors": "Fehler",
|
||||||
|
"created": "Erstellt",
|
||||||
|
"completed": "Abgeschlossen",
|
||||||
|
"statuses": {
|
||||||
|
"pending": "Ausstehend",
|
||||||
|
"processing": "In Bearbeitung",
|
||||||
|
"completed": "Abgeschlossen",
|
||||||
|
"completed_with_errors": "Mit Fehlern abgeschlossen",
|
||||||
|
"failed": "Fehlgeschlagen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"letzshop": {
|
||||||
|
"title": "Letzshop-Integration",
|
||||||
|
"subtitle": "Letzshop-Verbindung und Synchronisation verwalten",
|
||||||
|
"credentials": {
|
||||||
|
"title": "API-Anmeldedaten",
|
||||||
|
"api_key": "API-Schlüssel",
|
||||||
|
"api_key_help": "Ihr Letzshop API-Schlüssel",
|
||||||
|
"endpoint": "API-Endpunkt",
|
||||||
|
"test_mode": "Testmodus",
|
||||||
|
"test_mode_help": "Wenn aktiviert, werden keine Änderungen bei Letzshop vorgenommen"
|
||||||
|
},
|
||||||
|
"sync": {
|
||||||
|
"title": "Synchronisation",
|
||||||
|
"auto_sync": "Auto-Sync",
|
||||||
|
"auto_sync_help": "Bestellungen automatisch von Letzshop synchronisieren",
|
||||||
|
"interval": "Sync-Intervall",
|
||||||
|
"interval_help": "Minuten zwischen Synchronisationen",
|
||||||
|
"last_sync": "Letzte Sync",
|
||||||
|
"last_status": "Letzter Status",
|
||||||
|
"sync_now": "Jetzt synchronisieren"
|
||||||
|
},
|
||||||
|
"carrier": {
|
||||||
|
"title": "Versanddiensteinstellungen",
|
||||||
|
"default_carrier": "Standard-Versanddienstleister",
|
||||||
|
"greco": "Greco",
|
||||||
|
"colissimo": "Colissimo",
|
||||||
|
"xpresslogistics": "XpressLogistics",
|
||||||
|
"label_url": "Label-URL-Präfix"
|
||||||
|
},
|
||||||
|
"historical": {
|
||||||
|
"title": "Historischer Import",
|
||||||
|
"subtitle": "Vergangene Bestellungen von Letzshop importieren",
|
||||||
|
"start_import": "Historischen Import starten",
|
||||||
|
"phase": "Phase",
|
||||||
|
"confirmed": "Bestätigte Bestellungen",
|
||||||
|
"unconfirmed": "Unbestätigte Bestellungen",
|
||||||
|
"fetching": "Abrufen...",
|
||||||
|
"processing": "Verarbeiten...",
|
||||||
|
"page": "Seite",
|
||||||
|
"fetched": "Abgerufen",
|
||||||
|
"processed": "Verarbeitet",
|
||||||
|
"imported": "Importiert",
|
||||||
|
"updated": "Aktualisiert",
|
||||||
|
"skipped": "Übersprungen"
|
||||||
|
},
|
||||||
|
"vendors": {
|
||||||
|
"title": "Verkäuferverzeichnis",
|
||||||
|
"subtitle": "Letzshop-Verkäufer durchsuchen",
|
||||||
|
"claim": "Beanspruchen",
|
||||||
|
"claimed": "Beansprucht",
|
||||||
|
"unclaimed": "Nicht beansprucht",
|
||||||
|
"last_synced": "Zuletzt synchronisiert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"title": "Produkte exportieren",
|
||||||
|
"subtitle": "Produkte im Marktplatz-Format exportieren",
|
||||||
|
"format": "Format",
|
||||||
|
"format_csv": "CSV",
|
||||||
|
"format_xml": "XML",
|
||||||
|
"download": "Export herunterladen"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"import_started": "Import erfolgreich gestartet",
|
||||||
|
"import_completed": "Import abgeschlossen",
|
||||||
|
"import_failed": "Import fehlgeschlagen",
|
||||||
|
"credentials_saved": "Anmeldedaten erfolgreich gespeichert",
|
||||||
|
"sync_started": "Synchronisation gestartet",
|
||||||
|
"sync_completed": "Synchronisation abgeschlossen",
|
||||||
|
"sync_failed": "Synchronisation fehlgeschlagen",
|
||||||
|
"export_ready": "Export zum Download bereit",
|
||||||
|
"error_loading": "Fehler beim Laden der Daten"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"all_marketplaces": "Alle Marktplätze",
|
||||||
|
"all_vendors": "Alle Verkäufer",
|
||||||
|
"search_placeholder": "Produkte suchen..."
|
||||||
|
}
|
||||||
|
}
|
||||||
122
app/modules/marketplace/locales/en.json
Normal file
122
app/modules/marketplace/locales/en.json
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"title": "Marketplace Integration",
|
||||||
|
"description": "Letzshop product and order synchronization",
|
||||||
|
"products": {
|
||||||
|
"title": "Marketplace Products",
|
||||||
|
"subtitle": "Products imported from marketplaces",
|
||||||
|
"empty": "No products found",
|
||||||
|
"empty_search": "No products match your search",
|
||||||
|
"import": "Import Products"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"title": "Import Products",
|
||||||
|
"subtitle": "Import products from marketplace feeds",
|
||||||
|
"source_url": "Feed URL",
|
||||||
|
"source_url_help": "URL to the marketplace CSV feed",
|
||||||
|
"marketplace": "Marketplace",
|
||||||
|
"language": "Language",
|
||||||
|
"language_help": "Language for product translations",
|
||||||
|
"batch_size": "Batch Size",
|
||||||
|
"start_import": "Start Import",
|
||||||
|
"cancel": "Cancel"
|
||||||
|
},
|
||||||
|
"import_jobs": {
|
||||||
|
"title": "Import History",
|
||||||
|
"subtitle": "Past and current import jobs",
|
||||||
|
"empty": "No import jobs",
|
||||||
|
"job_id": "Job ID",
|
||||||
|
"marketplace": "Marketplace",
|
||||||
|
"vendor": "Vendor",
|
||||||
|
"status": "Status",
|
||||||
|
"imported": "Imported",
|
||||||
|
"updated": "Updated",
|
||||||
|
"errors": "Errors",
|
||||||
|
"created": "Created",
|
||||||
|
"completed": "Completed",
|
||||||
|
"statuses": {
|
||||||
|
"pending": "Pending",
|
||||||
|
"processing": "Processing",
|
||||||
|
"completed": "Completed",
|
||||||
|
"completed_with_errors": "Completed with Errors",
|
||||||
|
"failed": "Failed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"letzshop": {
|
||||||
|
"title": "Letzshop Integration",
|
||||||
|
"subtitle": "Manage Letzshop connection and sync",
|
||||||
|
"credentials": {
|
||||||
|
"title": "API Credentials",
|
||||||
|
"api_key": "API Key",
|
||||||
|
"api_key_help": "Your Letzshop API key",
|
||||||
|
"endpoint": "API Endpoint",
|
||||||
|
"test_mode": "Test Mode",
|
||||||
|
"test_mode_help": "When enabled, no changes are made to Letzshop"
|
||||||
|
},
|
||||||
|
"sync": {
|
||||||
|
"title": "Synchronization",
|
||||||
|
"auto_sync": "Auto Sync",
|
||||||
|
"auto_sync_help": "Automatically sync orders from Letzshop",
|
||||||
|
"interval": "Sync Interval",
|
||||||
|
"interval_help": "Minutes between syncs",
|
||||||
|
"last_sync": "Last Sync",
|
||||||
|
"last_status": "Last Status",
|
||||||
|
"sync_now": "Sync Now"
|
||||||
|
},
|
||||||
|
"carrier": {
|
||||||
|
"title": "Carrier Settings",
|
||||||
|
"default_carrier": "Default Carrier",
|
||||||
|
"greco": "Greco",
|
||||||
|
"colissimo": "Colissimo",
|
||||||
|
"xpresslogistics": "XpressLogistics",
|
||||||
|
"label_url": "Label URL Prefix"
|
||||||
|
},
|
||||||
|
"historical": {
|
||||||
|
"title": "Historical Import",
|
||||||
|
"subtitle": "Import past orders from Letzshop",
|
||||||
|
"start_import": "Start Historical Import",
|
||||||
|
"phase": "Phase",
|
||||||
|
"confirmed": "Confirmed Orders",
|
||||||
|
"unconfirmed": "Unconfirmed Orders",
|
||||||
|
"fetching": "Fetching...",
|
||||||
|
"processing": "Processing...",
|
||||||
|
"page": "Page",
|
||||||
|
"fetched": "Fetched",
|
||||||
|
"processed": "Processed",
|
||||||
|
"imported": "Imported",
|
||||||
|
"updated": "Updated",
|
||||||
|
"skipped": "Skipped"
|
||||||
|
},
|
||||||
|
"vendors": {
|
||||||
|
"title": "Vendor Directory",
|
||||||
|
"subtitle": "Browse Letzshop vendors",
|
||||||
|
"claim": "Claim",
|
||||||
|
"claimed": "Claimed",
|
||||||
|
"unclaimed": "Unclaimed",
|
||||||
|
"last_synced": "Last synced"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"title": "Export Products",
|
||||||
|
"subtitle": "Export products to marketplace format",
|
||||||
|
"format": "Format",
|
||||||
|
"format_csv": "CSV",
|
||||||
|
"format_xml": "XML",
|
||||||
|
"download": "Download Export"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"import_started": "Import started successfully",
|
||||||
|
"import_completed": "Import completed",
|
||||||
|
"import_failed": "Import failed",
|
||||||
|
"credentials_saved": "Credentials saved successfully",
|
||||||
|
"sync_started": "Sync started",
|
||||||
|
"sync_completed": "Sync completed",
|
||||||
|
"sync_failed": "Sync failed",
|
||||||
|
"export_ready": "Export ready for download",
|
||||||
|
"error_loading": "Error loading data"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"all_marketplaces": "All Marketplaces",
|
||||||
|
"all_vendors": "All Vendors",
|
||||||
|
"search_placeholder": "Search products..."
|
||||||
|
}
|
||||||
|
}
|
||||||
122
app/modules/marketplace/locales/fr.json
Normal file
122
app/modules/marketplace/locales/fr.json
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"title": "Intégration Marketplace",
|
||||||
|
"description": "Synchronisation des produits et commandes Letzshop",
|
||||||
|
"products": {
|
||||||
|
"title": "Produits Marketplace",
|
||||||
|
"subtitle": "Produits importés des marketplaces",
|
||||||
|
"empty": "Aucun produit trouvé",
|
||||||
|
"empty_search": "Aucun produit ne correspond à votre recherche",
|
||||||
|
"import": "Importer des produits"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"title": "Importer des produits",
|
||||||
|
"subtitle": "Importer des produits depuis les flux marketplace",
|
||||||
|
"source_url": "URL du flux",
|
||||||
|
"source_url_help": "URL du flux CSV marketplace",
|
||||||
|
"marketplace": "Marketplace",
|
||||||
|
"language": "Langue",
|
||||||
|
"language_help": "Langue pour les traductions de produits",
|
||||||
|
"batch_size": "Taille du lot",
|
||||||
|
"start_import": "Démarrer l'import",
|
||||||
|
"cancel": "Annuler"
|
||||||
|
},
|
||||||
|
"import_jobs": {
|
||||||
|
"title": "Historique des imports",
|
||||||
|
"subtitle": "Imports passés et en cours",
|
||||||
|
"empty": "Aucun import",
|
||||||
|
"job_id": "ID du job",
|
||||||
|
"marketplace": "Marketplace",
|
||||||
|
"vendor": "Vendeur",
|
||||||
|
"status": "Statut",
|
||||||
|
"imported": "Importés",
|
||||||
|
"updated": "Mis à jour",
|
||||||
|
"errors": "Erreurs",
|
||||||
|
"created": "Créé",
|
||||||
|
"completed": "Terminé",
|
||||||
|
"statuses": {
|
||||||
|
"pending": "En attente",
|
||||||
|
"processing": "En cours",
|
||||||
|
"completed": "Terminé",
|
||||||
|
"completed_with_errors": "Terminé avec erreurs",
|
||||||
|
"failed": "Échoué"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"letzshop": {
|
||||||
|
"title": "Intégration Letzshop",
|
||||||
|
"subtitle": "Gérer la connexion et la synchronisation Letzshop",
|
||||||
|
"credentials": {
|
||||||
|
"title": "Identifiants API",
|
||||||
|
"api_key": "Clé API",
|
||||||
|
"api_key_help": "Votre clé API Letzshop",
|
||||||
|
"endpoint": "Point d'accès API",
|
||||||
|
"test_mode": "Mode test",
|
||||||
|
"test_mode_help": "Lorsqu'activé, aucune modification n'est effectuée sur Letzshop"
|
||||||
|
},
|
||||||
|
"sync": {
|
||||||
|
"title": "Synchronisation",
|
||||||
|
"auto_sync": "Sync automatique",
|
||||||
|
"auto_sync_help": "Synchroniser automatiquement les commandes depuis Letzshop",
|
||||||
|
"interval": "Intervalle de sync",
|
||||||
|
"interval_help": "Minutes entre les synchronisations",
|
||||||
|
"last_sync": "Dernière sync",
|
||||||
|
"last_status": "Dernier statut",
|
||||||
|
"sync_now": "Synchroniser maintenant"
|
||||||
|
},
|
||||||
|
"carrier": {
|
||||||
|
"title": "Paramètres transporteur",
|
||||||
|
"default_carrier": "Transporteur par défaut",
|
||||||
|
"greco": "Greco",
|
||||||
|
"colissimo": "Colissimo",
|
||||||
|
"xpresslogistics": "XpressLogistics",
|
||||||
|
"label_url": "Préfixe URL étiquette"
|
||||||
|
},
|
||||||
|
"historical": {
|
||||||
|
"title": "Import historique",
|
||||||
|
"subtitle": "Importer les commandes passées depuis Letzshop",
|
||||||
|
"start_import": "Démarrer l'import historique",
|
||||||
|
"phase": "Phase",
|
||||||
|
"confirmed": "Commandes confirmées",
|
||||||
|
"unconfirmed": "Commandes non confirmées",
|
||||||
|
"fetching": "Récupération...",
|
||||||
|
"processing": "Traitement...",
|
||||||
|
"page": "Page",
|
||||||
|
"fetched": "Récupérées",
|
||||||
|
"processed": "Traitées",
|
||||||
|
"imported": "Importées",
|
||||||
|
"updated": "Mises à jour",
|
||||||
|
"skipped": "Ignorées"
|
||||||
|
},
|
||||||
|
"vendors": {
|
||||||
|
"title": "Annuaire des vendeurs",
|
||||||
|
"subtitle": "Parcourir les vendeurs Letzshop",
|
||||||
|
"claim": "Revendiquer",
|
||||||
|
"claimed": "Revendiqué",
|
||||||
|
"unclaimed": "Non revendiqué",
|
||||||
|
"last_synced": "Dernière sync"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"title": "Exporter les produits",
|
||||||
|
"subtitle": "Exporter les produits au format marketplace",
|
||||||
|
"format": "Format",
|
||||||
|
"format_csv": "CSV",
|
||||||
|
"format_xml": "XML",
|
||||||
|
"download": "Télécharger l'export"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"import_started": "Import démarré avec succès",
|
||||||
|
"import_completed": "Import terminé",
|
||||||
|
"import_failed": "Import échoué",
|
||||||
|
"credentials_saved": "Identifiants enregistrés avec succès",
|
||||||
|
"sync_started": "Synchronisation démarrée",
|
||||||
|
"sync_completed": "Synchronisation terminée",
|
||||||
|
"sync_failed": "Synchronisation échouée",
|
||||||
|
"export_ready": "Export prêt au téléchargement",
|
||||||
|
"error_loading": "Erreur lors du chargement des données"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"all_marketplaces": "Tous les marketplaces",
|
||||||
|
"all_vendors": "Tous les vendeurs",
|
||||||
|
"search_placeholder": "Rechercher des produits..."
|
||||||
|
}
|
||||||
|
}
|
||||||
122
app/modules/marketplace/locales/lb.json
Normal file
122
app/modules/marketplace/locales/lb.json
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"title": "Marketplace-Integratioun",
|
||||||
|
"description": "Letzshop Produkt- a Bestellsynchronisatioun",
|
||||||
|
"products": {
|
||||||
|
"title": "Marketplace-Produkter",
|
||||||
|
"subtitle": "Vun Marketplacen importéiert Produkter",
|
||||||
|
"empty": "Keng Produkter fonnt",
|
||||||
|
"empty_search": "Keng Produkter passen zu Ärer Sich",
|
||||||
|
"import": "Produkter importéieren"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"title": "Produkter importéieren",
|
||||||
|
"subtitle": "Produkter aus Marketplace-Feeds importéieren",
|
||||||
|
"source_url": "Feed-URL",
|
||||||
|
"source_url_help": "URL zum Marketplace-CSV-Feed",
|
||||||
|
"marketplace": "Marketplace",
|
||||||
|
"language": "Sprooch",
|
||||||
|
"language_help": "Sprooch fir Produktiwwersetzungen",
|
||||||
|
"batch_size": "Batch-Gréisst",
|
||||||
|
"start_import": "Import starten",
|
||||||
|
"cancel": "Ofbriechen"
|
||||||
|
},
|
||||||
|
"import_jobs": {
|
||||||
|
"title": "Import-Verlaf",
|
||||||
|
"subtitle": "Vergaangen an aktuell Import-Jobs",
|
||||||
|
"empty": "Keng Import-Jobs",
|
||||||
|
"job_id": "Job-ID",
|
||||||
|
"marketplace": "Marketplace",
|
||||||
|
"vendor": "Verkeefer",
|
||||||
|
"status": "Status",
|
||||||
|
"imported": "Importéiert",
|
||||||
|
"updated": "Aktualiséiert",
|
||||||
|
"errors": "Feeler",
|
||||||
|
"created": "Erstallt",
|
||||||
|
"completed": "Ofgeschloss",
|
||||||
|
"statuses": {
|
||||||
|
"pending": "Waarden",
|
||||||
|
"processing": "Am Gaang",
|
||||||
|
"completed": "Ofgeschloss",
|
||||||
|
"completed_with_errors": "Mat Feeler ofgeschloss",
|
||||||
|
"failed": "Feelgeschloen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"letzshop": {
|
||||||
|
"title": "Letzshop-Integratioun",
|
||||||
|
"subtitle": "Letzshop-Verbindung a Synchronisatioun verwalten",
|
||||||
|
"credentials": {
|
||||||
|
"title": "API-Umeldedaten",
|
||||||
|
"api_key": "API-Schlëssel",
|
||||||
|
"api_key_help": "Ären Letzshop API-Schlëssel",
|
||||||
|
"endpoint": "API-Endpunkt",
|
||||||
|
"test_mode": "Testmodus",
|
||||||
|
"test_mode_help": "Wann aktivéiert, ginn keng Ännerungen bei Letzshop gemaach"
|
||||||
|
},
|
||||||
|
"sync": {
|
||||||
|
"title": "Synchronisatioun",
|
||||||
|
"auto_sync": "Auto-Sync",
|
||||||
|
"auto_sync_help": "Bestellungen automatesch vun Letzshop synchroniséieren",
|
||||||
|
"interval": "Sync-Intervall",
|
||||||
|
"interval_help": "Minutten tëscht Synchronisatiounen",
|
||||||
|
"last_sync": "Lescht Sync",
|
||||||
|
"last_status": "Leschte Status",
|
||||||
|
"sync_now": "Elo synchroniséieren"
|
||||||
|
},
|
||||||
|
"carrier": {
|
||||||
|
"title": "Versandastellungen",
|
||||||
|
"default_carrier": "Standard-Versanddienstleeschter",
|
||||||
|
"greco": "Greco",
|
||||||
|
"colissimo": "Colissimo",
|
||||||
|
"xpresslogistics": "XpressLogistics",
|
||||||
|
"label_url": "Label-URL-Präfix"
|
||||||
|
},
|
||||||
|
"historical": {
|
||||||
|
"title": "Historeschen Import",
|
||||||
|
"subtitle": "Vergaangen Bestellungen vun Letzshop importéieren",
|
||||||
|
"start_import": "Historeschen Import starten",
|
||||||
|
"phase": "Phas",
|
||||||
|
"confirmed": "Bestätegt Bestellungen",
|
||||||
|
"unconfirmed": "Onbestätegt Bestellungen",
|
||||||
|
"fetching": "Ofruff...",
|
||||||
|
"processing": "Veraarbecht...",
|
||||||
|
"page": "Säit",
|
||||||
|
"fetched": "Ofgeruff",
|
||||||
|
"processed": "Veraarbecht",
|
||||||
|
"imported": "Importéiert",
|
||||||
|
"updated": "Aktualiséiert",
|
||||||
|
"skipped": "Iwwersprong"
|
||||||
|
},
|
||||||
|
"vendors": {
|
||||||
|
"title": "Verkeeferverzeechnes",
|
||||||
|
"subtitle": "Letzshop-Verkeefer duerchsichen",
|
||||||
|
"claim": "Reklaméieren",
|
||||||
|
"claimed": "Reklaméiert",
|
||||||
|
"unclaimed": "Net reklaméiert",
|
||||||
|
"last_synced": "Lescht synchroniséiert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"title": "Produkter exportéieren",
|
||||||
|
"subtitle": "Produkter am Marketplace-Format exportéieren",
|
||||||
|
"format": "Format",
|
||||||
|
"format_csv": "CSV",
|
||||||
|
"format_xml": "XML",
|
||||||
|
"download": "Export eroflueden"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"import_started": "Import erfollegräich gestart",
|
||||||
|
"import_completed": "Import ofgeschloss",
|
||||||
|
"import_failed": "Import feelgeschloen",
|
||||||
|
"credentials_saved": "Umeldedaten erfollegräich gespäichert",
|
||||||
|
"sync_started": "Synchronisatioun gestart",
|
||||||
|
"sync_completed": "Synchronisatioun ofgeschloss",
|
||||||
|
"sync_failed": "Synchronisatioun feelgeschloen",
|
||||||
|
"export_ready": "Export prett zum Eroflueden",
|
||||||
|
"error_loading": "Feeler beim Lueden vun den Daten"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"all_marketplaces": "All Marketplacen",
|
||||||
|
"all_vendors": "All Verkeefer",
|
||||||
|
"search_placeholder": "Produkter sichen..."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
# app/modules/marketplace/models/__init__.py
|
# app/modules/marketplace/models/__init__.py
|
||||||
"""
|
"""
|
||||||
Marketplace module models.
|
Marketplace module database models.
|
||||||
|
|
||||||
Re-exports marketplace and Letzshop models from the central models location.
|
This is the canonical location for marketplace models. Module models are automatically
|
||||||
Models remain in models/database/ for now to avoid breaking existing imports.
|
discovered and registered with SQLAlchemy's Base.metadata at startup.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from app.modules.marketplace.models import (
|
from app.modules.marketplace.models import (
|
||||||
@@ -14,10 +14,19 @@ Usage:
|
|||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from models.database.marketplace_product import MarketplaceProduct
|
from app.modules.marketplace.models.marketplace_product import (
|
||||||
from models.database.marketplace_product_translation import MarketplaceProductTranslation
|
MarketplaceProduct,
|
||||||
from models.database.marketplace_import_job import MarketplaceImportJob
|
ProductType,
|
||||||
from models.database.letzshop import (
|
DigitalDeliveryMethod,
|
||||||
|
)
|
||||||
|
from app.modules.marketplace.models.marketplace_product_translation import (
|
||||||
|
MarketplaceProductTranslation,
|
||||||
|
)
|
||||||
|
from app.modules.marketplace.models.marketplace_import_job import (
|
||||||
|
MarketplaceImportJob,
|
||||||
|
MarketplaceImportError,
|
||||||
|
)
|
||||||
|
from app.modules.marketplace.models.letzshop import (
|
||||||
# Letzshop credentials and sync
|
# Letzshop credentials and sync
|
||||||
VendorLetzshopCredentials,
|
VendorLetzshopCredentials,
|
||||||
LetzshopFulfillmentQueue,
|
LetzshopFulfillmentQueue,
|
||||||
@@ -31,8 +40,11 @@ __all__ = [
|
|||||||
# Marketplace products
|
# Marketplace products
|
||||||
"MarketplaceProduct",
|
"MarketplaceProduct",
|
||||||
"MarketplaceProductTranslation",
|
"MarketplaceProductTranslation",
|
||||||
|
"ProductType",
|
||||||
|
"DigitalDeliveryMethod",
|
||||||
# Import jobs
|
# Import jobs
|
||||||
"MarketplaceImportJob",
|
"MarketplaceImportJob",
|
||||||
|
"MarketplaceImportError",
|
||||||
"LetzshopHistoricalImportJob",
|
"LetzshopHistoricalImportJob",
|
||||||
# Letzshop models
|
# Letzshop models
|
||||||
"VendorLetzshopCredentials",
|
"VendorLetzshopCredentials",
|
||||||
|
|||||||
379
app/modules/marketplace/models/letzshop.py
Normal file
379
app/modules/marketplace/models/letzshop.py
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
# app/modules/marketplace/models/letzshop.py
|
||||||
|
"""
|
||||||
|
Database models for Letzshop marketplace integration.
|
||||||
|
|
||||||
|
Provides models for:
|
||||||
|
- VendorLetzshopCredentials: Per-vendor API key storage (encrypted)
|
||||||
|
- LetzshopFulfillmentQueue: Outbound operation queue with retry
|
||||||
|
- LetzshopSyncLog: Audit trail for sync operations
|
||||||
|
- LetzshopHistoricalImportJob: Progress tracking for historical imports
|
||||||
|
|
||||||
|
Note: Orders are now stored in the unified `orders` table with channel='letzshop'.
|
||||||
|
The LetzshopOrder model has been removed in favor of the unified Order model.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 VendorLetzshopCredentials(Base, TimestampMixin):
|
||||||
|
"""
|
||||||
|
Per-vendor Letzshop API credentials.
|
||||||
|
|
||||||
|
Stores encrypted API keys and sync settings for each vendor's
|
||||||
|
Letzshop integration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "vendor_letzshop_credentials"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
vendor_id = Column(
|
||||||
|
Integer, ForeignKey("vendors.id"), unique=True, nullable=False, index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Encrypted API credentials
|
||||||
|
api_key_encrypted = Column(Text, nullable=False)
|
||||||
|
api_endpoint = Column(String(255), default="https://letzshop.lu/graphql")
|
||||||
|
|
||||||
|
# Sync settings
|
||||||
|
auto_sync_enabled = Column(Boolean, default=False)
|
||||||
|
sync_interval_minutes = Column(Integer, default=15)
|
||||||
|
|
||||||
|
# Test mode (disables API mutations when enabled)
|
||||||
|
test_mode_enabled = Column(Boolean, default=False)
|
||||||
|
|
||||||
|
# Default carrier settings
|
||||||
|
default_carrier = Column(String(50), nullable=True) # greco, colissimo, xpresslogistics
|
||||||
|
|
||||||
|
# Carrier label URL prefixes
|
||||||
|
carrier_greco_label_url = Column(
|
||||||
|
String(500), default="https://dispatchweb.fr/Tracky/Home/"
|
||||||
|
)
|
||||||
|
carrier_colissimo_label_url = Column(String(500), nullable=True)
|
||||||
|
carrier_xpresslogistics_label_url = Column(String(500), nullable=True)
|
||||||
|
|
||||||
|
# Last sync status
|
||||||
|
last_sync_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
last_sync_status = Column(String(50), nullable=True) # success, failed, partial
|
||||||
|
last_sync_error = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
vendor = relationship("Vendor", back_populates="letzshop_credentials")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<VendorLetzshopCredentials(vendor_id={self.vendor_id}, auto_sync={self.auto_sync_enabled})>"
|
||||||
|
|
||||||
|
|
||||||
|
class LetzshopFulfillmentQueue(Base, TimestampMixin):
|
||||||
|
"""
|
||||||
|
Queue for outbound fulfillment operations to Letzshop.
|
||||||
|
|
||||||
|
Supports retry logic for failed operations.
|
||||||
|
References the unified orders table.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "letzshop_fulfillment_queue"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False, index=True)
|
||||||
|
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False, index=True)
|
||||||
|
|
||||||
|
# Operation type
|
||||||
|
operation = Column(
|
||||||
|
String(50), nullable=False
|
||||||
|
) # confirm_item, decline_item, set_tracking
|
||||||
|
|
||||||
|
# Operation payload
|
||||||
|
payload = Column(JSON, nullable=False)
|
||||||
|
|
||||||
|
# Status and retry
|
||||||
|
status = Column(
|
||||||
|
String(50), default="pending"
|
||||||
|
) # pending, processing, completed, failed
|
||||||
|
attempts = Column(Integer, default=0)
|
||||||
|
max_attempts = Column(Integer, default=3)
|
||||||
|
last_attempt_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
next_retry_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
error_message = Column(Text, nullable=True)
|
||||||
|
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
|
# Response from Letzshop
|
||||||
|
response_data = Column(JSON, nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
vendor = relationship("Vendor")
|
||||||
|
order = relationship("Order")
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_fulfillment_queue_status", "status", "vendor_id"),
|
||||||
|
Index("idx_fulfillment_queue_retry", "status", "next_retry_at"),
|
||||||
|
Index("idx_fulfillment_queue_order", "order_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<LetzshopFulfillmentQueue(id={self.id}, order_id={self.order_id}, operation='{self.operation}', status='{self.status}')>"
|
||||||
|
|
||||||
|
|
||||||
|
class LetzshopSyncLog(Base, TimestampMixin):
|
||||||
|
"""
|
||||||
|
Audit log for all Letzshop sync operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "letzshop_sync_logs"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False, index=True)
|
||||||
|
|
||||||
|
# Operation details
|
||||||
|
operation_type = Column(
|
||||||
|
String(50), nullable=False
|
||||||
|
) # order_import, confirm_inventory, set_tracking, etc.
|
||||||
|
direction = Column(String(10), nullable=False) # inbound, outbound
|
||||||
|
|
||||||
|
# Status
|
||||||
|
status = Column(String(50), nullable=False) # success, failed, partial
|
||||||
|
|
||||||
|
# Details
|
||||||
|
records_processed = Column(Integer, default=0)
|
||||||
|
records_succeeded = Column(Integer, default=0)
|
||||||
|
records_failed = Column(Integer, default=0)
|
||||||
|
error_details = Column(JSON, nullable=True)
|
||||||
|
|
||||||
|
# Timestamps
|
||||||
|
started_at = Column(DateTime(timezone=True), nullable=False)
|
||||||
|
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
duration_seconds = Column(Integer, nullable=True)
|
||||||
|
|
||||||
|
# Triggered by
|
||||||
|
triggered_by = Column(String(100), nullable=True) # user_id, scheduler, webhook
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
vendor = relationship("Vendor")
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_sync_log_vendor_type", "vendor_id", "operation_type"),
|
||||||
|
Index("idx_sync_log_vendor_date", "vendor_id", "started_at"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<LetzshopSyncLog(id={self.id}, type='{self.operation_type}', status='{self.status}')>"
|
||||||
|
|
||||||
|
|
||||||
|
class LetzshopVendorCache(Base, TimestampMixin):
|
||||||
|
"""
|
||||||
|
Cache of Letzshop marketplace vendor directory.
|
||||||
|
|
||||||
|
This table stores vendor data fetched from Letzshop's public GraphQL API,
|
||||||
|
allowing users to browse and claim existing Letzshop shops during signup.
|
||||||
|
|
||||||
|
Data is periodically synced from Letzshop (e.g., daily via Celery task).
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "letzshop_vendor_cache"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
|
||||||
|
# Letzshop identifiers
|
||||||
|
letzshop_id = Column(String(50), unique=True, nullable=False, index=True)
|
||||||
|
"""Unique ID from Letzshop (e.g., 'lpkedYMRup')."""
|
||||||
|
|
||||||
|
slug = Column(String(200), unique=True, nullable=False, index=True)
|
||||||
|
"""URL slug (e.g., 'nicks-diecast-corner')."""
|
||||||
|
|
||||||
|
# Basic info
|
||||||
|
name = Column(String(255), nullable=False)
|
||||||
|
"""Vendor display name."""
|
||||||
|
|
||||||
|
company_name = Column(String(255), nullable=True)
|
||||||
|
"""Legal company name."""
|
||||||
|
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
"""Whether vendor is active on Letzshop."""
|
||||||
|
|
||||||
|
# Descriptions (multilingual)
|
||||||
|
description_en = Column(Text, nullable=True)
|
||||||
|
description_fr = Column(Text, nullable=True)
|
||||||
|
description_de = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
# Contact information
|
||||||
|
email = Column(String(255), nullable=True)
|
||||||
|
phone = Column(String(50), nullable=True)
|
||||||
|
fax = Column(String(50), nullable=True)
|
||||||
|
website = Column(String(500), nullable=True)
|
||||||
|
|
||||||
|
# Location
|
||||||
|
street = Column(String(255), nullable=True)
|
||||||
|
street_number = Column(String(50), nullable=True)
|
||||||
|
city = Column(String(100), nullable=True)
|
||||||
|
zipcode = Column(String(20), nullable=True)
|
||||||
|
country_iso = Column(String(5), default="LU")
|
||||||
|
latitude = Column(String(20), nullable=True)
|
||||||
|
longitude = Column(String(20), nullable=True)
|
||||||
|
|
||||||
|
# Categories (stored as JSON array of names)
|
||||||
|
categories = Column(JSON, default=list)
|
||||||
|
"""List of category names, e.g., ['Fashion', 'Shoes']."""
|
||||||
|
|
||||||
|
# Images
|
||||||
|
background_image_url = Column(String(500), nullable=True)
|
||||||
|
|
||||||
|
# Social media (stored as JSON array of URLs)
|
||||||
|
social_media_links = Column(JSON, default=list)
|
||||||
|
"""List of social media URLs."""
|
||||||
|
|
||||||
|
# Opening hours (multilingual text)
|
||||||
|
opening_hours_en = Column(Text, nullable=True)
|
||||||
|
opening_hours_fr = Column(Text, nullable=True)
|
||||||
|
opening_hours_de = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
# Representative
|
||||||
|
representative_name = Column(String(255), nullable=True)
|
||||||
|
representative_title = Column(String(100), nullable=True)
|
||||||
|
|
||||||
|
# Claiming status (linked to our platform)
|
||||||
|
claimed_by_vendor_id = Column(
|
||||||
|
Integer, ForeignKey("vendors.id"), nullable=True, index=True
|
||||||
|
)
|
||||||
|
"""If claimed, links to our Vendor record."""
|
||||||
|
|
||||||
|
claimed_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
"""When the vendor was claimed on our platform."""
|
||||||
|
|
||||||
|
# Sync metadata
|
||||||
|
last_synced_at = Column(DateTime(timezone=True), nullable=False)
|
||||||
|
"""When this record was last updated from Letzshop."""
|
||||||
|
|
||||||
|
raw_data = Column(JSON, nullable=True)
|
||||||
|
"""Full raw response from Letzshop API for reference."""
|
||||||
|
|
||||||
|
# Relationship to claimed vendor
|
||||||
|
claimed_vendor = relationship("Vendor", foreign_keys=[claimed_by_vendor_id])
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_vendor_cache_city", "city"),
|
||||||
|
Index("idx_vendor_cache_claimed", "claimed_by_vendor_id"),
|
||||||
|
Index("idx_vendor_cache_active", "is_active"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<LetzshopVendorCache(id={self.id}, slug='{self.slug}', name='{self.name}')>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_claimed(self) -> bool:
|
||||||
|
"""Check if this vendor has been claimed on our platform."""
|
||||||
|
return self.claimed_by_vendor_id is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def letzshop_url(self) -> str:
|
||||||
|
"""Get the Letzshop profile URL."""
|
||||||
|
return f"https://letzshop.lu/vendors/{self.slug}"
|
||||||
|
|
||||||
|
def get_description(self, lang: str = "en") -> str | None:
|
||||||
|
"""Get description in specified language with fallback."""
|
||||||
|
descriptions = {
|
||||||
|
"en": self.description_en,
|
||||||
|
"fr": self.description_fr,
|
||||||
|
"de": self.description_de,
|
||||||
|
}
|
||||||
|
# Try requested language, then fallback order
|
||||||
|
for try_lang in [lang, "en", "fr", "de"]:
|
||||||
|
if descriptions.get(try_lang):
|
||||||
|
return descriptions[try_lang]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_opening_hours(self, lang: str = "en") -> str | None:
|
||||||
|
"""Get opening hours in specified language with fallback."""
|
||||||
|
hours = {
|
||||||
|
"en": self.opening_hours_en,
|
||||||
|
"fr": self.opening_hours_fr,
|
||||||
|
"de": self.opening_hours_de,
|
||||||
|
}
|
||||||
|
for try_lang in [lang, "en", "fr", "de"]:
|
||||||
|
if hours.get(try_lang):
|
||||||
|
return hours[try_lang]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_full_address(self) -> str | None:
|
||||||
|
"""Get formatted full address."""
|
||||||
|
parts = []
|
||||||
|
if self.street:
|
||||||
|
addr = self.street
|
||||||
|
if self.street_number:
|
||||||
|
addr += f" {self.street_number}"
|
||||||
|
parts.append(addr)
|
||||||
|
if self.zipcode or self.city:
|
||||||
|
parts.append(f"{self.zipcode or ''} {self.city or ''}".strip())
|
||||||
|
return ", ".join(parts) if parts else None
|
||||||
|
|
||||||
|
|
||||||
|
class LetzshopHistoricalImportJob(Base, TimestampMixin):
|
||||||
|
"""
|
||||||
|
Track progress of historical order imports from Letzshop.
|
||||||
|
|
||||||
|
Enables real-time progress tracking via polling for long-running imports.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "letzshop_historical_import_jobs"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False, index=True)
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
|
||||||
|
# Status: pending | fetching | processing | completed | failed
|
||||||
|
status = Column(String(50), default="pending", nullable=False)
|
||||||
|
|
||||||
|
# Current phase: "confirmed" | "declined"
|
||||||
|
current_phase = Column(String(20), nullable=True)
|
||||||
|
|
||||||
|
# Fetch progress
|
||||||
|
current_page = Column(Integer, default=0)
|
||||||
|
total_pages = Column(Integer, nullable=True) # null = unknown yet
|
||||||
|
shipments_fetched = Column(Integer, default=0)
|
||||||
|
|
||||||
|
# Processing progress
|
||||||
|
orders_processed = Column(Integer, default=0)
|
||||||
|
orders_imported = Column(Integer, default=0)
|
||||||
|
orders_updated = Column(Integer, default=0)
|
||||||
|
orders_skipped = Column(Integer, default=0)
|
||||||
|
|
||||||
|
# EAN matching stats
|
||||||
|
products_matched = Column(Integer, default=0)
|
||||||
|
products_not_found = Column(Integer, default=0)
|
||||||
|
|
||||||
|
# Phase-specific stats (stored as JSON for combining confirmed + declined)
|
||||||
|
confirmed_stats = Column(JSON, nullable=True)
|
||||||
|
declined_stats = Column(JSON, nullable=True)
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
error_message = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
# Celery task tracking (optional - for USE_CELERY=true)
|
||||||
|
celery_task_id = Column(String(255), nullable=True, index=True)
|
||||||
|
|
||||||
|
# Timing
|
||||||
|
started_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
vendor = relationship("Vendor")
|
||||||
|
user = relationship("User")
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_historical_import_vendor", "vendor_id", "status"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<LetzshopHistoricalImportJob(id={self.id}, status='{self.status}', phase='{self.current_phase}')>"
|
||||||
119
app/modules/marketplace/models/marketplace_import_job.py
Normal file
119
app/modules/marketplace/models/marketplace_import_job.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# app/modules/marketplace/models/marketplace_import_job.py
|
||||||
|
"""Marketplace Import Job models for tracking CSV and historical imports."""
|
||||||
|
|
||||||
|
from sqlalchemy import JSON, Column, DateTime, ForeignKey, Index, Integer, String, Text
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from app.core.database import Base
|
||||||
|
from models.database.base import TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceImportError(Base, TimestampMixin):
|
||||||
|
"""
|
||||||
|
Stores detailed information about individual import errors.
|
||||||
|
|
||||||
|
Each row that fails during import creates an error record with:
|
||||||
|
- Row number from the source file
|
||||||
|
- Identifier (marketplace_product_id if available)
|
||||||
|
- Error type and message
|
||||||
|
- Raw row data for review
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "marketplace_import_errors"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
import_job_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("marketplace_import_jobs.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Error location
|
||||||
|
row_number = Column(Integer, nullable=False)
|
||||||
|
|
||||||
|
# Identifier from the row (if available)
|
||||||
|
identifier = Column(String) # marketplace_product_id, gtin, mpn, etc.
|
||||||
|
|
||||||
|
# Error details
|
||||||
|
error_type = Column(
|
||||||
|
String(50), nullable=False
|
||||||
|
) # missing_title, missing_id, parse_error, etc.
|
||||||
|
error_message = Column(Text, nullable=False)
|
||||||
|
|
||||||
|
# Raw row data for review (JSON)
|
||||||
|
row_data = Column(JSON)
|
||||||
|
|
||||||
|
# Relationship
|
||||||
|
import_job = relationship("MarketplaceImportJob", back_populates="errors")
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_import_error_job_id", "import_job_id"),
|
||||||
|
Index("idx_import_error_type", "error_type"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"<MarketplaceImportError(id={self.id}, job_id={self.import_job_id}, "
|
||||||
|
f"row={self.row_number}, type='{self.error_type}')>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceImportJob(Base, TimestampMixin):
|
||||||
|
__tablename__ = "marketplace_import_jobs"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False, index=True)
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
|
||||||
|
# Import configuration
|
||||||
|
marketplace = Column(String, nullable=False, index=True, default="Letzshop")
|
||||||
|
source_url = Column(String, nullable=False)
|
||||||
|
language = Column(
|
||||||
|
String(5), nullable=False, default="en"
|
||||||
|
) # Language for translations
|
||||||
|
|
||||||
|
# Status tracking
|
||||||
|
status = Column(
|
||||||
|
String, nullable=False, default="pending"
|
||||||
|
) # pending, processing, completed, failed, completed_with_errors
|
||||||
|
|
||||||
|
# Results
|
||||||
|
imported_count = Column(Integer, default=0)
|
||||||
|
updated_count = Column(Integer, default=0)
|
||||||
|
error_count = Column(Integer, default=0)
|
||||||
|
total_processed = Column(Integer, default=0)
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
error_message = Column(Text)
|
||||||
|
|
||||||
|
# Celery task tracking (optional - for USE_CELERY=true)
|
||||||
|
celery_task_id = Column(String(255), nullable=True, index=True)
|
||||||
|
|
||||||
|
# Timestamps
|
||||||
|
started_at = Column(DateTime(timezone=True))
|
||||||
|
completed_at = Column(DateTime(timezone=True))
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
vendor = relationship("Vendor", back_populates="marketplace_import_jobs")
|
||||||
|
user = relationship("User", foreign_keys=[user_id])
|
||||||
|
errors = relationship(
|
||||||
|
"MarketplaceImportError",
|
||||||
|
back_populates="import_job",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
order_by="MarketplaceImportError.row_number",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Indexes for performance
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_import_vendor_status", "vendor_id", "status"),
|
||||||
|
Index("idx_import_vendor_created", "vendor_id", "created_at"),
|
||||||
|
Index("idx_import_user_marketplace", "user_id", "marketplace"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"<MarketplaceImportJob(id={self.id}, vendor_id={self.vendor_id}, "
|
||||||
|
f"marketplace='{self.marketplace}', status='{self.status}', "
|
||||||
|
f"imported={self.imported_count})>"
|
||||||
|
)
|
||||||
307
app/modules/marketplace/models/marketplace_product.py
Normal file
307
app/modules/marketplace/models/marketplace_product.py
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
# app/modules/marketplace/models/marketplace_product.py
|
||||||
|
"""Marketplace Product model for multi-marketplace product integration.
|
||||||
|
|
||||||
|
This model stores canonical product data from various marketplaces (Letzshop,
|
||||||
|
Amazon, eBay, CodesWholesale, etc.) in a universal format. It supports:
|
||||||
|
- Physical and digital products
|
||||||
|
- Multi-language translations (via MarketplaceProductTranslation)
|
||||||
|
- Flexible attributes for marketplace-specific data
|
||||||
|
- Google Shopping fields for Letzshop compatibility
|
||||||
|
|
||||||
|
Money values are stored as integer cents (e.g., €105.91 = 10591).
|
||||||
|
Weight is stored as integer grams (e.g., 1.5kg = 1500g).
|
||||||
|
See docs/architecture/money-handling.md for details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
Boolean,
|
||||||
|
Column,
|
||||||
|
Index,
|
||||||
|
Integer,
|
||||||
|
String,
|
||||||
|
)
|
||||||
|
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 ProductType(str, Enum):
|
||||||
|
"""Product type classification."""
|
||||||
|
|
||||||
|
PHYSICAL = "physical"
|
||||||
|
DIGITAL = "digital"
|
||||||
|
SERVICE = "service"
|
||||||
|
SUBSCRIPTION = "subscription"
|
||||||
|
|
||||||
|
|
||||||
|
class DigitalDeliveryMethod(str, Enum):
|
||||||
|
"""Digital product delivery methods."""
|
||||||
|
|
||||||
|
DOWNLOAD = "download"
|
||||||
|
EMAIL = "email"
|
||||||
|
IN_APP = "in_app"
|
||||||
|
STREAMING = "streaming"
|
||||||
|
LICENSE_KEY = "license_key"
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceProduct(Base, TimestampMixin):
|
||||||
|
"""Canonical product data from marketplace sources.
|
||||||
|
|
||||||
|
This table stores normalized product information from all marketplace sources.
|
||||||
|
Localized content (title, description) is stored in MarketplaceProductTranslation.
|
||||||
|
|
||||||
|
Price fields use integer cents for precision (€19.99 = 1999 cents).
|
||||||
|
Weight uses integer grams (1.5kg = 1500 grams).
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "marketplace_products"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
|
||||||
|
# === UNIVERSAL IDENTIFIERS ===
|
||||||
|
marketplace_product_id = Column(String, unique=True, index=True, nullable=False)
|
||||||
|
gtin = Column(String, index=True) # EAN/UPC - primary cross-marketplace matching
|
||||||
|
mpn = Column(String, index=True) # Manufacturer Part Number
|
||||||
|
sku = Column(String, index=True) # Internal SKU if assigned
|
||||||
|
|
||||||
|
# === SOURCE TRACKING ===
|
||||||
|
marketplace = Column(
|
||||||
|
String, index=True, nullable=True, default="letzshop"
|
||||||
|
) # 'letzshop', 'amazon', 'ebay', 'codeswholesale'
|
||||||
|
source_url = Column(String) # Original product URL
|
||||||
|
vendor_name = Column(String, index=True) # Seller/vendor in marketplace
|
||||||
|
|
||||||
|
# === PRODUCT TYPE ===
|
||||||
|
product_type_enum = Column(
|
||||||
|
String(20), nullable=False, default=ProductType.PHYSICAL.value
|
||||||
|
)
|
||||||
|
is_digital = Column(Boolean, default=False, index=True)
|
||||||
|
|
||||||
|
# === DIGITAL PRODUCT FIELDS ===
|
||||||
|
digital_delivery_method = Column(String(20)) # DigitalDeliveryMethod values
|
||||||
|
platform = Column(String(50), index=True) # 'steam', 'playstation', 'xbox', etc.
|
||||||
|
region_restrictions = Column(JSON) # ["EU", "US"] or null for global
|
||||||
|
license_type = Column(String(50)) # 'single_use', 'subscription', 'lifetime'
|
||||||
|
|
||||||
|
# === NON-LOCALIZED FIELDS ===
|
||||||
|
brand = Column(String, index=True)
|
||||||
|
google_product_category = Column(String, index=True)
|
||||||
|
category_path = Column(String) # Normalized category hierarchy
|
||||||
|
condition = Column(String)
|
||||||
|
|
||||||
|
# === PRICING (stored as integer cents) ===
|
||||||
|
price = Column(String) # Raw price string "19.99 EUR" (kept for reference)
|
||||||
|
price_cents = Column(Integer) # Parsed numeric price in cents
|
||||||
|
sale_price = Column(String) # Raw sale price string
|
||||||
|
sale_price_cents = Column(Integer) # Parsed numeric sale price in cents
|
||||||
|
currency = Column(String(3), default="EUR")
|
||||||
|
|
||||||
|
# === TAX / VAT ===
|
||||||
|
# Luxembourg VAT rates: 0 (exempt), 3 (super-reduced), 8 (reduced), 14 (intermediate), 17 (standard)
|
||||||
|
# Prices are stored as gross (VAT-inclusive). Default to standard rate.
|
||||||
|
tax_rate_percent = Column(Integer, default=17, nullable=False)
|
||||||
|
|
||||||
|
# === MEDIA ===
|
||||||
|
image_link = Column(String)
|
||||||
|
additional_image_link = Column(String) # Legacy single string
|
||||||
|
additional_images = Column(JSON) # Array of image URLs
|
||||||
|
|
||||||
|
# === PRODUCT ATTRIBUTES (Flexible) ===
|
||||||
|
attributes = Column(JSON) # {color, size, material, etc.}
|
||||||
|
|
||||||
|
# === PHYSICAL PRODUCT FIELDS ===
|
||||||
|
weight_grams = Column(Integer) # Weight in grams (1.5kg = 1500)
|
||||||
|
weight_unit = Column(String(10), default="kg") # Display unit
|
||||||
|
dimensions = Column(JSON) # {length, width, height, unit}
|
||||||
|
|
||||||
|
# === GOOGLE SHOPPING FIELDS (Preserved for Letzshop) ===
|
||||||
|
link = Column(String)
|
||||||
|
availability = Column(String, index=True)
|
||||||
|
adult = Column(String)
|
||||||
|
multipack = Column(Integer)
|
||||||
|
is_bundle = Column(String)
|
||||||
|
age_group = Column(String)
|
||||||
|
color = Column(String)
|
||||||
|
gender = Column(String)
|
||||||
|
material = Column(String)
|
||||||
|
pattern = Column(String)
|
||||||
|
size = Column(String)
|
||||||
|
size_type = Column(String)
|
||||||
|
size_system = Column(String)
|
||||||
|
item_group_id = Column(String)
|
||||||
|
product_type_raw = Column(String) # Original feed value (renamed from product_type)
|
||||||
|
custom_label_0 = Column(String)
|
||||||
|
custom_label_1 = Column(String)
|
||||||
|
custom_label_2 = Column(String)
|
||||||
|
custom_label_3 = Column(String)
|
||||||
|
custom_label_4 = Column(String)
|
||||||
|
unit_pricing_measure = Column(String)
|
||||||
|
unit_pricing_base_measure = Column(String)
|
||||||
|
identifier_exists = Column(String)
|
||||||
|
shipping = Column(String)
|
||||||
|
|
||||||
|
# === STATUS ===
|
||||||
|
is_active = Column(Boolean, default=True, index=True)
|
||||||
|
|
||||||
|
# === RELATIONSHIPS ===
|
||||||
|
translations = relationship(
|
||||||
|
"MarketplaceProductTranslation",
|
||||||
|
back_populates="marketplace_product",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
vendor_products = relationship("Product", back_populates="marketplace_product")
|
||||||
|
|
||||||
|
# === INDEXES ===
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_marketplace_vendor", "marketplace", "vendor_name"),
|
||||||
|
Index("idx_marketplace_brand", "marketplace", "brand"),
|
||||||
|
Index("idx_mp_gtin_marketplace", "gtin", "marketplace"),
|
||||||
|
Index("idx_mp_product_type", "product_type_enum", "is_digital"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"<MarketplaceProduct(id={self.id}, "
|
||||||
|
f"marketplace_product_id='{self.marketplace_product_id}', "
|
||||||
|
f"marketplace='{self.marketplace}', "
|
||||||
|
f"vendor='{self.vendor_name}')>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# === PRICE PROPERTIES (Euro convenience accessors) ===
|
||||||
|
|
||||||
|
@property
|
||||||
|
def price_numeric(self) -> float | None:
|
||||||
|
"""Get price in euros (for API/display). Legacy name for compatibility."""
|
||||||
|
if self.price_cents is not None:
|
||||||
|
return cents_to_euros(self.price_cents)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@price_numeric.setter
|
||||||
|
def price_numeric(self, value: float | None):
|
||||||
|
"""Set price from euros. Legacy name for compatibility."""
|
||||||
|
self.price_cents = euros_to_cents(value) if value is not None else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sale_price_numeric(self) -> float | None:
|
||||||
|
"""Get sale price in euros (for API/display). Legacy name for compatibility."""
|
||||||
|
if self.sale_price_cents is not None:
|
||||||
|
return cents_to_euros(self.sale_price_cents)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@sale_price_numeric.setter
|
||||||
|
def sale_price_numeric(self, value: float | None):
|
||||||
|
"""Set sale price from euros. Legacy name for compatibility."""
|
||||||
|
self.sale_price_cents = euros_to_cents(value) if value is not None else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def weight(self) -> float | None:
|
||||||
|
"""Get weight in kg (for API/display)."""
|
||||||
|
if self.weight_grams is not None:
|
||||||
|
return self.weight_grams / 1000.0
|
||||||
|
return None
|
||||||
|
|
||||||
|
@weight.setter
|
||||||
|
def weight(self, value: float | None):
|
||||||
|
"""Set weight from kg."""
|
||||||
|
self.weight_grams = int(value * 1000) if value is not None else None
|
||||||
|
|
||||||
|
# === HELPER PROPERTIES ===
|
||||||
|
|
||||||
|
@property
|
||||||
|
def product_type(self) -> ProductType:
|
||||||
|
"""Get product type as enum."""
|
||||||
|
return ProductType(self.product_type_enum)
|
||||||
|
|
||||||
|
@product_type.setter
|
||||||
|
def product_type(self, value: ProductType | str):
|
||||||
|
"""Set product type from enum or string."""
|
||||||
|
if isinstance(value, ProductType):
|
||||||
|
self.product_type_enum = value.value
|
||||||
|
else:
|
||||||
|
self.product_type_enum = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def delivery_method(self) -> DigitalDeliveryMethod | None:
|
||||||
|
"""Get digital delivery method as enum."""
|
||||||
|
if self.digital_delivery_method:
|
||||||
|
return DigitalDeliveryMethod(self.digital_delivery_method)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@delivery_method.setter
|
||||||
|
def delivery_method(self, value: DigitalDeliveryMethod | str | None):
|
||||||
|
"""Set delivery method from enum or string."""
|
||||||
|
if value is None:
|
||||||
|
self.digital_delivery_method = None
|
||||||
|
elif isinstance(value, DigitalDeliveryMethod):
|
||||||
|
self.digital_delivery_method = value.value
|
||||||
|
else:
|
||||||
|
self.digital_delivery_method = value
|
||||||
|
|
||||||
|
def get_translation(self, language: str) -> "MarketplaceProductTranslation | 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 with fallback to 'en'."""
|
||||||
|
translation = self.get_translation(language)
|
||||||
|
if translation:
|
||||||
|
return translation.title
|
||||||
|
|
||||||
|
# Fallback to English
|
||||||
|
if language != "en":
|
||||||
|
en_translation = self.get_translation("en")
|
||||||
|
if en_translation:
|
||||||
|
return en_translation.title
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_description(self, language: str = "en") -> str | None:
|
||||||
|
"""Get description for a specific language with fallback to 'en'."""
|
||||||
|
translation = self.get_translation(language)
|
||||||
|
if translation:
|
||||||
|
return translation.description
|
||||||
|
|
||||||
|
# Fallback to English
|
||||||
|
if language != "en":
|
||||||
|
en_translation = self.get_translation("en")
|
||||||
|
if en_translation:
|
||||||
|
return en_translation.description
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effective_price(self) -> float | None:
|
||||||
|
"""Get the effective numeric price in euros."""
|
||||||
|
return self.price_numeric
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effective_sale_price(self) -> float | None:
|
||||||
|
"""Get the effective numeric sale price in euros."""
|
||||||
|
return self.sale_price_numeric
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_images(self) -> list[str]:
|
||||||
|
"""Get all product images as a list."""
|
||||||
|
images = []
|
||||||
|
if self.image_link:
|
||||||
|
images.append(self.image_link)
|
||||||
|
if self.additional_images:
|
||||||
|
images.extend(self.additional_images)
|
||||||
|
elif self.additional_image_link:
|
||||||
|
# Legacy single string format
|
||||||
|
images.append(self.additional_image_link)
|
||||||
|
return images
|
||||||
|
|
||||||
|
|
||||||
|
# Import MarketplaceProductTranslation for type hints
|
||||||
|
from app.modules.marketplace.models.marketplace_product_translation import (
|
||||||
|
MarketplaceProductTranslation,
|
||||||
|
)
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# app/modules/marketplace/models/marketplace_product_translation.py
|
||||||
|
"""Marketplace Product Translation model for multi-language support.
|
||||||
|
|
||||||
|
This model stores localized content (title, description, SEO fields) for
|
||||||
|
marketplace products. Each marketplace product can have multiple translations
|
||||||
|
for different languages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 MarketplaceProductTranslation(Base, TimestampMixin):
|
||||||
|
"""Localized content for marketplace products.
|
||||||
|
|
||||||
|
Stores translations for product titles, descriptions, and SEO fields.
|
||||||
|
Each marketplace_product can have one translation per language.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "marketplace_product_translations"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
marketplace_product_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("marketplace_products.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
language = Column(String(5), nullable=False) # 'en', 'fr', 'de', 'lb'
|
||||||
|
|
||||||
|
# === LOCALIZED CONTENT ===
|
||||||
|
title = Column(String, nullable=False)
|
||||||
|
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))
|
||||||
|
|
||||||
|
# === SOURCE TRACKING ===
|
||||||
|
source_import_id = Column(Integer) # Which import job provided this
|
||||||
|
source_file = Column(String) # e.g., "letzshop_fr.csv"
|
||||||
|
|
||||||
|
# === RELATIONSHIPS ===
|
||||||
|
marketplace_product = relationship(
|
||||||
|
"MarketplaceProduct",
|
||||||
|
back_populates="translations",
|
||||||
|
)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"marketplace_product_id",
|
||||||
|
"language",
|
||||||
|
name="uq_marketplace_product_translation",
|
||||||
|
),
|
||||||
|
Index("idx_mpt_marketplace_product_id", "marketplace_product_id"),
|
||||||
|
Index("idx_mpt_language", "language"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"<MarketplaceProductTranslation(id={self.id}, "
|
||||||
|
f"marketplace_product_id={self.marketplace_product_id}, "
|
||||||
|
f"language='{self.language}', "
|
||||||
|
f"title='{self.title[:30] if self.title else None}...')>"
|
||||||
|
)
|
||||||
@@ -2,33 +2,36 @@
|
|||||||
"""
|
"""
|
||||||
Marketplace module route registration.
|
Marketplace module route registration.
|
||||||
|
|
||||||
This module provides functions to register marketplace routes
|
This module provides marketplace routes with module-based access control.
|
||||||
with module-based access control.
|
|
||||||
|
|
||||||
NOTE: Routers are NOT auto-imported to avoid circular dependencies.
|
Structure:
|
||||||
Import directly from admin.py or vendor.py as needed:
|
- routes/api/ - REST API endpoints
|
||||||
from app.modules.marketplace.routes.admin import admin_router, admin_letzshop_router
|
- routes/pages/ - HTML page rendering (templates)
|
||||||
from app.modules.marketplace.routes.vendor import vendor_router, vendor_letzshop_router
|
|
||||||
|
NOTE: Routers are not eagerly imported here to avoid circular imports.
|
||||||
|
Import directly from routes/api/admin.py or routes/api/vendor.py instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Routers are imported on-demand to avoid circular dependencies
|
|
||||||
# Do NOT add auto-imports here
|
|
||||||
|
|
||||||
__all__ = ["admin_router", "admin_letzshop_router", "vendor_router", "vendor_letzshop_router"]
|
|
||||||
|
|
||||||
|
|
||||||
def __getattr__(name: str):
|
def __getattr__(name: str):
|
||||||
"""Lazy import routers to avoid circular dependencies."""
|
"""Lazy import of routers to avoid circular imports."""
|
||||||
if name == "admin_router":
|
if name == "admin_router":
|
||||||
from app.modules.marketplace.routes.admin import admin_router
|
from app.modules.marketplace.routes.api.admin import admin_router
|
||||||
|
|
||||||
return admin_router
|
return admin_router
|
||||||
elif name == "admin_letzshop_router":
|
elif name == "admin_letzshop_router":
|
||||||
from app.modules.marketplace.routes.admin import admin_letzshop_router
|
from app.modules.marketplace.routes.api.admin import admin_letzshop_router
|
||||||
|
|
||||||
return admin_letzshop_router
|
return admin_letzshop_router
|
||||||
elif name == "vendor_router":
|
elif name == "vendor_router":
|
||||||
from app.modules.marketplace.routes.vendor import vendor_router
|
from app.modules.marketplace.routes.api.vendor import vendor_router
|
||||||
|
|
||||||
return vendor_router
|
return vendor_router
|
||||||
elif name == "vendor_letzshop_router":
|
elif name == "vendor_letzshop_router":
|
||||||
from app.modules.marketplace.routes.vendor import vendor_letzshop_router
|
from app.modules.marketplace.routes.api.vendor import vendor_letzshop_router
|
||||||
|
|
||||||
return vendor_letzshop_router
|
return vendor_letzshop_router
|
||||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["admin_router", "admin_letzshop_router", "vendor_router", "vendor_letzshop_router"]
|
||||||
|
|||||||
35
app/modules/marketplace/routes/api/__init__.py
Normal file
35
app/modules/marketplace/routes/api/__init__.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# app/modules/marketplace/routes/api/__init__.py
|
||||||
|
"""
|
||||||
|
Marketplace module API routes.
|
||||||
|
|
||||||
|
Provides REST API endpoints for marketplace integration:
|
||||||
|
- Admin API: Import jobs, vendor directory, marketplace products
|
||||||
|
- Vendor API: Letzshop sync, product imports, exports
|
||||||
|
|
||||||
|
NOTE: Routers are not eagerly imported here to avoid circular imports.
|
||||||
|
Import directly from admin.py or vendor.py instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str):
|
||||||
|
"""Lazy import of routers to avoid circular imports."""
|
||||||
|
if name == "admin_router":
|
||||||
|
from app.modules.marketplace.routes.api.admin import admin_router
|
||||||
|
|
||||||
|
return admin_router
|
||||||
|
elif name == "admin_letzshop_router":
|
||||||
|
from app.modules.marketplace.routes.api.admin import admin_letzshop_router
|
||||||
|
|
||||||
|
return admin_letzshop_router
|
||||||
|
elif name == "vendor_router":
|
||||||
|
from app.modules.marketplace.routes.api.vendor import vendor_router
|
||||||
|
|
||||||
|
return vendor_router
|
||||||
|
elif name == "vendor_letzshop_router":
|
||||||
|
from app.modules.marketplace.routes.api.vendor import vendor_letzshop_router
|
||||||
|
|
||||||
|
return vendor_letzshop_router
|
||||||
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["admin_router", "admin_letzshop_router", "vendor_router", "vendor_letzshop_router"]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# app/modules/marketplace/routes/admin.py
|
# app/modules/marketplace/routes/api/admin.py
|
||||||
"""
|
"""
|
||||||
Marketplace module admin routes.
|
Marketplace module admin routes.
|
||||||
|
|
||||||
@@ -11,13 +11,18 @@ Includes:
|
|||||||
- /letzshop/* - Letzshop integration
|
- /letzshop/* - Letzshop integration
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from app.api.deps import require_module_access
|
from app.api.deps import require_module_access
|
||||||
|
|
||||||
# Import original routers (direct import to avoid circular dependency)
|
# Import original routers using importlib to avoid circular imports
|
||||||
from app.api.v1.admin.marketplace import router as marketplace_original_router
|
# (direct import triggers app.api.v1.admin.__init__.py which imports us)
|
||||||
from app.api.v1.admin.letzshop import router as letzshop_original_router
|
_marketplace_module = importlib.import_module("app.api.v1.admin.marketplace")
|
||||||
|
_letzshop_module = importlib.import_module("app.api.v1.admin.letzshop")
|
||||||
|
marketplace_original_router = _marketplace_module.router
|
||||||
|
letzshop_original_router = _letzshop_module.router
|
||||||
|
|
||||||
# Create module-aware router for marketplace
|
# Create module-aware router for marketplace
|
||||||
admin_router = APIRouter(
|
admin_router = APIRouter(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# app/modules/marketplace/routes/vendor.py
|
# app/modules/marketplace/routes/api/vendor.py
|
||||||
"""
|
"""
|
||||||
Marketplace module vendor routes.
|
Marketplace module vendor routes.
|
||||||
|
|
||||||
@@ -11,13 +11,18 @@ Includes:
|
|||||||
- /letzshop/* - Letzshop integration
|
- /letzshop/* - Letzshop integration
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from app.api.deps import require_module_access
|
from app.api.deps import require_module_access
|
||||||
|
|
||||||
# Import original routers (direct import to avoid circular dependency)
|
# Import original routers using importlib to avoid circular imports
|
||||||
from app.api.v1.vendor.marketplace import router as marketplace_original_router
|
# (direct import triggers app.api.v1.vendor.__init__.py which imports us)
|
||||||
from app.api.v1.vendor.letzshop import router as letzshop_original_router
|
_marketplace_module = importlib.import_module("app.api.v1.vendor.marketplace")
|
||||||
|
_letzshop_module = importlib.import_module("app.api.v1.vendor.letzshop")
|
||||||
|
marketplace_original_router = _marketplace_module.router
|
||||||
|
letzshop_original_router = _letzshop_module.router
|
||||||
|
|
||||||
# Create module-aware router for marketplace
|
# Create module-aware router for marketplace
|
||||||
vendor_router = APIRouter(
|
vendor_router = APIRouter(
|
||||||
9
app/modules/marketplace/routes/pages/__init__.py
Normal file
9
app/modules/marketplace/routes/pages/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# app/modules/marketplace/routes/pages/__init__.py
|
||||||
|
"""
|
||||||
|
Marketplace module page routes (HTML rendering).
|
||||||
|
|
||||||
|
Provides Jinja2 template rendering for marketplace management.
|
||||||
|
Note: Page routes can be added here as needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__all__ = []
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
# app/modules/marketplace/schemas/__init__.py
|
# app/modules/marketplace/schemas/__init__.py
|
||||||
"""
|
"""
|
||||||
Marketplace module Pydantic schemas.
|
Marketplace module Pydantic schemas for API request/response validation.
|
||||||
|
|
||||||
Re-exports marketplace schemas from the central schemas location.
|
This is the canonical location for marketplace schemas.
|
||||||
Provides a module-local import path while maintaining backwards compatibility.
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from app.modules.marketplace.schemas import (
|
from app.modules.marketplace.schemas import (
|
||||||
@@ -13,13 +12,18 @@ Usage:
|
|||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from models.schema.marketplace_import_job import (
|
from app.modules.marketplace.schemas.marketplace_import_job import (
|
||||||
MarketplaceImportJobRequest,
|
MarketplaceImportJobRequest,
|
||||||
AdminMarketplaceImportJobRequest,
|
AdminMarketplaceImportJobRequest,
|
||||||
MarketplaceImportJobResponse,
|
MarketplaceImportJobResponse,
|
||||||
MarketplaceImportJobListResponse,
|
MarketplaceImportJobListResponse,
|
||||||
|
MarketplaceImportErrorResponse,
|
||||||
|
MarketplaceImportErrorListResponse,
|
||||||
|
AdminMarketplaceImportJobResponse,
|
||||||
|
AdminMarketplaceImportJobListResponse,
|
||||||
|
MarketplaceImportJobStatusUpdate,
|
||||||
)
|
)
|
||||||
from models.schema.marketplace_product import (
|
from app.modules.marketplace.schemas.marketplace_product import (
|
||||||
# Translation schemas
|
# Translation schemas
|
||||||
MarketplaceProductTranslationSchema,
|
MarketplaceProductTranslationSchema,
|
||||||
# Base schemas
|
# Base schemas
|
||||||
@@ -42,6 +46,11 @@ __all__ = [
|
|||||||
"AdminMarketplaceImportJobRequest",
|
"AdminMarketplaceImportJobRequest",
|
||||||
"MarketplaceImportJobResponse",
|
"MarketplaceImportJobResponse",
|
||||||
"MarketplaceImportJobListResponse",
|
"MarketplaceImportJobListResponse",
|
||||||
|
"MarketplaceImportErrorResponse",
|
||||||
|
"MarketplaceImportErrorListResponse",
|
||||||
|
"AdminMarketplaceImportJobResponse",
|
||||||
|
"AdminMarketplaceImportJobListResponse",
|
||||||
|
"MarketplaceImportJobStatusUpdate",
|
||||||
# Product schemas
|
# Product schemas
|
||||||
"MarketplaceProductTranslationSchema",
|
"MarketplaceProductTranslationSchema",
|
||||||
"MarketplaceProductBase",
|
"MarketplaceProductBase",
|
||||||
|
|||||||
170
app/modules/marketplace/schemas/marketplace_import_job.py
Normal file
170
app/modules/marketplace/schemas/marketplace_import_job.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# app/modules/marketplace/schemas/marketplace_import_job.py
|
||||||
|
"""Pydantic schemas for marketplace import jobs."""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceImportJobRequest(BaseModel):
|
||||||
|
"""Request schema for triggering marketplace import.
|
||||||
|
|
||||||
|
Note: vendor_id is injected by middleware, not from request body.
|
||||||
|
"""
|
||||||
|
|
||||||
|
source_url: str = Field(..., description="URL to CSV file from marketplace")
|
||||||
|
marketplace: str = Field(default="Letzshop", description="Marketplace name")
|
||||||
|
batch_size: int | None = Field(
|
||||||
|
1000, description="Processing batch size", ge=100, le=10000
|
||||||
|
)
|
||||||
|
language: str = Field(
|
||||||
|
default="en",
|
||||||
|
description="Language code for product translations (e.g., 'en', 'fr', 'de')",
|
||||||
|
)
|
||||||
|
|
||||||
|
@field_validator("source_url")
|
||||||
|
@classmethod
|
||||||
|
def validate_url(cls, v):
|
||||||
|
if not v.startswith(("http://", "https://")): # noqa: SEC-034
|
||||||
|
raise ValueError("URL must start with http:// or https://") # noqa: SEC-034
|
||||||
|
return v.strip()
|
||||||
|
|
||||||
|
@field_validator("marketplace")
|
||||||
|
@classmethod
|
||||||
|
def validate_marketplace(cls, v):
|
||||||
|
return v.strip()
|
||||||
|
|
||||||
|
@field_validator("language")
|
||||||
|
@classmethod
|
||||||
|
def validate_language(cls, v):
|
||||||
|
# Basic language code validation (2-5 chars)
|
||||||
|
v = v.strip().lower()
|
||||||
|
if not 2 <= len(v) <= 5:
|
||||||
|
raise ValueError("Language code must be 2-5 characters (e.g., 'en', 'fr')")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class AdminMarketplaceImportJobRequest(BaseModel):
|
||||||
|
"""Request schema for admin-triggered marketplace import.
|
||||||
|
|
||||||
|
Includes vendor_id since admin can import for any vendor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
vendor_id: int = Field(..., description="Vendor ID to import products for")
|
||||||
|
source_url: str = Field(..., description="URL to CSV file from marketplace")
|
||||||
|
marketplace: str = Field(default="Letzshop", description="Marketplace name")
|
||||||
|
batch_size: int | None = Field(
|
||||||
|
1000, description="Processing batch size", ge=100, le=10000
|
||||||
|
)
|
||||||
|
language: str = Field(
|
||||||
|
default="en",
|
||||||
|
description="Language code for product translations (e.g., 'en', 'fr', 'de')",
|
||||||
|
)
|
||||||
|
|
||||||
|
@field_validator("source_url")
|
||||||
|
@classmethod
|
||||||
|
def validate_url(cls, v):
|
||||||
|
if not v.startswith(("http://", "https://")): # noqa: SEC-034
|
||||||
|
raise ValueError("URL must start with http:// or https://") # noqa: SEC-034
|
||||||
|
return v.strip()
|
||||||
|
|
||||||
|
@field_validator("marketplace")
|
||||||
|
@classmethod
|
||||||
|
def validate_marketplace(cls, v):
|
||||||
|
return v.strip()
|
||||||
|
|
||||||
|
@field_validator("language")
|
||||||
|
@classmethod
|
||||||
|
def validate_language(cls, v):
|
||||||
|
v = v.strip().lower()
|
||||||
|
if not 2 <= len(v) <= 5:
|
||||||
|
raise ValueError("Language code must be 2-5 characters (e.g., 'en', 'fr')")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceImportErrorResponse(BaseModel):
|
||||||
|
"""Response schema for individual import error."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
row_number: int
|
||||||
|
identifier: str | None = None
|
||||||
|
error_type: str
|
||||||
|
error_message: str
|
||||||
|
row_data: dict | None = None
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceImportErrorListResponse(BaseModel):
|
||||||
|
"""Response schema for list of import errors."""
|
||||||
|
|
||||||
|
errors: list[MarketplaceImportErrorResponse]
|
||||||
|
total: int
|
||||||
|
import_job_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceImportJobResponse(BaseModel):
|
||||||
|
"""Response schema for marketplace import job."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
job_id: int
|
||||||
|
vendor_id: int
|
||||||
|
vendor_code: str | None = None # Populated from vendor relationship
|
||||||
|
vendor_name: str | None = None # Populated from vendor relationship
|
||||||
|
marketplace: str
|
||||||
|
source_url: str
|
||||||
|
status: str
|
||||||
|
language: str | None = None # Language used for translations
|
||||||
|
|
||||||
|
# Counts
|
||||||
|
imported: int = 0
|
||||||
|
updated: int = 0
|
||||||
|
total_processed: int = 0
|
||||||
|
error_count: int = 0
|
||||||
|
|
||||||
|
# Error details
|
||||||
|
error_message: str | None = None
|
||||||
|
|
||||||
|
# Timestamps
|
||||||
|
created_at: datetime
|
||||||
|
started_at: datetime | None = None
|
||||||
|
completed_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceImportJobListResponse(BaseModel):
|
||||||
|
"""Response schema for list of import jobs."""
|
||||||
|
|
||||||
|
jobs: list[MarketplaceImportJobResponse]
|
||||||
|
total: int
|
||||||
|
skip: int
|
||||||
|
limit: int
|
||||||
|
|
||||||
|
|
||||||
|
class AdminMarketplaceImportJobResponse(MarketplaceImportJobResponse):
|
||||||
|
"""Extended response schema for admin with additional fields."""
|
||||||
|
|
||||||
|
id: int # Alias for job_id (frontend compatibility)
|
||||||
|
error_details: list = [] # Placeholder for future error details
|
||||||
|
created_by_name: str | None = None # Username of who created the job
|
||||||
|
|
||||||
|
|
||||||
|
class AdminMarketplaceImportJobListResponse(BaseModel):
|
||||||
|
"""Response schema for paginated list of import jobs (admin)."""
|
||||||
|
|
||||||
|
items: list[AdminMarketplaceImportJobResponse]
|
||||||
|
total: int
|
||||||
|
page: int
|
||||||
|
limit: int
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceImportJobStatusUpdate(BaseModel):
|
||||||
|
"""Schema for updating import job status (internal use)."""
|
||||||
|
|
||||||
|
status: str
|
||||||
|
imported_count: int | None = None
|
||||||
|
updated_count: int | None = None
|
||||||
|
error_count: int | None = None
|
||||||
|
total_processed: int | None = None
|
||||||
|
error_message: str | None = None
|
||||||
225
app/modules/marketplace/schemas/marketplace_product.py
Normal file
225
app/modules/marketplace/schemas/marketplace_product.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
# app/modules/marketplace/schemas/marketplace_product.py
|
||||||
|
"""Pydantic schemas for MarketplaceProduct API validation.
|
||||||
|
|
||||||
|
Note: title and description are stored in MarketplaceProductTranslation table,
|
||||||
|
but we keep them in the API schemas for convenience. The service layer
|
||||||
|
handles creating/updating translations separately.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
from models.schema.inventory import ProductInventorySummary
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceProductTranslationSchema(BaseModel):
|
||||||
|
"""Schema for product translation."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
language: str
|
||||||
|
title: str
|
||||||
|
description: str | None = None
|
||||||
|
short_description: str | None = None
|
||||||
|
meta_title: str | None = None
|
||||||
|
meta_description: str | None = None
|
||||||
|
url_slug: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceProductBase(BaseModel):
|
||||||
|
"""Base schema for marketplace products."""
|
||||||
|
|
||||||
|
marketplace_product_id: str | None = None
|
||||||
|
|
||||||
|
# Localized fields (passed to translations)
|
||||||
|
title: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
# Links and media
|
||||||
|
link: str | None = None
|
||||||
|
image_link: str | None = None
|
||||||
|
additional_image_link: str | None = None
|
||||||
|
|
||||||
|
# Status
|
||||||
|
availability: str | None = None
|
||||||
|
is_active: bool | None = None
|
||||||
|
|
||||||
|
# Pricing
|
||||||
|
price: str | None = None
|
||||||
|
sale_price: str | None = None
|
||||||
|
currency: str | None = None
|
||||||
|
|
||||||
|
# Product identifiers
|
||||||
|
brand: str | None = None
|
||||||
|
gtin: str | None = None
|
||||||
|
mpn: str | None = None
|
||||||
|
sku: str | None = None
|
||||||
|
|
||||||
|
# Product attributes
|
||||||
|
condition: str | None = None
|
||||||
|
adult: str | None = None
|
||||||
|
multipack: int | None = None
|
||||||
|
is_bundle: str | None = None
|
||||||
|
age_group: str | None = None
|
||||||
|
color: str | None = None
|
||||||
|
gender: str | None = None
|
||||||
|
material: str | None = None
|
||||||
|
pattern: str | None = None
|
||||||
|
size: str | None = None
|
||||||
|
size_type: str | None = None
|
||||||
|
size_system: str | None = None
|
||||||
|
item_group_id: str | None = None
|
||||||
|
|
||||||
|
# Categories
|
||||||
|
google_product_category: str | None = None
|
||||||
|
product_type_raw: str | None = (
|
||||||
|
None # Original feed value (renamed from product_type)
|
||||||
|
)
|
||||||
|
category_path: str | None = None
|
||||||
|
|
||||||
|
# Custom labels
|
||||||
|
custom_label_0: str | None = None
|
||||||
|
custom_label_1: str | None = None
|
||||||
|
custom_label_2: str | None = None
|
||||||
|
custom_label_3: str | None = None
|
||||||
|
custom_label_4: str | None = None
|
||||||
|
|
||||||
|
# Unit pricing
|
||||||
|
unit_pricing_measure: str | None = None
|
||||||
|
unit_pricing_base_measure: str | None = None
|
||||||
|
identifier_exists: str | None = None
|
||||||
|
shipping: str | None = None
|
||||||
|
|
||||||
|
# Source tracking
|
||||||
|
marketplace: str | None = None
|
||||||
|
vendor_name: str | None = None
|
||||||
|
source_url: str | None = None
|
||||||
|
|
||||||
|
# Product type classification
|
||||||
|
product_type_enum: str | None = (
|
||||||
|
None # 'physical', 'digital', 'service', 'subscription'
|
||||||
|
)
|
||||||
|
is_digital: bool | None = None
|
||||||
|
|
||||||
|
# Digital product fields
|
||||||
|
digital_delivery_method: str | None = None
|
||||||
|
platform: str | None = None
|
||||||
|
license_type: str | None = None
|
||||||
|
|
||||||
|
# Physical product fields
|
||||||
|
weight: float | None = None
|
||||||
|
weight_unit: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceProductCreate(MarketplaceProductBase):
|
||||||
|
"""Schema for creating a marketplace product."""
|
||||||
|
|
||||||
|
marketplace_product_id: str = Field(
|
||||||
|
..., description="Unique product identifier from marketplace"
|
||||||
|
)
|
||||||
|
# Title is required for API creation (will be stored in translations)
|
||||||
|
title: str = Field(..., description="Product title")
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceProductUpdate(MarketplaceProductBase):
|
||||||
|
"""Schema for updating a marketplace product.
|
||||||
|
|
||||||
|
All fields are optional - only provided fields will be updated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceProductResponse(BaseModel):
|
||||||
|
"""Schema for marketplace product API response."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
marketplace_product_id: str
|
||||||
|
|
||||||
|
# These will be populated from translations
|
||||||
|
title: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
# Links and media
|
||||||
|
link: str | None = None
|
||||||
|
image_link: str | None = None
|
||||||
|
additional_image_link: str | None = None
|
||||||
|
|
||||||
|
# Status
|
||||||
|
availability: str | None = None
|
||||||
|
is_active: bool | None = None
|
||||||
|
|
||||||
|
# Pricing
|
||||||
|
price: str | None = None
|
||||||
|
price_numeric: float | None = None
|
||||||
|
sale_price: str | None = None
|
||||||
|
sale_price_numeric: float | None = None
|
||||||
|
currency: str | None = None
|
||||||
|
|
||||||
|
# Product identifiers
|
||||||
|
brand: str | None = None
|
||||||
|
gtin: str | None = None
|
||||||
|
mpn: str | None = None
|
||||||
|
sku: str | None = None
|
||||||
|
|
||||||
|
# Product attributes
|
||||||
|
condition: str | None = None
|
||||||
|
color: str | None = None
|
||||||
|
size: str | None = None
|
||||||
|
|
||||||
|
# Categories
|
||||||
|
google_product_category: str | None = None
|
||||||
|
product_type_raw: str | None = None
|
||||||
|
category_path: str | None = None
|
||||||
|
|
||||||
|
# Source tracking
|
||||||
|
marketplace: str | None = None
|
||||||
|
vendor_name: str | None = None
|
||||||
|
|
||||||
|
# Product type
|
||||||
|
product_type_enum: str | None = None
|
||||||
|
is_digital: bool | None = None
|
||||||
|
platform: str | None = None
|
||||||
|
|
||||||
|
# Timestamps
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
# Translations (optional - included when requested)
|
||||||
|
translations: list[MarketplaceProductTranslationSchema] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceProductListResponse(BaseModel):
|
||||||
|
"""Schema for paginated product list response."""
|
||||||
|
|
||||||
|
products: list[MarketplaceProductResponse]
|
||||||
|
total: int
|
||||||
|
skip: int
|
||||||
|
limit: int
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceProductDetailResponse(BaseModel):
|
||||||
|
"""Schema for detailed product response with inventory."""
|
||||||
|
|
||||||
|
product: MarketplaceProductResponse
|
||||||
|
inventory_info: ProductInventorySummary | None = None
|
||||||
|
translations: list[MarketplaceProductTranslationSchema] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceImportRequest(BaseModel):
|
||||||
|
"""Schema for marketplace import request."""
|
||||||
|
|
||||||
|
url: str = Field(..., description="URL to CSV file")
|
||||||
|
marketplace: str = Field(default="Letzshop", description="Marketplace name")
|
||||||
|
vendor_name: str | None = Field(default=None, description="Vendor name")
|
||||||
|
language: str = Field(default="en", description="Language code for translations")
|
||||||
|
batch_size: int = Field(default=100, ge=1, le=1000, description="Batch size")
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceImportResponse(BaseModel):
|
||||||
|
"""Schema for marketplace import response."""
|
||||||
|
|
||||||
|
job_id: int
|
||||||
|
status: str
|
||||||
|
message: str
|
||||||
Reference in New Issue
Block a user