# 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"