refactor: migrate modules from re-exports to canonical implementations
Move actual code implementations into module directories: - orders: 5 services, 4 models, order/invoice schemas - inventory: 3 services, 2 models, 30+ schemas - customers: 3 services, 2 models, customer schemas - messaging: 3 services, 2 models, message/notification schemas - monitoring: background_tasks_service - marketplace: 5+ services including letzshop submodule - dev_tools: code_quality_service, test_runner_service - billing: billing_service - contracts: definition.py Legacy files in app/services/, models/database/, models/schema/ now re-export from canonical module locations for backwards compatibility. Architecture validator passes with 0 errors. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,333 +1,69 @@
|
||||
# models/schema/customer.py
|
||||
"""
|
||||
Pydantic schema for customer-related operations.
|
||||
LEGACY LOCATION - Re-exports from module for backwards compatibility.
|
||||
|
||||
The canonical implementation is now in:
|
||||
app/modules/customers/schemas/customer.py
|
||||
|
||||
This file exists to maintain backwards compatibility with code that
|
||||
imports from the old location. All new code should import directly
|
||||
from the module:
|
||||
|
||||
from app.modules.customers.schemas import CustomerRegister, CustomerResponse
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, EmailStr, Field, field_validator
|
||||
|
||||
# ============================================================================
|
||||
# Customer Registration & Authentication
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class CustomerRegister(BaseModel):
|
||||
"""Schema for customer registration."""
|
||||
|
||||
email: EmailStr = Field(..., description="Customer email address")
|
||||
password: str = Field(
|
||||
..., min_length=8, description="Password (minimum 8 characters)"
|
||||
)
|
||||
first_name: str = Field(..., min_length=1, max_length=100)
|
||||
last_name: str = Field(..., min_length=1, max_length=100)
|
||||
phone: str | None = Field(None, max_length=50)
|
||||
marketing_consent: bool = Field(default=False)
|
||||
preferred_language: str | None = Field(
|
||||
None, description="Preferred language (en, fr, de, lb)"
|
||||
)
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def email_lowercase(cls, v: str) -> str:
|
||||
"""Convert email to lowercase."""
|
||||
return v.lower()
|
||||
|
||||
@field_validator("password")
|
||||
@classmethod
|
||||
def password_strength(cls, v: str) -> str:
|
||||
"""Validate password strength."""
|
||||
if len(v) < 8:
|
||||
raise ValueError("Password must be at least 8 characters")
|
||||
if not any(char.isdigit() for char in v):
|
||||
raise ValueError("Password must contain at least one digit")
|
||||
if not any(char.isalpha() for char in v):
|
||||
raise ValueError("Password must contain at least one letter")
|
||||
return v
|
||||
|
||||
|
||||
class CustomerUpdate(BaseModel):
|
||||
"""Schema for updating customer profile."""
|
||||
|
||||
email: EmailStr | None = None
|
||||
first_name: str | None = Field(None, min_length=1, max_length=100)
|
||||
last_name: str | None = Field(None, min_length=1, max_length=100)
|
||||
phone: str | None = Field(None, max_length=50)
|
||||
marketing_consent: bool | None = None
|
||||
preferred_language: str | None = Field(
|
||||
None, description="Preferred language (en, fr, de, lb)"
|
||||
)
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def email_lowercase(cls, v: str | None) -> str | None:
|
||||
"""Convert email to lowercase."""
|
||||
return v.lower() if v else None
|
||||
|
||||
|
||||
class CustomerPasswordChange(BaseModel):
|
||||
"""Schema for customer password change."""
|
||||
|
||||
current_password: str = Field(..., description="Current password")
|
||||
new_password: str = Field(
|
||||
..., min_length=8, description="New password (minimum 8 characters)"
|
||||
)
|
||||
confirm_password: str = Field(..., description="Confirm new password")
|
||||
|
||||
@field_validator("new_password")
|
||||
@classmethod
|
||||
def password_strength(cls, v: str) -> str:
|
||||
"""Validate password strength."""
|
||||
if len(v) < 8:
|
||||
raise ValueError("Password must be at least 8 characters")
|
||||
if not any(char.isdigit() for char in v):
|
||||
raise ValueError("Password must contain at least one digit")
|
||||
if not any(char.isalpha() for char in v):
|
||||
raise ValueError("Password must contain at least one letter")
|
||||
return v
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Customer Response
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class CustomerResponse(BaseModel):
|
||||
"""Schema for customer response (excludes password)."""
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
email: str
|
||||
first_name: str | None
|
||||
last_name: str | None
|
||||
phone: str | None
|
||||
customer_number: str
|
||||
marketing_consent: bool
|
||||
preferred_language: str | None
|
||||
last_order_date: datetime | None
|
||||
total_orders: int
|
||||
total_spent: Decimal
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@property
|
||||
def full_name(self) -> str:
|
||||
"""Get customer full name."""
|
||||
if self.first_name and self.last_name:
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
return self.email
|
||||
|
||||
|
||||
class CustomerListResponse(BaseModel):
|
||||
"""Schema for paginated customer list."""
|
||||
|
||||
customers: list[CustomerResponse]
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
total_pages: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Customer Address
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class CustomerAddressCreate(BaseModel):
|
||||
"""Schema for creating customer address."""
|
||||
|
||||
address_type: str = Field(..., pattern="^(billing|shipping)$")
|
||||
first_name: str = Field(..., min_length=1, max_length=100)
|
||||
last_name: str = Field(..., min_length=1, max_length=100)
|
||||
company: str | None = Field(None, max_length=200)
|
||||
address_line_1: str = Field(..., min_length=1, max_length=255)
|
||||
address_line_2: str | None = Field(None, max_length=255)
|
||||
city: str = Field(..., min_length=1, max_length=100)
|
||||
postal_code: str = Field(..., min_length=1, max_length=20)
|
||||
country_name: str = Field(..., min_length=2, max_length=100)
|
||||
country_iso: str = Field(..., min_length=2, max_length=5)
|
||||
is_default: bool = Field(default=False)
|
||||
|
||||
|
||||
class CustomerAddressUpdate(BaseModel):
|
||||
"""Schema for updating customer address."""
|
||||
|
||||
address_type: str | None = Field(None, pattern="^(billing|shipping)$")
|
||||
first_name: str | None = Field(None, min_length=1, max_length=100)
|
||||
last_name: str | None = Field(None, min_length=1, max_length=100)
|
||||
company: str | None = Field(None, max_length=200)
|
||||
address_line_1: str | None = Field(None, min_length=1, max_length=255)
|
||||
address_line_2: str | None = Field(None, max_length=255)
|
||||
city: str | None = Field(None, min_length=1, max_length=100)
|
||||
postal_code: str | None = Field(None, min_length=1, max_length=20)
|
||||
country_name: str | None = Field(None, min_length=2, max_length=100)
|
||||
country_iso: str | None = Field(None, min_length=2, max_length=5)
|
||||
is_default: bool | None = None
|
||||
|
||||
|
||||
class CustomerAddressResponse(BaseModel):
|
||||
"""Schema for customer address response."""
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
customer_id: int
|
||||
address_type: str
|
||||
first_name: str
|
||||
last_name: str
|
||||
company: str | None
|
||||
address_line_1: str
|
||||
address_line_2: str | None
|
||||
city: str
|
||||
postal_code: str
|
||||
country_name: str
|
||||
country_iso: str
|
||||
is_default: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class CustomerAddressListResponse(BaseModel):
|
||||
"""Schema for customer address list response."""
|
||||
|
||||
addresses: list[CustomerAddressResponse]
|
||||
total: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Customer Preferences
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class CustomerPreferencesUpdate(BaseModel):
|
||||
"""Schema for updating customer preferences."""
|
||||
|
||||
marketing_consent: bool | None = None
|
||||
preferred_language: str | None = Field(
|
||||
None, description="Preferred language (en, fr, de, lb)"
|
||||
)
|
||||
currency: str | None = Field(None, max_length=3)
|
||||
notification_preferences: dict[str, bool] | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Vendor Customer Management Response Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class CustomerMessageResponse(BaseModel):
|
||||
"""Simple message response for customer operations."""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
class VendorCustomerListResponse(BaseModel):
|
||||
"""Schema for vendor customer list with skip/limit pagination."""
|
||||
|
||||
customers: list[CustomerResponse] = []
|
||||
total: int = 0
|
||||
skip: int = 0
|
||||
limit: int = 100
|
||||
message: str | None = None
|
||||
|
||||
|
||||
class CustomerDetailResponse(BaseModel):
|
||||
"""Detailed customer response for vendor management."""
|
||||
|
||||
id: int | None = None
|
||||
vendor_id: int | None = None
|
||||
email: str | None = None
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
phone: str | None = None
|
||||
customer_number: str | None = None
|
||||
marketing_consent: bool | None = None
|
||||
preferred_language: str | None = None
|
||||
last_order_date: datetime | None = None
|
||||
total_orders: int | None = None
|
||||
total_spent: Decimal | None = None
|
||||
is_active: bool | None = None
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
message: str | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class CustomerOrderInfo(BaseModel):
|
||||
"""Basic order info for customer order history."""
|
||||
|
||||
id: int
|
||||
order_number: str
|
||||
status: str
|
||||
total: Decimal
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class CustomerOrdersResponse(BaseModel):
|
||||
"""Response for customer order history."""
|
||||
|
||||
orders: list[CustomerOrderInfo] = []
|
||||
total: int = 0
|
||||
message: str | None = None
|
||||
|
||||
|
||||
class CustomerStatisticsResponse(BaseModel):
|
||||
"""Response for customer statistics."""
|
||||
|
||||
total: int = 0
|
||||
active: int = 0
|
||||
inactive: int = 0
|
||||
with_orders: int = 0
|
||||
total_spent: float = 0.0
|
||||
total_orders: int = 0
|
||||
avg_order_value: float = 0.0
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Admin Customer Management Response Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AdminCustomerItem(BaseModel):
|
||||
"""Admin customer list item with vendor info."""
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
email: str
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
phone: str | None = None
|
||||
customer_number: str
|
||||
marketing_consent: bool = False
|
||||
preferred_language: str | None = None
|
||||
last_order_date: datetime | None = None
|
||||
total_orders: int = 0
|
||||
total_spent: float = 0.0
|
||||
is_active: bool = True
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
vendor_name: str | None = None
|
||||
vendor_code: str | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class CustomerListResponse(BaseModel):
|
||||
"""Admin paginated customer list with skip/limit."""
|
||||
|
||||
customers: list[AdminCustomerItem] = []
|
||||
total: int = 0
|
||||
skip: int = 0
|
||||
limit: int = 20
|
||||
|
||||
|
||||
class CustomerDetailResponse(AdminCustomerItem):
|
||||
"""Detailed customer response for admin."""
|
||||
|
||||
pass
|
||||
from app.modules.customers.schemas.customer import (
|
||||
# Registration & Authentication
|
||||
CustomerRegister,
|
||||
CustomerUpdate,
|
||||
CustomerPasswordChange,
|
||||
# Customer Response
|
||||
CustomerResponse,
|
||||
CustomerListResponse,
|
||||
# Address
|
||||
CustomerAddressCreate,
|
||||
CustomerAddressUpdate,
|
||||
CustomerAddressResponse,
|
||||
CustomerAddressListResponse,
|
||||
# Preferences
|
||||
CustomerPreferencesUpdate,
|
||||
# Vendor Management
|
||||
CustomerMessageResponse,
|
||||
VendorCustomerListResponse,
|
||||
CustomerDetailResponse,
|
||||
CustomerOrderInfo,
|
||||
CustomerOrdersResponse,
|
||||
CustomerStatisticsResponse,
|
||||
# Admin Management
|
||||
AdminCustomerItem,
|
||||
AdminCustomerListResponse,
|
||||
AdminCustomerDetailResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Registration & Authentication
|
||||
"CustomerRegister",
|
||||
"CustomerUpdate",
|
||||
"CustomerPasswordChange",
|
||||
# Customer Response
|
||||
"CustomerResponse",
|
||||
"CustomerListResponse",
|
||||
# Address
|
||||
"CustomerAddressCreate",
|
||||
"CustomerAddressUpdate",
|
||||
"CustomerAddressResponse",
|
||||
"CustomerAddressListResponse",
|
||||
# Preferences
|
||||
"CustomerPreferencesUpdate",
|
||||
# Vendor Management
|
||||
"CustomerMessageResponse",
|
||||
"VendorCustomerListResponse",
|
||||
"CustomerDetailResponse",
|
||||
"CustomerOrderInfo",
|
||||
"CustomerOrdersResponse",
|
||||
"CustomerStatisticsResponse",
|
||||
# Admin Management
|
||||
"AdminCustomerItem",
|
||||
"AdminCustomerListResponse",
|
||||
"AdminCustomerDetailResponse",
|
||||
]
|
||||
|
||||
@@ -1,294 +1,85 @@
|
||||
# models/schema/inventory.py
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class InventoryBase(BaseModel):
|
||||
product_id: int = Field(..., description="Product ID in vendor catalog")
|
||||
location: str = Field(..., description="Storage location")
|
||||
|
||||
|
||||
class InventoryCreate(InventoryBase):
|
||||
"""Set exact inventory quantity (replaces existing)."""
|
||||
|
||||
quantity: int = Field(..., description="Exact inventory quantity", ge=0)
|
||||
|
||||
|
||||
class InventoryAdjust(InventoryBase):
|
||||
"""Add or remove inventory quantity."""
|
||||
|
||||
quantity: int = Field(
|
||||
..., description="Quantity to add (positive) or remove (negative)"
|
||||
)
|
||||
|
||||
|
||||
class InventoryUpdate(BaseModel):
|
||||
"""Update inventory fields."""
|
||||
|
||||
quantity: int | None = Field(None, ge=0)
|
||||
reserved_quantity: int | None = Field(None, ge=0)
|
||||
location: str | None = None
|
||||
|
||||
|
||||
class InventoryReserve(BaseModel):
|
||||
"""Reserve inventory for orders."""
|
||||
|
||||
product_id: int
|
||||
location: str
|
||||
quantity: int = Field(..., gt=0)
|
||||
|
||||
|
||||
class InventoryResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
product_id: int
|
||||
vendor_id: int
|
||||
location: str
|
||||
quantity: int
|
||||
reserved_quantity: int
|
||||
gtin: str | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@property
|
||||
def available_quantity(self):
|
||||
return max(0, self.quantity - self.reserved_quantity)
|
||||
|
||||
|
||||
class InventoryLocationResponse(BaseModel):
|
||||
location: str
|
||||
quantity: int
|
||||
reserved_quantity: int
|
||||
available_quantity: int
|
||||
|
||||
|
||||
class ProductInventorySummary(BaseModel):
|
||||
"""Inventory summary for a product."""
|
||||
|
||||
product_id: int
|
||||
vendor_id: int
|
||||
product_sku: str | None
|
||||
product_title: str
|
||||
total_quantity: int
|
||||
total_reserved: int
|
||||
total_available: int
|
||||
locations: list[InventoryLocationResponse]
|
||||
|
||||
|
||||
class InventoryListResponse(BaseModel):
|
||||
inventories: list[InventoryResponse]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
class InventoryMessageResponse(BaseModel):
|
||||
"""Simple message response for inventory operations."""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
class InventorySummaryResponse(BaseModel):
|
||||
"""Inventory summary response for marketplace product service."""
|
||||
|
||||
gtin: str
|
||||
total_quantity: int
|
||||
locations: list[InventoryLocationResponse]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Admin Inventory Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AdminInventoryCreate(BaseModel):
|
||||
"""Admin version of inventory create - requires explicit vendor_id."""
|
||||
|
||||
vendor_id: int = Field(..., description="Target vendor ID")
|
||||
product_id: int = Field(..., description="Product ID in vendor catalog")
|
||||
location: str = Field(..., description="Storage location")
|
||||
quantity: int = Field(..., description="Exact inventory quantity", ge=0)
|
||||
|
||||
|
||||
class AdminInventoryAdjust(BaseModel):
|
||||
"""Admin version of inventory adjust - requires explicit vendor_id."""
|
||||
|
||||
vendor_id: int = Field(..., description="Target vendor ID")
|
||||
product_id: int = Field(..., description="Product ID in vendor catalog")
|
||||
location: str = Field(..., description="Storage location")
|
||||
quantity: int = Field(
|
||||
..., description="Quantity to add (positive) or remove (negative)"
|
||||
)
|
||||
reason: str | None = Field(None, description="Reason for adjustment")
|
||||
|
||||
|
||||
class AdminInventoryItem(BaseModel):
|
||||
"""Inventory item with vendor info for admin list view."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
product_id: int
|
||||
vendor_id: int
|
||||
vendor_name: str | None = None
|
||||
vendor_code: str | None = None
|
||||
product_title: str | None = None
|
||||
product_sku: str | None = None
|
||||
location: str
|
||||
quantity: int
|
||||
reserved_quantity: int
|
||||
available_quantity: int
|
||||
gtin: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class AdminInventoryListResponse(BaseModel):
|
||||
"""Cross-vendor inventory list for admin."""
|
||||
|
||||
inventories: list[AdminInventoryItem]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
vendor_filter: int | None = None
|
||||
location_filter: str | None = None
|
||||
|
||||
|
||||
class AdminInventoryStats(BaseModel):
|
||||
"""Inventory statistics for admin dashboard."""
|
||||
|
||||
total_entries: int
|
||||
total_quantity: int
|
||||
total_reserved: int
|
||||
total_available: int
|
||||
low_stock_count: int
|
||||
vendors_with_inventory: int
|
||||
unique_locations: int
|
||||
|
||||
|
||||
class AdminLowStockItem(BaseModel):
|
||||
"""Low stock item for admin alerts."""
|
||||
|
||||
id: int
|
||||
product_id: int
|
||||
vendor_id: int
|
||||
vendor_name: str | None = None
|
||||
product_title: str | None = None
|
||||
location: str
|
||||
quantity: int
|
||||
reserved_quantity: int
|
||||
available_quantity: int
|
||||
|
||||
|
||||
class AdminVendorWithInventory(BaseModel):
|
||||
"""Vendor with inventory entries."""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
vendor_code: str
|
||||
|
||||
|
||||
class AdminVendorsWithInventoryResponse(BaseModel):
|
||||
"""Response for vendors with inventory list."""
|
||||
|
||||
vendors: list[AdminVendorWithInventory]
|
||||
|
||||
|
||||
class AdminInventoryLocationsResponse(BaseModel):
|
||||
"""Response for unique inventory locations."""
|
||||
|
||||
locations: list[str]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Inventory Transaction Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class InventoryTransactionResponse(BaseModel):
|
||||
"""Single inventory transaction record."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
product_id: int
|
||||
inventory_id: int | None = None
|
||||
transaction_type: str
|
||||
quantity_change: int
|
||||
quantity_after: int
|
||||
reserved_after: int
|
||||
location: str | None = None
|
||||
warehouse: str | None = None
|
||||
order_id: int | None = None
|
||||
order_number: str | None = None
|
||||
reason: str | None = None
|
||||
created_by: str | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class InventoryTransactionWithProduct(InventoryTransactionResponse):
|
||||
"""Transaction with product details for list views."""
|
||||
|
||||
product_title: str | None = None
|
||||
product_sku: str | None = None
|
||||
|
||||
|
||||
class InventoryTransactionListResponse(BaseModel):
|
||||
"""Paginated list of inventory transactions."""
|
||||
|
||||
transactions: list[InventoryTransactionWithProduct]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
class ProductTransactionHistoryResponse(BaseModel):
|
||||
"""Transaction history for a specific product."""
|
||||
|
||||
product_id: int
|
||||
product_title: str | None = None
|
||||
product_sku: str | None = None
|
||||
current_quantity: int
|
||||
current_reserved: int
|
||||
transactions: list[InventoryTransactionResponse]
|
||||
total: int
|
||||
|
||||
|
||||
class OrderTransactionHistoryResponse(BaseModel):
|
||||
"""Transaction history for a specific order."""
|
||||
|
||||
order_id: int
|
||||
order_number: str
|
||||
transactions: list[InventoryTransactionWithProduct]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Admin Inventory Transaction Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AdminInventoryTransactionItem(InventoryTransactionWithProduct):
|
||||
"""Transaction with vendor details for admin views."""
|
||||
|
||||
vendor_name: str | None = None
|
||||
vendor_code: str | None = None
|
||||
|
||||
|
||||
class AdminInventoryTransactionListResponse(BaseModel):
|
||||
"""Paginated list of transactions for admin."""
|
||||
|
||||
transactions: list[AdminInventoryTransactionItem]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
class AdminTransactionStatsResponse(BaseModel):
|
||||
"""Transaction statistics for admin dashboard."""
|
||||
|
||||
total_transactions: int
|
||||
transactions_today: int
|
||||
by_type: dict[str, int]
|
||||
"""
|
||||
LEGACY LOCATION - Re-exports from module for backwards compatibility.
|
||||
|
||||
The canonical implementation is now in:
|
||||
app/modules/inventory/schemas/inventory.py
|
||||
|
||||
This file exists to maintain backwards compatibility with code that
|
||||
imports from the old location. All new code should import directly
|
||||
from the module:
|
||||
|
||||
from app.modules.inventory.schemas import InventoryCreate, InventoryResponse
|
||||
"""
|
||||
|
||||
from app.modules.inventory.schemas.inventory import (
|
||||
# Base schemas
|
||||
InventoryBase,
|
||||
InventoryCreate,
|
||||
InventoryAdjust,
|
||||
InventoryUpdate,
|
||||
InventoryReserve,
|
||||
# Response schemas
|
||||
InventoryResponse,
|
||||
InventoryLocationResponse,
|
||||
ProductInventorySummary,
|
||||
InventoryListResponse,
|
||||
InventoryMessageResponse,
|
||||
InventorySummaryResponse,
|
||||
# Admin schemas
|
||||
AdminInventoryCreate,
|
||||
AdminInventoryAdjust,
|
||||
AdminInventoryItem,
|
||||
AdminInventoryListResponse,
|
||||
AdminInventoryStats,
|
||||
AdminLowStockItem,
|
||||
AdminVendorWithInventory,
|
||||
AdminVendorsWithInventoryResponse,
|
||||
AdminInventoryLocationsResponse,
|
||||
# Transaction schemas
|
||||
InventoryTransactionResponse,
|
||||
InventoryTransactionWithProduct,
|
||||
InventoryTransactionListResponse,
|
||||
ProductTransactionHistoryResponse,
|
||||
OrderTransactionHistoryResponse,
|
||||
# Admin transaction schemas
|
||||
AdminInventoryTransactionItem,
|
||||
AdminInventoryTransactionListResponse,
|
||||
AdminTransactionStatsResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Base schemas
|
||||
"InventoryBase",
|
||||
"InventoryCreate",
|
||||
"InventoryAdjust",
|
||||
"InventoryUpdate",
|
||||
"InventoryReserve",
|
||||
# Response schemas
|
||||
"InventoryResponse",
|
||||
"InventoryLocationResponse",
|
||||
"ProductInventorySummary",
|
||||
"InventoryListResponse",
|
||||
"InventoryMessageResponse",
|
||||
"InventorySummaryResponse",
|
||||
# Admin schemas
|
||||
"AdminInventoryCreate",
|
||||
"AdminInventoryAdjust",
|
||||
"AdminInventoryItem",
|
||||
"AdminInventoryListResponse",
|
||||
"AdminInventoryStats",
|
||||
"AdminLowStockItem",
|
||||
"AdminVendorWithInventory",
|
||||
"AdminVendorsWithInventoryResponse",
|
||||
"AdminInventoryLocationsResponse",
|
||||
# Transaction schemas
|
||||
"InventoryTransactionResponse",
|
||||
"InventoryTransactionWithProduct",
|
||||
"InventoryTransactionListResponse",
|
||||
"ProductTransactionHistoryResponse",
|
||||
"OrderTransactionHistoryResponse",
|
||||
# Admin transaction schemas
|
||||
"AdminInventoryTransactionItem",
|
||||
"AdminInventoryTransactionListResponse",
|
||||
"AdminTransactionStatsResponse",
|
||||
]
|
||||
|
||||
@@ -1,310 +1,61 @@
|
||||
# models/schema/invoice.py
|
||||
"""
|
||||
Pydantic schemas for invoice operations.
|
||||
LEGACY LOCATION - Re-exports from module for backwards compatibility.
|
||||
|
||||
Supports invoice settings management and invoice generation.
|
||||
The canonical implementation is now in:
|
||||
app/modules/orders/schemas/invoice.py
|
||||
|
||||
This file exists to maintain backwards compatibility with code that
|
||||
imports from the old location. All new code should import directly
|
||||
from the module:
|
||||
|
||||
from app.modules.orders.schemas import invoice
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
# ============================================================================
|
||||
# Invoice Settings Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class VendorInvoiceSettingsCreate(BaseModel):
|
||||
"""Schema for creating vendor invoice settings."""
|
||||
|
||||
company_name: str = Field(..., min_length=1, max_length=255)
|
||||
company_address: str | None = Field(None, max_length=255)
|
||||
company_city: str | None = Field(None, max_length=100)
|
||||
company_postal_code: str | None = Field(None, max_length=20)
|
||||
company_country: str = Field(default="LU", min_length=2, max_length=2)
|
||||
|
||||
vat_number: str | None = Field(None, max_length=50)
|
||||
is_vat_registered: bool = True
|
||||
|
||||
is_oss_registered: bool = False
|
||||
oss_registration_country: str | None = Field(None, min_length=2, max_length=2)
|
||||
|
||||
invoice_prefix: str = Field(default="INV", max_length=20)
|
||||
invoice_number_padding: int = Field(default=5, ge=1, le=10)
|
||||
|
||||
payment_terms: str | None = None
|
||||
bank_name: str | None = Field(None, max_length=255)
|
||||
bank_iban: str | None = Field(None, max_length=50)
|
||||
bank_bic: str | None = Field(None, max_length=20)
|
||||
|
||||
footer_text: str | None = None
|
||||
default_vat_rate: Decimal = Field(default=Decimal("17.00"), ge=0, le=100)
|
||||
|
||||
|
||||
class VendorInvoiceSettingsUpdate(BaseModel):
|
||||
"""Schema for updating vendor invoice settings."""
|
||||
|
||||
company_name: str | None = Field(None, min_length=1, max_length=255)
|
||||
company_address: str | None = Field(None, max_length=255)
|
||||
company_city: str | None = Field(None, max_length=100)
|
||||
company_postal_code: str | None = Field(None, max_length=20)
|
||||
company_country: str | None = Field(None, min_length=2, max_length=2)
|
||||
|
||||
vat_number: str | None = None
|
||||
is_vat_registered: bool | None = None
|
||||
|
||||
is_oss_registered: bool | None = None
|
||||
oss_registration_country: str | None = None
|
||||
|
||||
invoice_prefix: str | None = Field(None, max_length=20)
|
||||
invoice_number_padding: int | None = Field(None, ge=1, le=10)
|
||||
|
||||
payment_terms: str | None = None
|
||||
bank_name: str | None = Field(None, max_length=255)
|
||||
bank_iban: str | None = Field(None, max_length=50)
|
||||
bank_bic: str | None = Field(None, max_length=20)
|
||||
|
||||
footer_text: str | None = None
|
||||
default_vat_rate: Decimal | None = Field(None, ge=0, le=100)
|
||||
|
||||
|
||||
class VendorInvoiceSettingsResponse(BaseModel):
|
||||
"""Schema for vendor invoice settings response."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
|
||||
company_name: str
|
||||
company_address: str | None
|
||||
company_city: str | None
|
||||
company_postal_code: str | None
|
||||
company_country: str
|
||||
|
||||
vat_number: str | None
|
||||
is_vat_registered: bool
|
||||
|
||||
is_oss_registered: bool
|
||||
oss_registration_country: str | None
|
||||
|
||||
invoice_prefix: str
|
||||
invoice_next_number: int
|
||||
invoice_number_padding: int
|
||||
|
||||
payment_terms: str | None
|
||||
bank_name: str | None
|
||||
bank_iban: str | None
|
||||
bank_bic: str | None
|
||||
|
||||
footer_text: str | None
|
||||
default_vat_rate: Decimal
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Invoice Line Item Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class InvoiceLineItem(BaseModel):
|
||||
"""Schema for invoice line item."""
|
||||
|
||||
description: str
|
||||
quantity: int = Field(..., ge=1)
|
||||
unit_price_cents: int
|
||||
total_cents: int
|
||||
sku: str | None = None
|
||||
ean: str | None = None
|
||||
|
||||
|
||||
class InvoiceLineItemResponse(BaseModel):
|
||||
"""Schema for invoice line item in response."""
|
||||
|
||||
description: str
|
||||
quantity: int
|
||||
unit_price_cents: int
|
||||
total_cents: int
|
||||
sku: str | None = None
|
||||
ean: str | None = None
|
||||
|
||||
@property
|
||||
def unit_price(self) -> float:
|
||||
return self.unit_price_cents / 100
|
||||
|
||||
@property
|
||||
def total(self) -> float:
|
||||
return self.total_cents / 100
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Invoice Address Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class InvoiceSellerDetails(BaseModel):
|
||||
"""Seller details for invoice."""
|
||||
|
||||
company_name: str
|
||||
address: str | None = None
|
||||
city: str | None = None
|
||||
postal_code: str | None = None
|
||||
country: str
|
||||
vat_number: str | None = None
|
||||
|
||||
|
||||
class InvoiceBuyerDetails(BaseModel):
|
||||
"""Buyer details for invoice."""
|
||||
|
||||
name: str
|
||||
email: str | None = None
|
||||
address: str | None = None
|
||||
city: str | None = None
|
||||
postal_code: str | None = None
|
||||
country: str
|
||||
vat_number: str | None = None # For B2B
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Invoice Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class InvoiceCreate(BaseModel):
|
||||
"""Schema for creating an invoice from an order."""
|
||||
|
||||
order_id: int
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
class InvoiceManualCreate(BaseModel):
|
||||
"""Schema for creating a manual invoice (without order)."""
|
||||
|
||||
buyer_details: InvoiceBuyerDetails
|
||||
line_items: list[InvoiceLineItem]
|
||||
notes: str | None = None
|
||||
payment_terms: str | None = None
|
||||
|
||||
|
||||
class InvoiceResponse(BaseModel):
|
||||
"""Schema for invoice response."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
order_id: int | None
|
||||
|
||||
invoice_number: str
|
||||
invoice_date: datetime
|
||||
status: str
|
||||
|
||||
seller_details: dict
|
||||
buyer_details: dict
|
||||
line_items: list[dict]
|
||||
|
||||
vat_regime: str
|
||||
destination_country: str | None
|
||||
vat_rate: Decimal
|
||||
vat_rate_label: str | None
|
||||
|
||||
currency: str
|
||||
subtotal_cents: int
|
||||
vat_amount_cents: int
|
||||
total_cents: int
|
||||
|
||||
payment_terms: str | None
|
||||
bank_details: dict | None
|
||||
footer_text: str | None
|
||||
|
||||
pdf_generated_at: datetime | None
|
||||
pdf_path: str | None
|
||||
|
||||
notes: str | None
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@property
|
||||
def subtotal(self) -> float:
|
||||
return self.subtotal_cents / 100
|
||||
|
||||
@property
|
||||
def vat_amount(self) -> float:
|
||||
return self.vat_amount_cents / 100
|
||||
|
||||
@property
|
||||
def total(self) -> float:
|
||||
return self.total_cents / 100
|
||||
|
||||
|
||||
class InvoiceListResponse(BaseModel):
|
||||
"""Schema for invoice list response (summary)."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
invoice_number: str
|
||||
invoice_date: datetime
|
||||
status: str
|
||||
currency: str
|
||||
total_cents: int
|
||||
order_id: int | None
|
||||
|
||||
# Buyer name for display
|
||||
buyer_name: str | None = None
|
||||
|
||||
@property
|
||||
def total(self) -> float:
|
||||
return self.total_cents / 100
|
||||
|
||||
|
||||
class InvoiceStatusUpdate(BaseModel):
|
||||
"""Schema for updating invoice status."""
|
||||
|
||||
status: str = Field(..., pattern="^(draft|issued|paid|cancelled)$")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Paginated Response
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class InvoiceListPaginatedResponse(BaseModel):
|
||||
"""Paginated invoice list response."""
|
||||
|
||||
items: list[InvoiceListResponse]
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
pages: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PDF Response
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class InvoicePDFGeneratedResponse(BaseModel):
|
||||
"""Response for PDF generation."""
|
||||
|
||||
pdf_path: str
|
||||
message: str = "PDF generated successfully"
|
||||
|
||||
|
||||
class InvoiceStatsResponse(BaseModel):
|
||||
"""Invoice statistics response."""
|
||||
|
||||
total_invoices: int
|
||||
total_revenue_cents: int
|
||||
draft_count: int
|
||||
issued_count: int
|
||||
paid_count: int
|
||||
cancelled_count: int
|
||||
|
||||
@property
|
||||
def total_revenue(self) -> float:
|
||||
return self.total_revenue_cents / 100
|
||||
from app.modules.orders.schemas.invoice import (
|
||||
# Invoice settings schemas
|
||||
VendorInvoiceSettingsCreate,
|
||||
VendorInvoiceSettingsUpdate,
|
||||
VendorInvoiceSettingsResponse,
|
||||
# Line item schemas
|
||||
InvoiceLineItem,
|
||||
InvoiceLineItemResponse,
|
||||
# Address schemas
|
||||
InvoiceSellerDetails,
|
||||
InvoiceBuyerDetails,
|
||||
# Invoice CRUD schemas
|
||||
InvoiceCreate,
|
||||
InvoiceManualCreate,
|
||||
InvoiceResponse,
|
||||
InvoiceListResponse,
|
||||
InvoiceStatusUpdate,
|
||||
# Pagination
|
||||
InvoiceListPaginatedResponse,
|
||||
# PDF
|
||||
InvoicePDFGeneratedResponse,
|
||||
InvoiceStatsResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Invoice settings schemas
|
||||
"VendorInvoiceSettingsCreate",
|
||||
"VendorInvoiceSettingsUpdate",
|
||||
"VendorInvoiceSettingsResponse",
|
||||
# Line item schemas
|
||||
"InvoiceLineItem",
|
||||
"InvoiceLineItemResponse",
|
||||
# Address schemas
|
||||
"InvoiceSellerDetails",
|
||||
"InvoiceBuyerDetails",
|
||||
# Invoice CRUD schemas
|
||||
"InvoiceCreate",
|
||||
"InvoiceManualCreate",
|
||||
"InvoiceResponse",
|
||||
"InvoiceListResponse",
|
||||
"InvoiceStatusUpdate",
|
||||
# Pagination
|
||||
"InvoiceListPaginatedResponse",
|
||||
# PDF
|
||||
"InvoicePDFGeneratedResponse",
|
||||
"InvoiceStatsResponse",
|
||||
]
|
||||
|
||||
@@ -1,308 +1,83 @@
|
||||
# models/schema/message.py
|
||||
"""
|
||||
Pydantic schemas for the messaging system.
|
||||
LEGACY LOCATION - Re-exports from module for backwards compatibility.
|
||||
|
||||
Supports three communication channels:
|
||||
- Admin <-> Vendor
|
||||
- Vendor <-> Customer
|
||||
- Admin <-> Customer
|
||||
The canonical implementation is now in:
|
||||
app/modules/messaging/schemas/message.py
|
||||
|
||||
This file exists to maintain backwards compatibility with code that
|
||||
imports from the old location. All new code should import directly
|
||||
from the module:
|
||||
|
||||
from app.modules.messaging.schemas import message
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from models.database.message import ConversationType, ParticipantType
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Attachment Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AttachmentResponse(BaseModel):
|
||||
"""Schema for message attachment in responses."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
filename: str
|
||||
original_filename: str
|
||||
file_size: int
|
||||
mime_type: str
|
||||
is_image: bool
|
||||
image_width: int | None = None
|
||||
image_height: int | None = None
|
||||
download_url: str | None = None
|
||||
thumbnail_url: str | None = None
|
||||
|
||||
@property
|
||||
def file_size_display(self) -> str:
|
||||
"""Human-readable file size."""
|
||||
if self.file_size < 1024:
|
||||
return f"{self.file_size} B"
|
||||
elif self.file_size < 1024 * 1024:
|
||||
return f"{self.file_size / 1024:.1f} KB"
|
||||
else:
|
||||
return f"{self.file_size / 1024 / 1024:.1f} MB"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Message Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MessageCreate(BaseModel):
|
||||
"""Schema for sending a new message."""
|
||||
|
||||
content: str = Field(..., min_length=1, max_length=10000)
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
"""Schema for a single message in responses."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
conversation_id: int
|
||||
sender_type: ParticipantType
|
||||
sender_id: int
|
||||
content: str
|
||||
is_system_message: bool
|
||||
is_deleted: bool
|
||||
created_at: datetime
|
||||
|
||||
# Enriched sender info (populated by API)
|
||||
sender_name: str | None = None
|
||||
sender_email: str | None = None
|
||||
|
||||
# Attachments
|
||||
attachments: list[AttachmentResponse] = []
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Participant Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ParticipantInfo(BaseModel):
|
||||
"""Schema for participant information."""
|
||||
|
||||
id: int
|
||||
type: ParticipantType
|
||||
name: str
|
||||
email: str | None = None
|
||||
avatar_url: str | None = None
|
||||
|
||||
|
||||
class ParticipantResponse(BaseModel):
|
||||
"""Schema for conversation participant in responses."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
participant_type: ParticipantType
|
||||
participant_id: int
|
||||
unread_count: int
|
||||
last_read_at: datetime | None
|
||||
email_notifications: bool
|
||||
muted: bool
|
||||
|
||||
# Enriched info (populated by API)
|
||||
participant_info: ParticipantInfo | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Conversation Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ConversationCreate(BaseModel):
|
||||
"""Schema for creating a new conversation."""
|
||||
|
||||
conversation_type: ConversationType
|
||||
subject: str = Field(..., min_length=1, max_length=500)
|
||||
recipient_type: ParticipantType
|
||||
recipient_id: int
|
||||
vendor_id: int | None = None
|
||||
initial_message: str | None = Field(None, min_length=1, max_length=10000)
|
||||
|
||||
|
||||
class ConversationSummary(BaseModel):
|
||||
"""Schema for conversation in list views."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
conversation_type: ConversationType
|
||||
subject: str
|
||||
vendor_id: int | None = None
|
||||
is_closed: bool
|
||||
closed_at: datetime | None
|
||||
last_message_at: datetime | None
|
||||
message_count: int
|
||||
created_at: datetime
|
||||
|
||||
# Unread count for current user (from participant)
|
||||
unread_count: int = 0
|
||||
|
||||
# Other participant info (enriched by API)
|
||||
other_participant: ParticipantInfo | None = None
|
||||
|
||||
# Last message preview
|
||||
last_message_preview: str | None = None
|
||||
|
||||
|
||||
class ConversationDetailResponse(BaseModel):
|
||||
"""Schema for full conversation detail with messages."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
conversation_type: ConversationType
|
||||
subject: str
|
||||
vendor_id: int | None = None
|
||||
is_closed: bool
|
||||
closed_at: datetime | None
|
||||
closed_by_type: ParticipantType | None = None
|
||||
closed_by_id: int | None = None
|
||||
last_message_at: datetime | None
|
||||
message_count: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
# Participants with enriched info
|
||||
participants: list[ParticipantResponse] = []
|
||||
|
||||
# Messages ordered by created_at
|
||||
messages: list[MessageResponse] = []
|
||||
|
||||
# Current user's unread count
|
||||
unread_count: int = 0
|
||||
|
||||
# Vendor info if applicable
|
||||
vendor_name: str | None = None
|
||||
|
||||
|
||||
class ConversationListResponse(BaseModel):
|
||||
"""Schema for paginated conversation list."""
|
||||
|
||||
conversations: list[ConversationSummary]
|
||||
total: int
|
||||
total_unread: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Unread Count Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class UnreadCountResponse(BaseModel):
|
||||
"""Schema for unread message count (for header badge)."""
|
||||
|
||||
total_unread: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Notification Preferences Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class NotificationPreferencesUpdate(BaseModel):
|
||||
"""Schema for updating notification preferences."""
|
||||
|
||||
email_notifications: bool | None = None
|
||||
muted: bool | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Conversation Action Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class CloseConversationResponse(BaseModel):
|
||||
"""Response after closing a conversation."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
conversation_id: int
|
||||
|
||||
|
||||
class ReopenConversationResponse(BaseModel):
|
||||
"""Response after reopening a conversation."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
conversation_id: int
|
||||
|
||||
|
||||
class MarkReadResponse(BaseModel):
|
||||
"""Response after marking conversation as read."""
|
||||
|
||||
success: bool
|
||||
conversation_id: int
|
||||
unread_count: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Recipient Selection Schemas (for compose modal)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class RecipientOption(BaseModel):
|
||||
"""Schema for a selectable recipient in compose modal."""
|
||||
|
||||
id: int
|
||||
type: ParticipantType
|
||||
name: str
|
||||
email: str | None = None
|
||||
vendor_id: int | None = None # For vendor users
|
||||
vendor_name: str | None = None
|
||||
|
||||
|
||||
class RecipientListResponse(BaseModel):
|
||||
"""Schema for list of available recipients."""
|
||||
|
||||
recipients: list[RecipientOption]
|
||||
total: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Admin-specific Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AdminConversationSummary(ConversationSummary):
|
||||
"""Extended conversation summary with vendor info for admin views."""
|
||||
|
||||
vendor_name: str | None = None
|
||||
vendor_code: str | None = None
|
||||
|
||||
|
||||
class AdminConversationListResponse(BaseModel):
|
||||
"""Schema for admin conversation list with vendor info."""
|
||||
|
||||
conversations: list[AdminConversationSummary]
|
||||
total: int
|
||||
total_unread: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
class AdminMessageStats(BaseModel):
|
||||
"""Messaging statistics for admin dashboard."""
|
||||
|
||||
total_conversations: int = 0
|
||||
open_conversations: int = 0
|
||||
closed_conversations: int = 0
|
||||
total_messages: int = 0
|
||||
|
||||
# By type
|
||||
admin_vendor_conversations: int = 0
|
||||
vendor_customer_conversations: int = 0
|
||||
admin_customer_conversations: int = 0
|
||||
|
||||
# Unread
|
||||
unread_admin: int = 0
|
||||
from app.modules.messaging.schemas.message import (
|
||||
# Attachment schemas
|
||||
AttachmentResponse,
|
||||
# Message schemas
|
||||
MessageCreate,
|
||||
MessageResponse,
|
||||
# Participant schemas
|
||||
ParticipantInfo,
|
||||
ParticipantResponse,
|
||||
# Conversation schemas
|
||||
ConversationCreate,
|
||||
ConversationSummary,
|
||||
ConversationDetailResponse,
|
||||
ConversationListResponse,
|
||||
ConversationResponse,
|
||||
# Unread count
|
||||
UnreadCountResponse,
|
||||
# Notification preferences
|
||||
NotificationPreferencesUpdate,
|
||||
# Conversation actions
|
||||
CloseConversationResponse,
|
||||
ReopenConversationResponse,
|
||||
MarkReadResponse,
|
||||
# Recipient selection
|
||||
RecipientOption,
|
||||
RecipientListResponse,
|
||||
# Admin schemas
|
||||
AdminConversationSummary,
|
||||
AdminConversationListResponse,
|
||||
AdminMessageStats,
|
||||
)
|
||||
|
||||
# Re-export enums from models for backward compatibility
|
||||
from app.modules.messaging.models.message import ConversationType, ParticipantType
|
||||
|
||||
__all__ = [
|
||||
# Attachment schemas
|
||||
"AttachmentResponse",
|
||||
# Message schemas
|
||||
"MessageCreate",
|
||||
"MessageResponse",
|
||||
# Participant schemas
|
||||
"ParticipantInfo",
|
||||
"ParticipantResponse",
|
||||
# Conversation schemas
|
||||
"ConversationCreate",
|
||||
"ConversationSummary",
|
||||
"ConversationDetailResponse",
|
||||
"ConversationListResponse",
|
||||
"ConversationResponse",
|
||||
# Unread count
|
||||
"UnreadCountResponse",
|
||||
# Notification preferences
|
||||
"NotificationPreferencesUpdate",
|
||||
# Conversation actions
|
||||
"CloseConversationResponse",
|
||||
"ReopenConversationResponse",
|
||||
"MarkReadResponse",
|
||||
# Recipient selection
|
||||
"RecipientOption",
|
||||
"RecipientListResponse",
|
||||
# Admin schemas
|
||||
"AdminConversationSummary",
|
||||
"AdminConversationListResponse",
|
||||
"AdminMessageStats",
|
||||
# Enums
|
||||
"ConversationType",
|
||||
"ParticipantType",
|
||||
]
|
||||
|
||||
@@ -1,152 +1,53 @@
|
||||
# models/schema/notification.py
|
||||
"""
|
||||
Notification Pydantic schemas for API validation and responses.
|
||||
LEGACY LOCATION - Re-exports from module for backwards compatibility.
|
||||
|
||||
This module provides schemas for:
|
||||
- Vendor notifications (list, read, delete)
|
||||
- Notification settings management
|
||||
- Notification email templates
|
||||
- Unread counts and statistics
|
||||
The canonical implementation is now in:
|
||||
app/modules/messaging/schemas/notification.py
|
||||
|
||||
This file exists to maintain backwards compatibility with code that
|
||||
imports from the old location. All new code should import directly
|
||||
from the module:
|
||||
|
||||
from app.modules.messaging.schemas import notification
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from app.modules.messaging.schemas.notification import (
|
||||
# Response schemas
|
||||
MessageResponse,
|
||||
UnreadCountResponse,
|
||||
# Notification schemas
|
||||
NotificationResponse,
|
||||
NotificationListResponse,
|
||||
# Settings schemas
|
||||
NotificationSettingsResponse,
|
||||
NotificationSettingsUpdate,
|
||||
# Template schemas
|
||||
NotificationTemplateResponse,
|
||||
NotificationTemplateListResponse,
|
||||
NotificationTemplateUpdate,
|
||||
# Test notification
|
||||
TestNotificationRequest,
|
||||
# Alert statistics
|
||||
AlertStatisticsResponse,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# ============================================================================
|
||||
# SHARED RESPONSE SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
"""Generic message response for simple operations."""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
class UnreadCountResponse(BaseModel):
|
||||
"""Response for unread notification count."""
|
||||
|
||||
unread_count: int
|
||||
message: str | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# NOTIFICATION SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class NotificationResponse(BaseModel):
|
||||
"""Single notification response."""
|
||||
|
||||
id: int
|
||||
type: str
|
||||
title: str
|
||||
message: str
|
||||
is_read: bool
|
||||
read_at: datetime | None = None
|
||||
priority: str = "normal"
|
||||
action_url: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class NotificationListResponse(BaseModel):
|
||||
"""Paginated list of notifications."""
|
||||
|
||||
notifications: list[NotificationResponse] = []
|
||||
total: int = 0
|
||||
unread_count: int = 0
|
||||
message: str | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# NOTIFICATION SETTINGS SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class NotificationSettingsResponse(BaseModel):
|
||||
"""Notification preferences response."""
|
||||
|
||||
email_notifications: bool = True
|
||||
in_app_notifications: bool = True
|
||||
notification_types: dict[str, bool] = Field(default_factory=dict)
|
||||
message: str | None = None
|
||||
|
||||
|
||||
class NotificationSettingsUpdate(BaseModel):
|
||||
"""Request model for updating notification settings."""
|
||||
|
||||
email_notifications: bool | None = None
|
||||
in_app_notifications: bool | None = None
|
||||
notification_types: dict[str, bool] | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# NOTIFICATION TEMPLATE SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class NotificationTemplateResponse(BaseModel):
|
||||
"""Single notification template response."""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
type: str
|
||||
subject: str
|
||||
body_html: str | None = None
|
||||
body_text: str | None = None
|
||||
variables: list[str] = Field(default_factory=list)
|
||||
is_active: bool = True
|
||||
created_at: datetime
|
||||
updated_at: datetime | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class NotificationTemplateListResponse(BaseModel):
|
||||
"""List of notification templates."""
|
||||
|
||||
templates: list[NotificationTemplateResponse] = []
|
||||
message: str | None = None
|
||||
|
||||
|
||||
class NotificationTemplateUpdate(BaseModel):
|
||||
"""Request model for updating notification template."""
|
||||
|
||||
subject: str | None = Field(None, max_length=200)
|
||||
body_html: str | None = None
|
||||
body_text: str | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TEST NOTIFICATION SCHEMA
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestNotificationRequest(BaseModel):
|
||||
"""Request model for sending test notification."""
|
||||
|
||||
template_id: int | None = Field(None, description="Template to use")
|
||||
email: str | None = Field(None, description="Override recipient email")
|
||||
notification_type: str = Field(
|
||||
default="test", description="Type of notification to send"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ADMIN ALERT STATISTICS SCHEMA
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AlertStatisticsResponse(BaseModel):
|
||||
"""Response for alert statistics."""
|
||||
|
||||
total_alerts: int = 0
|
||||
active_alerts: int = 0
|
||||
critical_alerts: int = 0
|
||||
resolved_today: int = 0
|
||||
__all__ = [
|
||||
# Response schemas
|
||||
"MessageResponse",
|
||||
"UnreadCountResponse",
|
||||
# Notification schemas
|
||||
"NotificationResponse",
|
||||
"NotificationListResponse",
|
||||
# Settings schemas
|
||||
"NotificationSettingsResponse",
|
||||
"NotificationSettingsUpdate",
|
||||
# Template schemas
|
||||
"NotificationTemplateResponse",
|
||||
"NotificationTemplateListResponse",
|
||||
"NotificationTemplateUpdate",
|
||||
# Test notification
|
||||
"TestNotificationRequest",
|
||||
# Alert statistics
|
||||
"AlertStatisticsResponse",
|
||||
]
|
||||
|
||||
@@ -1,584 +1,89 @@
|
||||
# models/schema/order.py
|
||||
"""
|
||||
Pydantic schemas for unified order operations.
|
||||
LEGACY LOCATION - Re-exports from module for backwards compatibility.
|
||||
|
||||
Supports both direct orders and marketplace orders (Letzshop, etc.)
|
||||
with snapshotted customer and address data.
|
||||
The canonical implementation is now in:
|
||||
app/modules/orders/schemas/order.py
|
||||
|
||||
This file exists to maintain backwards compatibility with code that
|
||||
imports from the old location. All new code should import directly
|
||||
from the module:
|
||||
|
||||
from app.modules.orders.schemas import order
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
# ============================================================================
|
||||
# Address Snapshot Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AddressSnapshot(BaseModel):
|
||||
"""Address snapshot for order creation."""
|
||||
|
||||
first_name: str = Field(..., min_length=1, max_length=100)
|
||||
last_name: str = Field(..., min_length=1, max_length=100)
|
||||
company: str | None = Field(None, max_length=200)
|
||||
address_line_1: str = Field(..., min_length=1, max_length=255)
|
||||
address_line_2: str | None = Field(None, max_length=255)
|
||||
city: str = Field(..., min_length=1, max_length=100)
|
||||
postal_code: str = Field(..., min_length=1, max_length=20)
|
||||
country_iso: str = Field(..., min_length=2, max_length=5)
|
||||
|
||||
|
||||
class AddressSnapshotResponse(BaseModel):
|
||||
"""Address snapshot in order response."""
|
||||
|
||||
first_name: str
|
||||
last_name: str
|
||||
company: str | None
|
||||
address_line_1: str
|
||||
address_line_2: str | None
|
||||
city: str
|
||||
postal_code: str
|
||||
country_iso: str
|
||||
|
||||
@property
|
||||
def full_name(self) -> str:
|
||||
return f"{self.first_name} {self.last_name}".strip()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Order Item Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class OrderItemCreate(BaseModel):
|
||||
"""Schema for creating an order item."""
|
||||
|
||||
product_id: int
|
||||
quantity: int = Field(..., ge=1)
|
||||
|
||||
|
||||
class OrderItemExceptionBrief(BaseModel):
|
||||
"""Brief exception info for embedding in order item responses."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
original_gtin: str | None
|
||||
original_product_name: str | None
|
||||
exception_type: str
|
||||
status: str
|
||||
resolved_product_id: int | None
|
||||
|
||||
|
||||
class OrderItemResponse(BaseModel):
|
||||
"""Schema for order item response."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
order_id: int
|
||||
product_id: int
|
||||
product_name: str
|
||||
product_sku: str | None
|
||||
gtin: str | None
|
||||
gtin_type: str | None
|
||||
quantity: int
|
||||
unit_price: float
|
||||
total_price: float
|
||||
|
||||
# External references (for marketplace items)
|
||||
external_item_id: str | None = None
|
||||
external_variant_id: str | None = None
|
||||
|
||||
# Item state (for marketplace confirmation flow)
|
||||
item_state: str | None = None
|
||||
|
||||
# Inventory tracking
|
||||
inventory_reserved: bool
|
||||
inventory_fulfilled: bool
|
||||
|
||||
# Exception tracking
|
||||
needs_product_match: bool = False
|
||||
exception: OrderItemExceptionBrief | None = None
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@property
|
||||
def is_confirmed(self) -> bool:
|
||||
"""Check if item has been confirmed (available or unavailable)."""
|
||||
return self.item_state in ("confirmed_available", "confirmed_unavailable")
|
||||
|
||||
@property
|
||||
def is_available(self) -> bool:
|
||||
"""Check if item is confirmed as available."""
|
||||
return self.item_state == "confirmed_available"
|
||||
|
||||
@property
|
||||
def is_declined(self) -> bool:
|
||||
"""Check if item was declined (unavailable)."""
|
||||
return self.item_state == "confirmed_unavailable"
|
||||
|
||||
@property
|
||||
def has_unresolved_exception(self) -> bool:
|
||||
"""Check if item has an unresolved exception blocking confirmation."""
|
||||
if not self.exception:
|
||||
return False
|
||||
return self.exception.status in ("pending", "ignored")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Customer Snapshot Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class CustomerSnapshot(BaseModel):
|
||||
"""Customer snapshot for order creation."""
|
||||
|
||||
first_name: str = Field(..., min_length=1, max_length=100)
|
||||
last_name: str = Field(..., min_length=1, max_length=100)
|
||||
email: str = Field(..., max_length=255)
|
||||
phone: str | None = Field(None, max_length=50)
|
||||
locale: str | None = Field(None, max_length=10)
|
||||
|
||||
|
||||
class CustomerSnapshotResponse(BaseModel):
|
||||
"""Customer snapshot in order response."""
|
||||
|
||||
first_name: str
|
||||
last_name: str
|
||||
email: str
|
||||
phone: str | None
|
||||
locale: str | None
|
||||
|
||||
@property
|
||||
def full_name(self) -> str:
|
||||
return f"{self.first_name} {self.last_name}".strip()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Order Create/Update Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class OrderCreate(BaseModel):
|
||||
"""Schema for creating an order (direct channel)."""
|
||||
|
||||
customer_id: int | None = None # Optional for guest checkout
|
||||
items: list[OrderItemCreate] = Field(..., min_length=1)
|
||||
|
||||
# Customer info snapshot
|
||||
customer: CustomerSnapshot
|
||||
|
||||
# Addresses (snapshots)
|
||||
shipping_address: AddressSnapshot
|
||||
billing_address: AddressSnapshot | None = None # Use shipping if not provided
|
||||
|
||||
# Optional fields
|
||||
shipping_method: str | None = None
|
||||
customer_notes: str | None = Field(None, max_length=1000)
|
||||
|
||||
# Cart/session info
|
||||
session_id: str | None = None
|
||||
|
||||
|
||||
class OrderUpdate(BaseModel):
|
||||
"""Schema for updating order status."""
|
||||
|
||||
status: str | None = Field(
|
||||
None, pattern="^(pending|processing|shipped|delivered|cancelled|refunded)$"
|
||||
)
|
||||
tracking_number: str | None = None
|
||||
tracking_provider: str | None = None
|
||||
internal_notes: str | None = None
|
||||
|
||||
|
||||
class OrderTrackingUpdate(BaseModel):
|
||||
"""Schema for setting tracking information."""
|
||||
|
||||
tracking_number: str = Field(..., min_length=1, max_length=100)
|
||||
tracking_provider: str = Field(..., min_length=1, max_length=100)
|
||||
|
||||
|
||||
class OrderItemStateUpdate(BaseModel):
|
||||
"""Schema for updating item state (marketplace confirmation)."""
|
||||
|
||||
item_id: int
|
||||
state: str = Field(..., pattern="^(confirmed_available|confirmed_unavailable)$")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Order Response Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class OrderResponse(BaseModel):
|
||||
"""Schema for order response."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
customer_id: int
|
||||
order_number: str
|
||||
|
||||
# Channel/Source
|
||||
channel: str
|
||||
external_order_id: str | None = None
|
||||
external_shipment_id: str | None = None
|
||||
external_order_number: str | None = None
|
||||
|
||||
# Status
|
||||
status: str
|
||||
|
||||
# Financial
|
||||
subtotal: float | None
|
||||
tax_amount: float | None
|
||||
shipping_amount: float | None
|
||||
discount_amount: float | None
|
||||
total_amount: float
|
||||
currency: str
|
||||
|
||||
# VAT information
|
||||
vat_regime: str | None = None
|
||||
vat_rate: float | None = None
|
||||
vat_rate_label: str | None = None
|
||||
vat_destination_country: str | None = None
|
||||
|
||||
# Customer snapshot
|
||||
customer_first_name: str
|
||||
customer_last_name: str
|
||||
customer_email: str
|
||||
customer_phone: str | None
|
||||
customer_locale: str | None
|
||||
|
||||
# Shipping address snapshot
|
||||
ship_first_name: str
|
||||
ship_last_name: str
|
||||
ship_company: str | None
|
||||
ship_address_line_1: str
|
||||
ship_address_line_2: str | None
|
||||
ship_city: str
|
||||
ship_postal_code: str
|
||||
ship_country_iso: str
|
||||
|
||||
# Billing address snapshot
|
||||
bill_first_name: str
|
||||
bill_last_name: str
|
||||
bill_company: str | None
|
||||
bill_address_line_1: str
|
||||
bill_address_line_2: str | None
|
||||
bill_city: str
|
||||
bill_postal_code: str
|
||||
bill_country_iso: str
|
||||
|
||||
# Tracking
|
||||
shipping_method: str | None
|
||||
tracking_number: str | None
|
||||
tracking_provider: str | None
|
||||
tracking_url: str | None = None
|
||||
shipment_number: str | None = None
|
||||
shipping_carrier: str | None = None
|
||||
|
||||
# Notes
|
||||
customer_notes: str | None
|
||||
internal_notes: str | None
|
||||
|
||||
# Timestamps
|
||||
order_date: datetime
|
||||
confirmed_at: datetime | None
|
||||
shipped_at: datetime | None
|
||||
delivered_at: datetime | None
|
||||
cancelled_at: datetime | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@property
|
||||
def customer_full_name(self) -> str:
|
||||
return f"{self.customer_first_name} {self.customer_last_name}".strip()
|
||||
|
||||
@property
|
||||
def ship_full_name(self) -> str:
|
||||
return f"{self.ship_first_name} {self.ship_last_name}".strip()
|
||||
|
||||
@property
|
||||
def is_marketplace_order(self) -> bool:
|
||||
return self.channel != "direct"
|
||||
|
||||
|
||||
class OrderDetailResponse(OrderResponse):
|
||||
"""Schema for detailed order response with items."""
|
||||
|
||||
items: list[OrderItemResponse] = []
|
||||
|
||||
# Vendor info (enriched by API)
|
||||
vendor_name: str | None = None
|
||||
vendor_code: str | None = None
|
||||
|
||||
|
||||
class OrderListResponse(BaseModel):
|
||||
"""Schema for paginated order list."""
|
||||
|
||||
orders: list[OrderResponse]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Order List Item (Simplified for list views)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class OrderListItem(BaseModel):
|
||||
"""Simplified order item for list views."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
order_number: str
|
||||
channel: str
|
||||
status: str
|
||||
|
||||
# External references
|
||||
external_order_number: str | None = None
|
||||
|
||||
# Customer
|
||||
customer_full_name: str
|
||||
customer_email: str
|
||||
|
||||
# Financial
|
||||
total_amount: float
|
||||
currency: str
|
||||
|
||||
# Shipping
|
||||
ship_country_iso: str
|
||||
|
||||
# Tracking
|
||||
tracking_number: str | None
|
||||
tracking_provider: str | None
|
||||
tracking_url: str | None = None
|
||||
shipment_number: str | None = None
|
||||
shipping_carrier: str | None = None
|
||||
|
||||
# Item count
|
||||
item_count: int = 0
|
||||
|
||||
# Timestamps
|
||||
order_date: datetime
|
||||
confirmed_at: datetime | None
|
||||
shipped_at: datetime | None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Admin Order Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AdminOrderItem(BaseModel):
|
||||
"""Order item with vendor info for admin list view."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
vendor_name: str | None = None
|
||||
vendor_code: str | None = None
|
||||
customer_id: int
|
||||
order_number: str
|
||||
channel: str
|
||||
status: str
|
||||
|
||||
# External references
|
||||
external_order_number: str | None = None
|
||||
external_shipment_id: str | None = None
|
||||
|
||||
# Customer snapshot
|
||||
customer_full_name: str
|
||||
customer_email: str
|
||||
|
||||
# Financial
|
||||
subtotal: float | None
|
||||
tax_amount: float | None
|
||||
shipping_amount: float | None
|
||||
discount_amount: float | None
|
||||
total_amount: float
|
||||
currency: str
|
||||
|
||||
# VAT information
|
||||
vat_regime: str | None = None
|
||||
vat_rate: float | None = None
|
||||
vat_rate_label: str | None = None
|
||||
vat_destination_country: str | None = None
|
||||
|
||||
# Shipping
|
||||
ship_country_iso: str
|
||||
tracking_number: str | None
|
||||
tracking_provider: str | None
|
||||
tracking_url: str | None = None
|
||||
shipment_number: str | None = None
|
||||
shipping_carrier: str | None = None
|
||||
|
||||
# Item count
|
||||
item_count: int = 0
|
||||
|
||||
# Timestamps
|
||||
order_date: datetime
|
||||
confirmed_at: datetime | None
|
||||
shipped_at: datetime | None
|
||||
delivered_at: datetime | None
|
||||
cancelled_at: datetime | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class AdminOrderListResponse(BaseModel):
|
||||
"""Cross-vendor order list for admin."""
|
||||
|
||||
orders: list[AdminOrderItem]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
class AdminOrderStats(BaseModel):
|
||||
"""Order statistics for admin dashboard."""
|
||||
|
||||
total_orders: int = 0
|
||||
pending_orders: int = 0
|
||||
processing_orders: int = 0
|
||||
shipped_orders: int = 0
|
||||
delivered_orders: int = 0
|
||||
cancelled_orders: int = 0
|
||||
refunded_orders: int = 0
|
||||
total_revenue: float = 0.0
|
||||
|
||||
# By channel
|
||||
direct_orders: int = 0
|
||||
letzshop_orders: int = 0
|
||||
|
||||
# Vendors
|
||||
vendors_with_orders: int = 0
|
||||
|
||||
|
||||
class AdminOrderStatusUpdate(BaseModel):
|
||||
"""Admin version of status update with reason."""
|
||||
|
||||
status: str = Field(
|
||||
..., pattern="^(pending|processing|shipped|delivered|cancelled|refunded)$"
|
||||
)
|
||||
tracking_number: str | None = None
|
||||
tracking_provider: str | None = None
|
||||
reason: str | None = Field(None, description="Reason for status change")
|
||||
|
||||
|
||||
class AdminVendorWithOrders(BaseModel):
|
||||
"""Vendor with order count."""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
vendor_code: str
|
||||
order_count: int = 0
|
||||
|
||||
|
||||
class AdminVendorsWithOrdersResponse(BaseModel):
|
||||
"""Response for vendors with orders list."""
|
||||
|
||||
vendors: list[AdminVendorWithOrders]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Letzshop-specific Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class LetzshopOrderImport(BaseModel):
|
||||
"""Schema for importing a Letzshop order from shipment data."""
|
||||
|
||||
shipment_id: str
|
||||
order_id: str
|
||||
order_number: str
|
||||
order_date: datetime
|
||||
|
||||
# Customer
|
||||
customer_email: str
|
||||
customer_locale: str | None = None
|
||||
|
||||
# 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
|
||||
ship_country_iso: str
|
||||
|
||||
# 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
|
||||
bill_country_iso: str
|
||||
|
||||
# Totals
|
||||
total_amount: float
|
||||
currency: str = "EUR"
|
||||
|
||||
# State
|
||||
letzshop_state: str # unconfirmed, confirmed, declined
|
||||
|
||||
# Items
|
||||
inventory_units: list[dict]
|
||||
|
||||
# Raw data
|
||||
raw_data: dict | None = None
|
||||
|
||||
|
||||
class LetzshopShippingInfo(BaseModel):
|
||||
"""Shipping info retrieved from Letzshop."""
|
||||
|
||||
tracking_number: str
|
||||
tracking_provider: str
|
||||
shipment_id: str
|
||||
|
||||
|
||||
class LetzshopOrderConfirmItem(BaseModel):
|
||||
"""Schema for confirming/declining a single item."""
|
||||
|
||||
item_id: int
|
||||
external_item_id: str
|
||||
action: str = Field(..., pattern="^(confirm|decline)$")
|
||||
|
||||
|
||||
class LetzshopOrderConfirmRequest(BaseModel):
|
||||
"""Schema for confirming/declining order items."""
|
||||
|
||||
items: list[LetzshopOrderConfirmItem]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Mark as Shipped Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MarkAsShippedRequest(BaseModel):
|
||||
"""Schema for marking an order as shipped with tracking info."""
|
||||
|
||||
tracking_number: str | None = Field(None, max_length=100)
|
||||
tracking_url: str | None = Field(None, max_length=500)
|
||||
shipping_carrier: str | None = Field(None, max_length=50)
|
||||
|
||||
|
||||
class ShippingLabelInfo(BaseModel):
|
||||
"""Shipping label information for an order."""
|
||||
|
||||
shipment_number: str | None = None
|
||||
shipping_carrier: str | None = None
|
||||
label_url: str | None = None
|
||||
tracking_number: str | None = None
|
||||
tracking_url: str | None = None
|
||||
from app.modules.orders.schemas.order import (
|
||||
# Address schemas
|
||||
AddressSnapshot,
|
||||
AddressSnapshotResponse,
|
||||
# Order item schemas
|
||||
OrderItemCreate,
|
||||
OrderItemExceptionBrief,
|
||||
OrderItemResponse,
|
||||
# Customer schemas
|
||||
CustomerSnapshot,
|
||||
CustomerSnapshotResponse,
|
||||
# Order CRUD schemas
|
||||
OrderCreate,
|
||||
OrderUpdate,
|
||||
OrderTrackingUpdate,
|
||||
OrderItemStateUpdate,
|
||||
# Order response schemas
|
||||
OrderResponse,
|
||||
OrderDetailResponse,
|
||||
OrderListResponse,
|
||||
OrderListItem,
|
||||
# Admin schemas
|
||||
AdminOrderItem,
|
||||
AdminOrderListResponse,
|
||||
AdminOrderStats,
|
||||
AdminOrderStatusUpdate,
|
||||
AdminVendorWithOrders,
|
||||
AdminVendorsWithOrdersResponse,
|
||||
# Letzshop schemas
|
||||
LetzshopOrderImport,
|
||||
LetzshopShippingInfo,
|
||||
LetzshopOrderConfirmItem,
|
||||
LetzshopOrderConfirmRequest,
|
||||
# Shipping schemas
|
||||
MarkAsShippedRequest,
|
||||
ShippingLabelInfo,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Address schemas
|
||||
"AddressSnapshot",
|
||||
"AddressSnapshotResponse",
|
||||
# Order item schemas
|
||||
"OrderItemCreate",
|
||||
"OrderItemExceptionBrief",
|
||||
"OrderItemResponse",
|
||||
# Customer schemas
|
||||
"CustomerSnapshot",
|
||||
"CustomerSnapshotResponse",
|
||||
# Order CRUD schemas
|
||||
"OrderCreate",
|
||||
"OrderUpdate",
|
||||
"OrderTrackingUpdate",
|
||||
"OrderItemStateUpdate",
|
||||
# Order response schemas
|
||||
"OrderResponse",
|
||||
"OrderDetailResponse",
|
||||
"OrderListResponse",
|
||||
"OrderListItem",
|
||||
# Admin schemas
|
||||
"AdminOrderItem",
|
||||
"AdminOrderListResponse",
|
||||
"AdminOrderStats",
|
||||
"AdminOrderStatusUpdate",
|
||||
"AdminVendorWithOrders",
|
||||
"AdminVendorsWithOrdersResponse",
|
||||
# Letzshop schemas
|
||||
"LetzshopOrderImport",
|
||||
"LetzshopShippingInfo",
|
||||
"LetzshopOrderConfirmItem",
|
||||
"LetzshopOrderConfirmRequest",
|
||||
# Shipping schemas
|
||||
"MarkAsShippedRequest",
|
||||
"ShippingLabelInfo",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user