- 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>
627 lines
18 KiB
Python
627 lines
18 KiB
Python
# models/schema/letzshop.py
|
|
"""
|
|
Pydantic schemas for Letzshop marketplace integration.
|
|
|
|
Covers:
|
|
- Vendor credentials management
|
|
- Letzshop order import/sync
|
|
- Fulfillment queue operations
|
|
- Sync logs
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
# ============================================================================
|
|
# Credentials Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class LetzshopCredentialsCreate(BaseModel):
|
|
"""Schema for creating/updating Letzshop credentials."""
|
|
|
|
api_key: str = Field(..., min_length=1, description="Letzshop API key")
|
|
api_endpoint: str | None = Field(
|
|
None,
|
|
description="Custom API endpoint (defaults to https://letzshop.lu/graphql)",
|
|
)
|
|
auto_sync_enabled: bool = Field(False, description="Enable automatic order sync")
|
|
sync_interval_minutes: int = Field(
|
|
15, ge=5, le=1440, description="Sync interval in minutes (5-1440)"
|
|
)
|
|
test_mode_enabled: bool = Field(
|
|
False, description="Test mode - disables API mutations"
|
|
)
|
|
default_carrier: str | None = Field(
|
|
None, description="Default carrier (greco, colissimo, xpresslogistics)"
|
|
)
|
|
carrier_greco_label_url: str | None = Field(
|
|
"https://dispatchweb.fr/Tracky/Home/", description="Greco label URL prefix"
|
|
)
|
|
carrier_colissimo_label_url: str | None = Field(
|
|
None, description="Colissimo label URL prefix"
|
|
)
|
|
carrier_xpresslogistics_label_url: str | None = Field(
|
|
None, description="XpressLogistics label URL prefix"
|
|
)
|
|
|
|
|
|
class LetzshopCredentialsUpdate(BaseModel):
|
|
"""Schema for updating Letzshop credentials (partial update)."""
|
|
|
|
api_key: str | None = Field(None, min_length=1)
|
|
api_endpoint: str | None = None
|
|
auto_sync_enabled: bool | None = None
|
|
sync_interval_minutes: int | None = Field(None, ge=5, le=1440)
|
|
test_mode_enabled: bool | None = None
|
|
default_carrier: str | None = None
|
|
carrier_greco_label_url: str | None = None
|
|
carrier_colissimo_label_url: str | None = None
|
|
carrier_xpresslogistics_label_url: str | None = None
|
|
|
|
|
|
class LetzshopCredentialsResponse(BaseModel):
|
|
"""Schema for Letzshop credentials response (API key is masked)."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
vendor_id: int
|
|
api_key_masked: str = Field(..., description="Masked API key for display")
|
|
api_endpoint: str
|
|
auto_sync_enabled: bool
|
|
sync_interval_minutes: int
|
|
test_mode_enabled: bool = False
|
|
default_carrier: str | None = None
|
|
carrier_greco_label_url: str | None = None
|
|
carrier_colissimo_label_url: str | None = None
|
|
carrier_xpresslogistics_label_url: str | None = None
|
|
last_sync_at: datetime | None
|
|
last_sync_status: str | None
|
|
last_sync_error: str | None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class LetzshopCredentialsStatus(BaseModel):
|
|
"""Schema for Letzshop connection status."""
|
|
|
|
is_configured: bool
|
|
is_connected: bool
|
|
last_sync_at: datetime | None
|
|
last_sync_status: str | None
|
|
auto_sync_enabled: bool
|
|
|
|
|
|
# ============================================================================
|
|
# Letzshop Order Schemas (using unified Order model)
|
|
# ============================================================================
|
|
|
|
|
|
class LetzshopOrderItemResponse(BaseModel):
|
|
"""Schema for order item in Letzshop order response."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
product_id: int
|
|
product_name: str
|
|
product_sku: str | None = None
|
|
gtin: str | None = None
|
|
gtin_type: str | None = None
|
|
quantity: int
|
|
unit_price: float
|
|
total_price: float
|
|
external_item_id: str | None = None # Letzshop inventory unit ID
|
|
external_variant_id: str | None = None
|
|
item_state: str | None = None # confirmed_available, confirmed_unavailable
|
|
|
|
|
|
class LetzshopOrderResponse(BaseModel):
|
|
"""Schema for Letzshop order response (from unified Order model)."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
vendor_id: int
|
|
vendor_name: str | None = None # For cross-vendor views
|
|
order_number: str
|
|
|
|
# External references
|
|
external_order_id: str | None = None
|
|
external_shipment_id: str | None = None
|
|
external_order_number: str | None = None
|
|
|
|
# Status
|
|
status: str # pending, processing, shipped, delivered, cancelled
|
|
|
|
# Customer info
|
|
customer_email: str
|
|
customer_name: str # computed: customer_first_name + customer_last_name
|
|
customer_locale: str | None = None
|
|
|
|
# Address info
|
|
ship_country_iso: str
|
|
bill_country_iso: str
|
|
|
|
# Financial
|
|
total_amount: float
|
|
currency: str = "EUR"
|
|
|
|
# Tracking
|
|
tracking_number: str | None = None
|
|
tracking_provider: str | None = None
|
|
|
|
# Timestamps
|
|
order_date: datetime
|
|
confirmed_at: datetime | None = None
|
|
shipped_at: datetime | None = None
|
|
cancelled_at: datetime | None = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
# Items (for list view, may be empty)
|
|
items: list[LetzshopOrderItemResponse] = Field(default_factory=list)
|
|
|
|
|
|
class LetzshopOrderDetailResponse(LetzshopOrderResponse):
|
|
"""Schema for detailed Letzshop order response with all data."""
|
|
|
|
# Full customer snapshot
|
|
customer_first_name: str
|
|
customer_last_name: str
|
|
customer_phone: str | None = None
|
|
|
|
# Full shipping address
|
|
ship_first_name: str
|
|
ship_last_name: str
|
|
ship_company: str | None = None
|
|
ship_address_line_1: str
|
|
ship_address_line_2: str | None = None
|
|
ship_city: str
|
|
ship_postal_code: str
|
|
|
|
# Full billing address
|
|
bill_first_name: str
|
|
bill_last_name: str
|
|
bill_company: str | None = None
|
|
bill_address_line_1: str
|
|
bill_address_line_2: str | None = None
|
|
bill_city: str
|
|
bill_postal_code: str
|
|
|
|
# Raw marketplace data
|
|
external_data: dict[str, Any] | None = None
|
|
|
|
# Notes
|
|
customer_notes: str | None = None
|
|
internal_notes: str | None = None
|
|
|
|
|
|
class LetzshopOrderStats(BaseModel):
|
|
"""Schema for order statistics by status."""
|
|
|
|
pending: int = 0
|
|
processing: int = 0
|
|
shipped: int = 0
|
|
delivered: int = 0
|
|
cancelled: int = 0
|
|
total: int = 0
|
|
has_declined_items: int = 0 # Orders with at least one declined item
|
|
|
|
|
|
class LetzshopOrderListResponse(BaseModel):
|
|
"""Schema for paginated Letzshop order list."""
|
|
|
|
orders: list[LetzshopOrderResponse]
|
|
total: int
|
|
skip: int
|
|
limit: int
|
|
stats: LetzshopOrderStats | None = None
|
|
|
|
|
|
# ============================================================================
|
|
# Fulfillment Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class FulfillmentConfirmRequest(BaseModel):
|
|
"""Schema for confirming order fulfillment."""
|
|
|
|
inventory_unit_ids: list[str] = Field(
|
|
..., min_length=1, description="List of inventory unit IDs to confirm"
|
|
)
|
|
|
|
|
|
class FulfillmentRejectRequest(BaseModel):
|
|
"""Schema for rejecting order fulfillment."""
|
|
|
|
inventory_unit_ids: list[str] = Field(
|
|
..., min_length=1, description="List of inventory unit IDs to reject"
|
|
)
|
|
reason: str | None = Field(None, max_length=500, description="Rejection reason")
|
|
|
|
|
|
class FulfillmentTrackingRequest(BaseModel):
|
|
"""Schema for setting tracking information."""
|
|
|
|
tracking_number: str = Field(..., min_length=1, max_length=100)
|
|
tracking_carrier: str = Field(
|
|
..., min_length=1, max_length=100, description="Carrier code (e.g., dhl, ups)"
|
|
)
|
|
|
|
|
|
class FulfillmentQueueItemResponse(BaseModel):
|
|
"""Schema for fulfillment queue item response."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
vendor_id: int
|
|
order_id: int # FK to unified orders table
|
|
operation: str
|
|
payload: dict[str, Any]
|
|
status: str
|
|
attempts: int
|
|
max_attempts: int
|
|
last_attempt_at: datetime | None
|
|
next_retry_at: datetime | None
|
|
error_message: str | None
|
|
completed_at: datetime | None
|
|
response_data: dict[str, Any] | None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class FulfillmentQueueListResponse(BaseModel):
|
|
"""Schema for paginated fulfillment queue list."""
|
|
|
|
items: list[FulfillmentQueueItemResponse]
|
|
total: int
|
|
skip: int
|
|
limit: int
|
|
|
|
|
|
# ============================================================================
|
|
# Sync Log Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class LetzshopSyncLogResponse(BaseModel):
|
|
"""Schema for Letzshop sync log response."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
vendor_id: int
|
|
operation_type: str
|
|
direction: str
|
|
status: str
|
|
records_processed: int
|
|
records_succeeded: int
|
|
records_failed: int
|
|
error_details: dict[str, Any] | None
|
|
started_at: datetime
|
|
completed_at: datetime | None
|
|
duration_seconds: int | None
|
|
triggered_by: str | None
|
|
created_at: datetime
|
|
|
|
|
|
class LetzshopSyncLogListResponse(BaseModel):
|
|
"""Schema for paginated sync log list."""
|
|
|
|
logs: list[LetzshopSyncLogResponse]
|
|
total: int
|
|
skip: int
|
|
limit: int
|
|
|
|
|
|
# ============================================================================
|
|
# Sync Trigger Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class LetzshopSyncTriggerRequest(BaseModel):
|
|
"""Schema for triggering a sync operation."""
|
|
|
|
operation: str = Field(
|
|
"order_import",
|
|
pattern="^(order_import|full_sync)$",
|
|
description="Type of sync operation",
|
|
)
|
|
|
|
|
|
class LetzshopSyncTriggerResponse(BaseModel):
|
|
"""Schema for sync trigger response."""
|
|
|
|
success: bool
|
|
message: str
|
|
sync_log_id: int | None = None
|
|
orders_imported: int = 0
|
|
orders_updated: int = 0
|
|
errors: list[str] = Field(default_factory=list)
|
|
|
|
|
|
# ============================================================================
|
|
# Connection Test Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class LetzshopConnectionTestRequest(BaseModel):
|
|
"""Schema for testing Letzshop connection."""
|
|
|
|
api_key: str = Field(..., min_length=1, description="API key to test")
|
|
api_endpoint: str | None = Field(None, description="Custom endpoint to test")
|
|
|
|
|
|
class LetzshopConnectionTestResponse(BaseModel):
|
|
"""Schema for connection test response."""
|
|
|
|
success: bool
|
|
message: str
|
|
response_time_ms: float | None = None
|
|
error_details: str | None = None
|
|
|
|
|
|
# ============================================================================
|
|
# Generic Response Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class LetzshopSuccessResponse(BaseModel):
|
|
"""Generic success response for Letzshop operations."""
|
|
|
|
success: bool
|
|
message: str
|
|
|
|
|
|
class FulfillmentOperationResponse(BaseModel):
|
|
"""Response for fulfillment operations (confirm, reject, tracking)."""
|
|
|
|
success: bool
|
|
message: str
|
|
confirmed_units: list[str] | None = None
|
|
tracking_number: str | None = None
|
|
tracking_carrier: str | None = None
|
|
errors: list[str] | None = None
|
|
|
|
|
|
# ============================================================================
|
|
# Admin Overview Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class LetzshopVendorOverview(BaseModel):
|
|
"""Schema for vendor Letzshop integration overview (admin view)."""
|
|
|
|
vendor_id: int
|
|
vendor_name: str
|
|
vendor_code: str
|
|
is_configured: bool
|
|
auto_sync_enabled: bool
|
|
last_sync_at: datetime | None
|
|
last_sync_status: str | None
|
|
pending_orders: int
|
|
total_orders: int
|
|
|
|
|
|
class LetzshopVendorListResponse(BaseModel):
|
|
"""Schema for paginated vendor Letzshop overview list."""
|
|
|
|
vendors: list[LetzshopVendorOverview]
|
|
total: int
|
|
skip: int
|
|
limit: int
|
|
|
|
|
|
# ============================================================================
|
|
# Jobs Schemas (Unified view of imports, exports, and syncs)
|
|
# ============================================================================
|
|
|
|
|
|
class LetzshopJobItem(BaseModel):
|
|
"""Schema for a unified job item (import, export, order sync, or historical import)."""
|
|
|
|
id: int
|
|
type: str = Field(
|
|
..., description="Job type: import, export, order_sync, or historical_import"
|
|
)
|
|
status: str = Field(..., description="Job status")
|
|
created_at: datetime
|
|
started_at: datetime | None = None
|
|
completed_at: datetime | None = None
|
|
records_processed: int = 0
|
|
records_succeeded: int = 0
|
|
records_failed: int = 0
|
|
# Vendor info
|
|
vendor_id: int | None = Field(None, description="Vendor ID")
|
|
vendor_name: str | None = Field(None, description="Vendor name")
|
|
vendor_code: str | None = Field(None, description="Vendor code")
|
|
# Historical import specific fields
|
|
current_phase: str | None = Field(
|
|
None, description="Current phase for historical imports"
|
|
)
|
|
error_message: str | None = Field(None, description="Error message if failed")
|
|
error_details: dict[str, Any] | None = Field(
|
|
None, description="Error details or export file info"
|
|
)
|
|
|
|
|
|
class LetzshopJobsListResponse(BaseModel):
|
|
"""Schema for paginated jobs list."""
|
|
|
|
jobs: list[LetzshopJobItem]
|
|
total: int
|
|
|
|
|
|
# ============================================================================
|
|
# Historical Import Job Schemas
|
|
# ============================================================================
|
|
|
|
|
|
class LetzshopHistoricalImportJobResponse(BaseModel):
|
|
"""Schema for historical import job status (polling endpoint)."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
vendor_id: int
|
|
status: str # pending, fetching, processing, completed, failed
|
|
current_phase: str | None = None # "confirmed" or "declined"
|
|
|
|
# Fetch progress
|
|
current_page: int = 0
|
|
total_pages: int | None = None
|
|
shipments_fetched: int = 0
|
|
|
|
# Processing progress
|
|
orders_processed: int = 0
|
|
orders_imported: int = 0
|
|
orders_updated: int = 0
|
|
orders_skipped: int = 0
|
|
|
|
# EAN matching stats
|
|
products_matched: int = 0
|
|
products_not_found: int = 0
|
|
|
|
# Phase-specific stats (when complete)
|
|
confirmed_stats: dict[str, Any] | None = None
|
|
declined_stats: dict[str, Any] | None = None
|
|
|
|
# Error handling
|
|
error_message: str | None = None
|
|
|
|
# Timing
|
|
started_at: datetime | None = None
|
|
completed_at: datetime | None = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class LetzshopHistoricalImportStartResponse(BaseModel):
|
|
"""Schema for starting a historical import job."""
|
|
|
|
job_id: int
|
|
status: str = "pending"
|
|
message: str = "Historical import job started"
|
|
|
|
|
|
# ============================================================================
|
|
# Vendor Directory Schemas (Letzshop Marketplace Cache)
|
|
# ============================================================================
|
|
|
|
|
|
class LetzshopCachedVendorItem(BaseModel):
|
|
"""Schema for a cached Letzshop vendor in list view."""
|
|
|
|
id: int
|
|
letzshop_id: str
|
|
slug: str
|
|
name: str
|
|
company_name: str | None = None
|
|
email: str | None = None
|
|
phone: str | None = None
|
|
website: str | None = None
|
|
city: str | None = None
|
|
categories: list[str] = []
|
|
is_active: bool = True
|
|
is_claimed: bool = False
|
|
claimed_by_vendor_id: int | None = None
|
|
last_synced_at: datetime | None = None
|
|
letzshop_url: str
|
|
|
|
|
|
class LetzshopCachedVendorDetail(BaseModel):
|
|
"""Schema for detailed cached Letzshop vendor."""
|
|
|
|
id: int
|
|
letzshop_id: str
|
|
slug: str
|
|
name: str
|
|
company_name: str | None = None
|
|
description_en: str | None = None
|
|
description_fr: str | None = None
|
|
description_de: str | None = None
|
|
email: str | None = None
|
|
phone: str | None = None
|
|
fax: str | None = None
|
|
website: str | None = None
|
|
street: str | None = None
|
|
street_number: str | None = None
|
|
city: str | None = None
|
|
zipcode: str | None = None
|
|
country_iso: str | None = None
|
|
latitude: str | None = None
|
|
longitude: str | None = None
|
|
categories: list[str] = []
|
|
background_image_url: str | None = None
|
|
social_media_links: list[str] = []
|
|
opening_hours_en: str | None = None
|
|
opening_hours_fr: str | None = None
|
|
opening_hours_de: str | None = None
|
|
representative_name: str | None = None
|
|
representative_title: str | None = None
|
|
is_active: bool = True
|
|
is_claimed: bool = False
|
|
claimed_by_vendor_id: int | None = None
|
|
claimed_at: datetime | None = None
|
|
last_synced_at: datetime | None = None
|
|
letzshop_url: str
|
|
|
|
|
|
class LetzshopVendorDirectoryStats(BaseModel):
|
|
"""Schema for vendor directory cache statistics."""
|
|
|
|
total_vendors: int = 0
|
|
active_vendors: int = 0
|
|
claimed_vendors: int = 0
|
|
unclaimed_vendors: int = 0
|
|
unique_cities: int = 0
|
|
last_synced_at: str | None = None
|
|
|
|
|
|
class LetzshopVendorDirectoryStatsResponse(BaseModel):
|
|
"""Response schema for vendor directory stats endpoint."""
|
|
|
|
success: bool = True
|
|
stats: LetzshopVendorDirectoryStats
|
|
|
|
|
|
class LetzshopCachedVendorListResponse(BaseModel):
|
|
"""Response schema for vendor directory list endpoint."""
|
|
|
|
success: bool = True
|
|
vendors: list[LetzshopCachedVendorItem]
|
|
total: int
|
|
page: int
|
|
limit: int
|
|
has_more: bool
|
|
|
|
|
|
class LetzshopCachedVendorDetailResponse(BaseModel):
|
|
"""Response schema for vendor directory detail endpoint."""
|
|
|
|
success: bool = True
|
|
vendor: LetzshopCachedVendorDetail
|
|
|
|
|
|
class LetzshopVendorDirectorySyncResponse(BaseModel):
|
|
"""Response schema for vendor directory sync trigger."""
|
|
|
|
success: bool = True
|
|
message: str
|
|
task_id: str | None = None
|
|
mode: str = "celery"
|
|
|
|
|
|
class LetzshopCreateVendorFromCacheResponse(BaseModel):
|
|
"""Response schema for creating vendor from Letzshop cache."""
|
|
|
|
success: bool = True
|
|
message: str
|
|
vendor: dict[str, Any] | None = None
|
|
letzshop_vendor_slug: str
|