Files
orion/models/schema/letzshop.py
Samir Boulahtit 45b09d6d90 feat: add unified admin Marketplace Letzshop page
- Add new Marketplace section in admin sidebar with Letzshop sub-item
- Remove old Import and Letzshop Orders items from Product Catalog
- Create unified Letzshop management page with 3 tabs:
  - Products tab: Import/Export functionality
  - Orders tab: Order management with confirm/reject/tracking
  - Settings tab: API credentials and CSV URLs
- Add unified jobs table showing imports, exports, and order syncs
- Implement vendor autocomplete using Tom Select library (CDN + fallback)
- Add /vendors/{vendor_id}/jobs API endpoint for unified job listing
- Move database queries to service layer (LetzshopOrderService)
- Add LetzshopJobItem and LetzshopJobsListResponse schemas
- Include Tom Select CSS/JS assets as local fallback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 18:27:04 +01:00

361 lines
9.8 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)"
)
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)
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
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
# ============================================================================
class LetzshopInventoryUnit(BaseModel):
"""Schema for Letzshop inventory unit."""
id: str
state: str
class LetzshopOrderBase(BaseModel):
"""Base schema for Letzshop order."""
letzshop_order_id: str
letzshop_shipment_id: str | None = None
letzshop_order_number: str | None = None
letzshop_state: str | None = None
customer_email: str | None = None
customer_name: str | None = None
total_amount: str | None = None
currency: str = "EUR"
class LetzshopOrderCreate(LetzshopOrderBase):
"""Schema for creating a Letzshop order record."""
vendor_id: int
raw_order_data: dict[str, Any] | None = None
inventory_units: list[dict[str, Any]] | None = None
class LetzshopOrderResponse(LetzshopOrderBase):
"""Schema for Letzshop order response."""
model_config = ConfigDict(from_attributes=True)
id: int
vendor_id: int
local_order_id: int | None
sync_status: str
last_synced_at: datetime | None
sync_error: str | None
confirmed_at: datetime | None
rejected_at: datetime | None
tracking_set_at: datetime | None
tracking_number: str | None
tracking_carrier: str | None
inventory_units: list[dict[str, Any]] | None
created_at: datetime
updated_at: datetime
class LetzshopOrderDetailResponse(LetzshopOrderResponse):
"""Schema for detailed Letzshop order response with raw data."""
raw_order_data: dict[str, Any] | None = None
class LetzshopOrderListResponse(BaseModel):
"""Schema for paginated Letzshop order list."""
orders: list[LetzshopOrderResponse]
total: int
skip: int
limit: int
# ============================================================================
# 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
letzshop_order_id: int
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, or order sync)."""
id: int
type: str = Field(..., description="Job type: import, export, or order_sync")
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
class LetzshopJobsListResponse(BaseModel):
"""Schema for paginated jobs list."""
jobs: list[LetzshopJobItem]
total: int