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:
2026-01-29 21:28:56 +01:00
parent b5a803cde8
commit de83875d0a
99 changed files with 19413 additions and 15357 deletions

View File

@@ -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",
]

View File

@@ -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",
]

View File

@@ -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",
]

View File

@@ -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",
]

View File

@@ -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",
]

View File

@@ -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",
]