feat: add Letzshop vendor directory with sync and admin management

- Add LetzshopVendorCache model to store cached vendor data from Letzshop API
- Create LetzshopVendorSyncService for syncing vendor directory
- Add Celery task for background vendor sync
- Create admin page at /admin/letzshop/vendor-directory with:
  - Stats dashboard (total, claimed, unclaimed vendors)
  - Searchable/filterable vendor list
  - "Sync Now" button to trigger sync
  - Ability to create platform vendors from Letzshop cache
- Add API endpoints for vendor directory management
- Add Pydantic schemas for API responses

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-13 20:35:46 +01:00
parent 78b14a4b00
commit ccfbbcb804
13 changed files with 2571 additions and 46 deletions

View File

@@ -1,19 +1,22 @@
# app/tasks/celery_tasks/letzshop.py
"""
Celery tasks for Letzshop historical order imports.
Celery tasks for Letzshop integration.
Wraps the existing process_historical_import function for Celery execution.
Includes:
- Historical order imports
- Vendor directory sync
"""
import logging
from datetime import UTC, datetime
from typing import Callable
from typing import Any, Callable
from app.core.celery_config import celery_app
from app.services.admin_notification_service import admin_notification_service
from app.services.letzshop import LetzshopClientError
from app.services.letzshop.credentials_service import LetzshopCredentialsService
from app.services.letzshop.order_service import LetzshopOrderService
from app.services.letzshop.vendor_sync_service import LetzshopVendorSyncService
from app.tasks.celery_tasks.base import DatabaseTask
from models.database.letzshop import LetzshopHistoricalImportJob
@@ -270,3 +273,78 @@ def process_historical_import(self, job_id: int, vendor_id: int):
db.commit()
raise # Re-raise for Celery retry
# =============================================================================
# Vendor Directory Sync
# =============================================================================
@celery_app.task(
bind=True,
base=DatabaseTask,
name="app.tasks.celery_tasks.letzshop.sync_vendor_directory",
max_retries=2,
default_retry_delay=300,
autoretry_for=(Exception,),
retry_backoff=True,
)
def sync_vendor_directory(self) -> dict[str, Any]:
"""
Celery task to sync Letzshop vendor directory.
Fetches all vendors from Letzshop's public GraphQL API and updates
the local letzshop_vendor_cache table.
This task should be scheduled to run periodically (e.g., daily)
via Celery beat.
Returns:
dict: Sync statistics including created, updated, and error counts.
"""
with self.get_db() as db:
try:
logger.info("Starting Letzshop vendor directory sync...")
sync_service = LetzshopVendorSyncService(db)
def progress_callback(page: int, fetched: int, total: int):
"""Log progress during sync."""
logger.info(f"Vendor sync progress: page {page}, {fetched}/{total} vendors")
stats = sync_service.sync_all_vendors(progress_callback=progress_callback)
logger.info(
f"Vendor directory sync completed: "
f"{stats.get('created', 0)} created, "
f"{stats.get('updated', 0)} updated, "
f"{stats.get('errors', 0)} errors"
)
# Send admin notification if there were errors
if stats.get("errors", 0) > 0:
admin_notification_service.notify_system_info(
db=db,
title="Letzshop Vendor Sync Completed with Errors",
message=(
f"Synced {stats.get('total_fetched', 0)} vendors. "
f"Errors: {stats.get('errors', 0)}"
),
details=stats,
)
db.commit()
return stats
except Exception as e:
logger.error(f"Vendor directory sync failed: {e}", exc_info=True)
# Notify admins of failure
admin_notification_service.notify_critical_error(
db=db,
error_type="Vendor Directory Sync",
error_message=f"Failed to sync Letzshop vendor directory: {str(e)[:200]}",
details={"error": str(e)},
)
db.commit()
raise # Re-raise for Celery retry

View File

@@ -1,15 +1,16 @@
# app/tasks/letzshop_tasks.py
"""Background tasks for Letzshop historical order imports."""
"""Background tasks for Letzshop integration."""
import logging
from datetime import UTC, datetime
from typing import Callable
from typing import Any, Callable
from app.core.database import SessionLocal
from app.services.admin_notification_service import admin_notification_service
from app.services.letzshop import LetzshopClientError
from app.services.letzshop.credentials_service import LetzshopCredentialsService
from app.services.letzshop.order_service import LetzshopOrderService
from app.services.letzshop.vendor_sync_service import LetzshopVendorSyncService
from models.database.letzshop import LetzshopHistoricalImportJob
logger = logging.getLogger(__name__)
@@ -262,3 +263,80 @@ def process_historical_import(job_id: int, vendor_id: int):
db.close()
except Exception as close_error:
logger.error(f"Job {job_id}: Error closing database session: {close_error}")
# =============================================================================
# Vendor Directory Sync
# =============================================================================
def sync_letzshop_vendor_directory() -> dict[str, Any]:
"""
Sync Letzshop vendor directory to local cache.
This task fetches all vendors from Letzshop's public GraphQL API
and updates the local letzshop_vendor_cache table.
Should be run periodically (e.g., daily) via Celery beat.
Returns:
Dictionary with sync statistics.
"""
db = SessionLocal()
stats = {}
try:
logger.info("Starting Letzshop vendor directory sync task...")
sync_service = LetzshopVendorSyncService(db)
def progress_callback(page: int, fetched: int, total: int):
"""Log progress during sync."""
logger.info(f"Vendor sync progress: page {page}, {fetched}/{total} vendors")
stats = sync_service.sync_all_vendors(progress_callback=progress_callback)
logger.info(
f"Vendor directory sync completed: "
f"{stats.get('created', 0)} created, "
f"{stats.get('updated', 0)} updated, "
f"{stats.get('errors', 0)} errors"
)
# Send admin notification if there were errors
if stats.get("errors", 0) > 0:
admin_notification_service.notify_system_info(
db=db,
title="Letzshop Vendor Sync Completed with Errors",
message=(
f"Synced {stats.get('total_fetched', 0)} vendors. "
f"Errors: {stats.get('errors', 0)}"
),
details=stats,
)
return stats
except Exception as e:
logger.error(f"Vendor directory sync failed: {e}", exc_info=True)
# Notify admins of failure
try:
admin_notification_service.notify_critical_error(
db=db,
error_type="Vendor Directory Sync",
error_message=f"Failed to sync Letzshop vendor directory: {str(e)[:200]}",
details={"error": str(e)},
)
db.commit()
except Exception:
pass
raise
finally:
if hasattr(db, "close") and callable(db.close):
try:
db.close()
except Exception as close_error:
logger.error(f"Error closing database session: {close_error}")