feat: add Letzshop bidirectional order integration
Add complete Letzshop marketplace integration with: - GraphQL client for order import and fulfillment operations - Encrypted credential storage per vendor (Fernet encryption) - Admin and vendor API endpoints for credentials management - Order import, confirmation, rejection, and tracking - Fulfillment queue and sync logging - Comprehensive documentation and test coverage New files: - app/services/letzshop/ - GraphQL client and services - app/utils/encryption.py - Fernet encryption utility - models/database/letzshop.py - Database models - models/schema/letzshop.py - Pydantic schemas - app/api/v1/admin/letzshop.py - Admin API endpoints - app/api/v1/vendor/letzshop.py - Vendor API endpoints - docs/guides/letzshop-order-integration.md - Documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,12 @@ from .marketplace_product import (
|
||||
)
|
||||
from .marketplace_product_translation import MarketplaceProductTranslation
|
||||
from .order import Order, OrderItem
|
||||
from .letzshop import (
|
||||
LetzshopFulfillmentQueue,
|
||||
LetzshopOrder,
|
||||
LetzshopSyncLog,
|
||||
VendorLetzshopCredentials,
|
||||
)
|
||||
from .product import Product
|
||||
from .product_translation import ProductTranslation
|
||||
from .user import User
|
||||
@@ -82,4 +88,9 @@ __all__ = [
|
||||
# Orders
|
||||
"Order",
|
||||
"OrderItem",
|
||||
# Letzshop Integration
|
||||
"VendorLetzshopCredentials",
|
||||
"LetzshopOrder",
|
||||
"LetzshopFulfillmentQueue",
|
||||
"LetzshopSyncLog",
|
||||
]
|
||||
|
||||
221
models/database/letzshop.py
Normal file
221
models/database/letzshop.py
Normal file
@@ -0,0 +1,221 @@
|
||||
# models/database/letzshop.py
|
||||
"""
|
||||
Database models for Letzshop marketplace integration.
|
||||
|
||||
Provides models for:
|
||||
- VendorLetzshopCredentials: Per-vendor API key storage (encrypted)
|
||||
- LetzshopOrder: External order tracking and mapping
|
||||
- LetzshopFulfillmentQueue: Outbound operation queue with retry
|
||||
- LetzshopSyncLog: Audit trail for sync operations
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
# 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 LetzshopOrder(Base, TimestampMixin):
|
||||
"""
|
||||
Letzshop order tracking and mapping.
|
||||
|
||||
Stores imported orders from Letzshop with mapping to local Order model.
|
||||
"""
|
||||
|
||||
__tablename__ = "letzshop_orders"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False, index=True)
|
||||
|
||||
# Letzshop identifiers
|
||||
letzshop_order_id = Column(String(100), nullable=False, index=True)
|
||||
letzshop_shipment_id = Column(String(100), nullable=True, index=True)
|
||||
letzshop_order_number = Column(String(100), nullable=True)
|
||||
|
||||
# Local order mapping (if imported to local system)
|
||||
local_order_id = Column(Integer, ForeignKey("orders.id"), nullable=True)
|
||||
|
||||
# Order state from Letzshop
|
||||
letzshop_state = Column(String(50), nullable=True) # unconfirmed, confirmed, etc.
|
||||
|
||||
# Customer info from Letzshop
|
||||
customer_email = Column(String(255), nullable=True)
|
||||
customer_name = Column(String(255), nullable=True)
|
||||
|
||||
# Order totals from Letzshop
|
||||
total_amount = Column(String(50), nullable=True) # Store as string to preserve format
|
||||
currency = Column(String(10), default="EUR")
|
||||
|
||||
# Raw data storage (for debugging/auditing)
|
||||
raw_order_data = Column(JSON, nullable=True)
|
||||
|
||||
# Inventory units (from Letzshop)
|
||||
inventory_units = Column(JSON, nullable=True) # List of inventory unit IDs
|
||||
|
||||
# Sync status
|
||||
sync_status = Column(
|
||||
String(50), default="pending"
|
||||
) # pending, imported, confirmed, rejected, shipped
|
||||
last_synced_at = Column(DateTime(timezone=True), nullable=True)
|
||||
sync_error = Column(Text, nullable=True)
|
||||
|
||||
# Fulfillment status
|
||||
confirmed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
rejected_at = Column(DateTime(timezone=True), nullable=True)
|
||||
tracking_set_at = Column(DateTime(timezone=True), nullable=True)
|
||||
tracking_number = Column(String(100), nullable=True)
|
||||
tracking_carrier = Column(String(100), nullable=True)
|
||||
|
||||
# Relationships
|
||||
vendor = relationship("Vendor")
|
||||
local_order = relationship("Order")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_letzshop_order_vendor", "vendor_id", "letzshop_order_id"),
|
||||
Index("idx_letzshop_order_state", "vendor_id", "letzshop_state"),
|
||||
Index("idx_letzshop_order_sync", "vendor_id", "sync_status"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<LetzshopOrder(id={self.id}, letzshop_id='{self.letzshop_order_id}', state='{self.letzshop_state}')>"
|
||||
|
||||
|
||||
class LetzshopFulfillmentQueue(Base, TimestampMixin):
|
||||
"""
|
||||
Queue for outbound fulfillment operations to Letzshop.
|
||||
|
||||
Supports retry logic for failed operations.
|
||||
"""
|
||||
|
||||
__tablename__ = "letzshop_fulfillment_queue"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False, index=True)
|
||||
letzshop_order_id = Column(
|
||||
Integer, ForeignKey("letzshop_orders.id"), nullable=False
|
||||
)
|
||||
|
||||
# Operation type
|
||||
operation = Column(
|
||||
String(50), nullable=False
|
||||
) # confirm, reject, set_tracking, return
|
||||
|
||||
# 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")
|
||||
letzshop_order = relationship("LetzshopOrder")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_fulfillment_queue_status", "status", "vendor_id"),
|
||||
Index("idx_fulfillment_queue_retry", "status", "next_retry_at"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<LetzshopFulfillmentQueue(id={self.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}')>"
|
||||
@@ -10,6 +10,7 @@ from sqlalchemy import (
|
||||
String,
|
||||
Text,
|
||||
)
|
||||
from sqlalchemy.dialects.sqlite import JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.core.database import Base
|
||||
@@ -29,6 +30,15 @@ class Order(Base, TimestampMixin):
|
||||
|
||||
order_number = Column(String, nullable=False, unique=True, index=True)
|
||||
|
||||
# Order channel/source
|
||||
channel = Column(
|
||||
String(50), default="direct", index=True
|
||||
) # direct, letzshop, amazon, etc.
|
||||
external_order_id = Column(
|
||||
String(100), nullable=True, index=True
|
||||
) # External order reference
|
||||
external_channel_data = Column(JSON, nullable=True) # Channel-specific metadata
|
||||
|
||||
# Order status
|
||||
status = Column(String, nullable=False, default="pending", index=True)
|
||||
# pending, processing, shipped, delivered, cancelled, refunded
|
||||
|
||||
@@ -96,6 +96,14 @@ class Vendor(Base, TimestampMixin):
|
||||
"MarketplaceImportJob", back_populates="vendor"
|
||||
) # Relationship with MarketplaceImportJob model for import jobs related to this vendor
|
||||
|
||||
# Letzshop integration credentials (one-to-one)
|
||||
letzshop_credentials = relationship(
|
||||
"VendorLetzshopCredentials",
|
||||
back_populates="vendor",
|
||||
uselist=False,
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
domains = relationship(
|
||||
"VendorDomain",
|
||||
back_populates="vendor",
|
||||
|
||||
337
models/schema/letzshop.py
Normal file
337
models/schema/letzshop.py
Normal file
@@ -0,0 +1,337 @@
|
||||
# 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, field_validator
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 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
|
||||
Reference in New Issue
Block a user