refactor: migrate schemas to canonical module locations

Migrate remaining legacy schemas to their respective modules:

Marketplace module (app/modules/marketplace/schemas/):
- letzshop.py: Letzshop credentials, orders, fulfillment, sync
- onboarding.py: Vendor onboarding wizard schemas

Catalog module (app/modules/catalog/schemas/):
- product.py: ProductCreate, ProductUpdate, ProductResponse

Payments module (app/modules/payments/schemas/):
- payment.py: PaymentConfig, Stripe, transactions, balance

Delete legacy files:
- models/schema/letzshop.py
- models/schema/onboarding.py
- models/schema/product.py
- models/schema/payment.py
- models/schema/marketplace_product.py (re-export)
- models/schema/marketplace_import_job.py (re-export)
- models/schema/search.py (empty)

Update imports across 19 files to use canonical locations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 15:16:49 +01:00
parent 0c63f387aa
commit 1ef50893a1
31 changed files with 255 additions and 125 deletions

View File

@@ -2,13 +2,31 @@
"""Catalog module schemas."""
from app.modules.catalog.schemas.catalog import (
ProductDetailResponse as CatalogProductDetailResponse,
ProductListResponse as CatalogProductListResponse,
ProductResponse as CatalogProductResponse,
)
from app.modules.catalog.schemas.product import (
ProductCreate,
ProductUpdate,
ProductResponse,
ProductDetailResponse,
ProductListResponse,
ProductResponse,
ProductDeleteResponse,
ProductToggleResponse,
)
__all__ = [
# Catalog browsing schemas (storefront)
"CatalogProductResponse",
"CatalogProductDetailResponse",
"CatalogProductListResponse",
# Product CRUD schemas (vendor management)
"ProductCreate",
"ProductUpdate",
"ProductResponse",
"ProductDetailResponse",
"ProductListResponse",
"ProductDeleteResponse",
"ProductToggleResponse",
]

View File

@@ -11,7 +11,7 @@ from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
from app.modules.inventory.schemas import InventoryLocationResponse
from models.schema.marketplace_product import MarketplaceProductResponse
from app.modules.marketplace.schemas import MarketplaceProductResponse
class ProductResponse(BaseModel):

View File

@@ -0,0 +1,94 @@
# app/modules/catalog/schemas/product.py
"""
Pydantic schemas for Product CRUD operations.
These schemas are used for vendor product catalog management,
linking vendor products to marketplace products.
"""
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
from app.modules.inventory.schemas import InventoryLocationResponse
from app.modules.marketplace.schemas import MarketplaceProductResponse
class ProductCreate(BaseModel):
marketplace_product_id: int = Field(
..., description="MarketplaceProduct ID to add to vendor catalog"
)
vendor_sku: str | None = Field(None, description="Vendor's internal SKU")
price: float | None = Field(None, ge=0)
sale_price: float | None = Field(None, ge=0)
currency: str | None = None
availability: str | None = None
condition: str | None = None
is_featured: bool = False
min_quantity: int = Field(1, ge=1)
max_quantity: int | None = Field(None, ge=1)
class ProductUpdate(BaseModel):
vendor_sku: str | None = None
price: float | None = Field(None, ge=0)
sale_price: float | None = Field(None, ge=0)
currency: str | None = None
availability: str | None = None
condition: str | None = None
is_featured: bool | None = None
is_active: bool | None = None
min_quantity: int | None = Field(None, ge=1)
max_quantity: int | None = Field(None, ge=1)
class ProductResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
vendor_id: int
marketplace_product: MarketplaceProductResponse
vendor_sku: str | None
price: float | None
sale_price: float | None
currency: str | None
availability: str | None
condition: str | None
is_featured: bool
is_active: bool
display_order: int
min_quantity: int
max_quantity: int | None
created_at: datetime
updated_at: datetime
# Include inventory summary
total_inventory: int | None = None
available_inventory: int | None = None
class ProductDetailResponse(ProductResponse):
"""Product with full inventory details."""
inventory_locations: list[InventoryLocationResponse] = []
class ProductListResponse(BaseModel):
products: list[ProductResponse]
total: int
skip: int
limit: int
class ProductDeleteResponse(BaseModel):
"""Response for product deletion."""
message: str
class ProductToggleResponse(BaseModel):
"""Response for product toggle operations (active/featured)."""
message: str
is_active: bool | None = None
is_featured: bool | None = None

View File

@@ -39,6 +39,83 @@ from app.modules.marketplace.schemas.marketplace_product import (
MarketplaceImportRequest,
MarketplaceImportResponse,
)
from app.modules.marketplace.schemas.letzshop import (
# Credentials
LetzshopCredentialsCreate,
LetzshopCredentialsUpdate,
LetzshopCredentialsResponse,
LetzshopCredentialsStatus,
# Orders
LetzshopOrderItemResponse,
LetzshopOrderResponse,
LetzshopOrderDetailResponse,
LetzshopOrderStats,
LetzshopOrderListResponse,
# Fulfillment
FulfillmentConfirmRequest,
FulfillmentRejectRequest,
FulfillmentTrackingRequest,
FulfillmentQueueItemResponse,
FulfillmentQueueListResponse,
FulfillmentOperationResponse,
# Sync
LetzshopSyncLogResponse,
LetzshopSyncLogListResponse,
LetzshopSyncTriggerRequest,
LetzshopSyncTriggerResponse,
# Connection
LetzshopConnectionTestRequest,
LetzshopConnectionTestResponse,
LetzshopSuccessResponse,
# Admin
LetzshopVendorOverview,
LetzshopVendorListResponse,
# Jobs
LetzshopJobItem,
LetzshopJobsListResponse,
# Historical Import
LetzshopHistoricalImportJobResponse,
LetzshopHistoricalImportStartResponse,
# Vendor Directory
LetzshopCachedVendorItem,
LetzshopCachedVendorDetail,
LetzshopVendorDirectoryStats,
LetzshopVendorDirectoryStatsResponse,
LetzshopCachedVendorListResponse,
LetzshopCachedVendorDetailResponse,
LetzshopVendorDirectorySyncResponse,
LetzshopCreateVendorFromCacheResponse,
)
from app.modules.marketplace.schemas.onboarding import (
# Step status
StepStatus,
CompanyProfileStepStatus,
LetzshopApiStepStatus,
ProductImportStepStatus,
OrderSyncStepStatus,
# Main status
OnboardingStatusResponse,
# Step 1
CompanyProfileRequest,
CompanyProfileResponse,
# Step 2
LetzshopApiConfigRequest,
LetzshopApiTestRequest,
LetzshopApiTestResponse,
LetzshopApiConfigResponse,
# Step 3
ProductImportConfigRequest,
ProductImportConfigResponse,
# Step 4
OrderSyncTriggerRequest,
OrderSyncTriggerResponse,
OrderSyncProgressResponse,
OrderSyncCompleteRequest,
OrderSyncCompleteResponse,
# Admin
OnboardingSkipRequest,
OnboardingSkipResponse,
)
__all__ = [
# Import job schemas
@@ -62,4 +139,77 @@ __all__ = [
# Import schemas
"MarketplaceImportRequest",
"MarketplaceImportResponse",
# Letzshop - Credentials
"LetzshopCredentialsCreate",
"LetzshopCredentialsUpdate",
"LetzshopCredentialsResponse",
"LetzshopCredentialsStatus",
# Letzshop - Orders
"LetzshopOrderItemResponse",
"LetzshopOrderResponse",
"LetzshopOrderDetailResponse",
"LetzshopOrderStats",
"LetzshopOrderListResponse",
# Letzshop - Fulfillment
"FulfillmentConfirmRequest",
"FulfillmentRejectRequest",
"FulfillmentTrackingRequest",
"FulfillmentQueueItemResponse",
"FulfillmentQueueListResponse",
"FulfillmentOperationResponse",
# Letzshop - Sync
"LetzshopSyncLogResponse",
"LetzshopSyncLogListResponse",
"LetzshopSyncTriggerRequest",
"LetzshopSyncTriggerResponse",
# Letzshop - Connection
"LetzshopConnectionTestRequest",
"LetzshopConnectionTestResponse",
"LetzshopSuccessResponse",
# Letzshop - Admin
"LetzshopVendorOverview",
"LetzshopVendorListResponse",
# Letzshop - Jobs
"LetzshopJobItem",
"LetzshopJobsListResponse",
# Letzshop - Historical Import
"LetzshopHistoricalImportJobResponse",
"LetzshopHistoricalImportStartResponse",
# Letzshop - Vendor Directory
"LetzshopCachedVendorItem",
"LetzshopCachedVendorDetail",
"LetzshopVendorDirectoryStats",
"LetzshopVendorDirectoryStatsResponse",
"LetzshopCachedVendorListResponse",
"LetzshopCachedVendorDetailResponse",
"LetzshopVendorDirectorySyncResponse",
"LetzshopCreateVendorFromCacheResponse",
# Onboarding - Step status
"StepStatus",
"CompanyProfileStepStatus",
"LetzshopApiStepStatus",
"ProductImportStepStatus",
"OrderSyncStepStatus",
# Onboarding - Main status
"OnboardingStatusResponse",
# Onboarding - Step 1
"CompanyProfileRequest",
"CompanyProfileResponse",
# Onboarding - Step 2
"LetzshopApiConfigRequest",
"LetzshopApiTestRequest",
"LetzshopApiTestResponse",
"LetzshopApiConfigResponse",
# Onboarding - Step 3
"ProductImportConfigRequest",
"ProductImportConfigResponse",
# Onboarding - Step 4
"OrderSyncTriggerRequest",
"OrderSyncTriggerResponse",
"OrderSyncProgressResponse",
"OrderSyncCompleteRequest",
"OrderSyncCompleteResponse",
# Onboarding - Admin
"OnboardingSkipRequest",
"OnboardingSkipResponse",
]

View File

@@ -0,0 +1,626 @@
# app/modules/marketplace/schemas/letzshop.py
"""
Pydantic schemas for Letzshop marketplace integration.
Covers:
- Vendor credentials management
- Letzshop order import/sync
- Fulfillment queue operations
- Sync logs
"""
from datetime import datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
# ============================================================================
# Credentials Schemas
# ============================================================================
class LetzshopCredentialsCreate(BaseModel):
"""Schema for creating/updating Letzshop credentials."""
api_key: str = Field(..., min_length=1, description="Letzshop API key")
api_endpoint: str | None = Field(
None,
description="Custom API endpoint (defaults to https://letzshop.lu/graphql)",
)
auto_sync_enabled: bool = Field(False, description="Enable automatic order sync")
sync_interval_minutes: int = Field(
15, ge=5, le=1440, description="Sync interval in minutes (5-1440)"
)
test_mode_enabled: bool = Field(
False, description="Test mode - disables API mutations"
)
default_carrier: str | None = Field(
None, description="Default carrier (greco, colissimo, xpresslogistics)"
)
carrier_greco_label_url: str | None = Field(
"https://dispatchweb.fr/Tracky/Home/", description="Greco label URL prefix"
)
carrier_colissimo_label_url: str | None = Field(
None, description="Colissimo label URL prefix"
)
carrier_xpresslogistics_label_url: str | None = Field(
None, description="XpressLogistics label URL prefix"
)
class LetzshopCredentialsUpdate(BaseModel):
"""Schema for updating Letzshop credentials (partial update)."""
api_key: str | None = Field(None, min_length=1)
api_endpoint: str | None = None
auto_sync_enabled: bool | None = None
sync_interval_minutes: int | None = Field(None, ge=5, le=1440)
test_mode_enabled: bool | None = None
default_carrier: str | None = None
carrier_greco_label_url: str | None = None
carrier_colissimo_label_url: str | None = None
carrier_xpresslogistics_label_url: str | None = None
class LetzshopCredentialsResponse(BaseModel):
"""Schema for Letzshop credentials response (API key is masked)."""
model_config = ConfigDict(from_attributes=True)
id: int
vendor_id: int
api_key_masked: str = Field(..., description="Masked API key for display")
api_endpoint: str
auto_sync_enabled: bool
sync_interval_minutes: int
test_mode_enabled: bool = False
default_carrier: str | None = None
carrier_greco_label_url: str | None = None
carrier_colissimo_label_url: str | None = None
carrier_xpresslogistics_label_url: str | None = None
last_sync_at: datetime | None
last_sync_status: str | None
last_sync_error: str | None
created_at: datetime
updated_at: datetime
class LetzshopCredentialsStatus(BaseModel):
"""Schema for Letzshop connection status."""
is_configured: bool
is_connected: bool
last_sync_at: datetime | None
last_sync_status: str | None
auto_sync_enabled: bool
# ============================================================================
# Letzshop Order Schemas (using unified Order model)
# ============================================================================
class LetzshopOrderItemResponse(BaseModel):
"""Schema for order item in Letzshop order response."""
model_config = ConfigDict(from_attributes=True)
id: int
product_id: int
product_name: str
product_sku: str | None = None
gtin: str | None = None
gtin_type: str | None = None
quantity: int
unit_price: float
total_price: float
external_item_id: str | None = None # Letzshop inventory unit ID
external_variant_id: str | None = None
item_state: str | None = None # confirmed_available, confirmed_unavailable
class LetzshopOrderResponse(BaseModel):
"""Schema for Letzshop order response (from unified Order model)."""
model_config = ConfigDict(from_attributes=True)
id: int
vendor_id: int
vendor_name: str | None = None # For cross-vendor views
order_number: str
# External references
external_order_id: str | None = None
external_shipment_id: str | None = None
external_order_number: str | None = None
# Status
status: str # pending, processing, shipped, delivered, cancelled
# Customer info
customer_email: str
customer_name: str # computed: customer_first_name + customer_last_name
customer_locale: str | None = None
# Address info
ship_country_iso: str
bill_country_iso: str
# Financial
total_amount: float
currency: str = "EUR"
# Tracking
tracking_number: str | None = None
tracking_provider: str | None = None
# Timestamps
order_date: datetime
confirmed_at: datetime | None = None
shipped_at: datetime | None = None
cancelled_at: datetime | None = None
created_at: datetime
updated_at: datetime
# Items (for list view, may be empty)
items: list[LetzshopOrderItemResponse] = Field(default_factory=list)
class LetzshopOrderDetailResponse(LetzshopOrderResponse):
"""Schema for detailed Letzshop order response with all data."""
# Full customer snapshot
customer_first_name: str
customer_last_name: str
customer_phone: str | None = None
# Full shipping address
ship_first_name: str
ship_last_name: str
ship_company: str | None = None
ship_address_line_1: str
ship_address_line_2: str | None = None
ship_city: str
ship_postal_code: str
# Full billing address
bill_first_name: str
bill_last_name: str
bill_company: str | None = None
bill_address_line_1: str
bill_address_line_2: str | None = None
bill_city: str
bill_postal_code: str
# Raw marketplace data
external_data: dict[str, Any] | None = None
# Notes
customer_notes: str | None = None
internal_notes: str | None = None
class LetzshopOrderStats(BaseModel):
"""Schema for order statistics by status."""
pending: int = 0
processing: int = 0
shipped: int = 0
delivered: int = 0
cancelled: int = 0
total: int = 0
has_declined_items: int = 0 # Orders with at least one declined item
class LetzshopOrderListResponse(BaseModel):
"""Schema for paginated Letzshop order list."""
orders: list[LetzshopOrderResponse]
total: int
skip: int
limit: int
stats: LetzshopOrderStats | None = None
# ============================================================================
# Fulfillment Schemas
# ============================================================================
class FulfillmentConfirmRequest(BaseModel):
"""Schema for confirming order fulfillment."""
inventory_unit_ids: list[str] = Field(
..., min_length=1, description="List of inventory unit IDs to confirm"
)
class FulfillmentRejectRequest(BaseModel):
"""Schema for rejecting order fulfillment."""
inventory_unit_ids: list[str] = Field(
..., min_length=1, description="List of inventory unit IDs to reject"
)
reason: str | None = Field(None, max_length=500, description="Rejection reason")
class FulfillmentTrackingRequest(BaseModel):
"""Schema for setting tracking information."""
tracking_number: str = Field(..., min_length=1, max_length=100)
tracking_carrier: str = Field(
..., min_length=1, max_length=100, description="Carrier code (e.g., dhl, ups)"
)
class FulfillmentQueueItemResponse(BaseModel):
"""Schema for fulfillment queue item response."""
model_config = ConfigDict(from_attributes=True)
id: int
vendor_id: int
order_id: int # FK to unified orders table
operation: str
payload: dict[str, Any]
status: str
attempts: int
max_attempts: int
last_attempt_at: datetime | None
next_retry_at: datetime | None
error_message: str | None
completed_at: datetime | None
response_data: dict[str, Any] | None
created_at: datetime
updated_at: datetime
class FulfillmentQueueListResponse(BaseModel):
"""Schema for paginated fulfillment queue list."""
items: list[FulfillmentQueueItemResponse]
total: int
skip: int
limit: int
# ============================================================================
# Sync Log Schemas
# ============================================================================
class LetzshopSyncLogResponse(BaseModel):
"""Schema for Letzshop sync log response."""
model_config = ConfigDict(from_attributes=True)
id: int
vendor_id: int
operation_type: str
direction: str
status: str
records_processed: int
records_succeeded: int
records_failed: int
error_details: dict[str, Any] | None
started_at: datetime
completed_at: datetime | None
duration_seconds: int | None
triggered_by: str | None
created_at: datetime
class LetzshopSyncLogListResponse(BaseModel):
"""Schema for paginated sync log list."""
logs: list[LetzshopSyncLogResponse]
total: int
skip: int
limit: int
# ============================================================================
# Sync Trigger Schemas
# ============================================================================
class LetzshopSyncTriggerRequest(BaseModel):
"""Schema for triggering a sync operation."""
operation: str = Field(
"order_import",
pattern="^(order_import|full_sync)$",
description="Type of sync operation",
)
class LetzshopSyncTriggerResponse(BaseModel):
"""Schema for sync trigger response."""
success: bool
message: str
sync_log_id: int | None = None
orders_imported: int = 0
orders_updated: int = 0
errors: list[str] = Field(default_factory=list)
# ============================================================================
# Connection Test Schemas
# ============================================================================
class LetzshopConnectionTestRequest(BaseModel):
"""Schema for testing Letzshop connection."""
api_key: str = Field(..., min_length=1, description="API key to test")
api_endpoint: str | None = Field(None, description="Custom endpoint to test")
class LetzshopConnectionTestResponse(BaseModel):
"""Schema for connection test response."""
success: bool
message: str
response_time_ms: float | None = None
error_details: str | None = None
# ============================================================================
# Generic Response Schemas
# ============================================================================
class LetzshopSuccessResponse(BaseModel):
"""Generic success response for Letzshop operations."""
success: bool
message: str
class FulfillmentOperationResponse(BaseModel):
"""Response for fulfillment operations (confirm, reject, tracking)."""
success: bool
message: str
confirmed_units: list[str] | None = None
tracking_number: str | None = None
tracking_carrier: str | None = None
errors: list[str] | None = None
# ============================================================================
# Admin Overview Schemas
# ============================================================================
class LetzshopVendorOverview(BaseModel):
"""Schema for vendor Letzshop integration overview (admin view)."""
vendor_id: int
vendor_name: str
vendor_code: str
is_configured: bool
auto_sync_enabled: bool
last_sync_at: datetime | None
last_sync_status: str | None
pending_orders: int
total_orders: int
class LetzshopVendorListResponse(BaseModel):
"""Schema for paginated vendor Letzshop overview list."""
vendors: list[LetzshopVendorOverview]
total: int
skip: int
limit: int
# ============================================================================
# Jobs Schemas (Unified view of imports, exports, and syncs)
# ============================================================================
class LetzshopJobItem(BaseModel):
"""Schema for a unified job item (import, export, order sync, or historical import)."""
id: int
type: str = Field(
..., description="Job type: import, export, order_sync, or historical_import"
)
status: str = Field(..., description="Job status")
created_at: datetime
started_at: datetime | None = None
completed_at: datetime | None = None
records_processed: int = 0
records_succeeded: int = 0
records_failed: int = 0
# Vendor info
vendor_id: int | None = Field(None, description="Vendor ID")
vendor_name: str | None = Field(None, description="Vendor name")
vendor_code: str | None = Field(None, description="Vendor code")
# Historical import specific fields
current_phase: str | None = Field(
None, description="Current phase for historical imports"
)
error_message: str | None = Field(None, description="Error message if failed")
error_details: dict[str, Any] | None = Field(
None, description="Error details or export file info"
)
class LetzshopJobsListResponse(BaseModel):
"""Schema for paginated jobs list."""
jobs: list[LetzshopJobItem]
total: int
# ============================================================================
# Historical Import Job Schemas
# ============================================================================
class LetzshopHistoricalImportJobResponse(BaseModel):
"""Schema for historical import job status (polling endpoint)."""
model_config = ConfigDict(from_attributes=True)
id: int
vendor_id: int
status: str # pending, fetching, processing, completed, failed
current_phase: str | None = None # "confirmed" or "declined"
# Fetch progress
current_page: int = 0
total_pages: int | None = None
shipments_fetched: int = 0
# Processing progress
orders_processed: int = 0
orders_imported: int = 0
orders_updated: int = 0
orders_skipped: int = 0
# EAN matching stats
products_matched: int = 0
products_not_found: int = 0
# Phase-specific stats (when complete)
confirmed_stats: dict[str, Any] | None = None
declined_stats: dict[str, Any] | None = None
# Error handling
error_message: str | None = None
# Timing
started_at: datetime | None = None
completed_at: datetime | None = None
created_at: datetime
updated_at: datetime
class LetzshopHistoricalImportStartResponse(BaseModel):
"""Schema for starting a historical import job."""
job_id: int
status: str = "pending"
message: str = "Historical import job started"
# ============================================================================
# Vendor Directory Schemas (Letzshop Marketplace Cache)
# ============================================================================
class LetzshopCachedVendorItem(BaseModel):
"""Schema for a cached Letzshop vendor in list view."""
id: int
letzshop_id: str
slug: str
name: str
company_name: str | None = None
email: str | None = None
phone: str | None = None
website: str | None = None
city: str | None = None
categories: list[str] = []
is_active: bool = True
is_claimed: bool = False
claimed_by_vendor_id: int | None = None
last_synced_at: datetime | None = None
letzshop_url: str
class LetzshopCachedVendorDetail(BaseModel):
"""Schema for detailed cached Letzshop vendor."""
id: int
letzshop_id: str
slug: str
name: str
company_name: str | None = None
description_en: str | None = None
description_fr: str | None = None
description_de: str | None = None
email: str | None = None
phone: str | None = None
fax: str | None = None
website: str | None = None
street: str | None = None
street_number: str | None = None
city: str | None = None
zipcode: str | None = None
country_iso: str | None = None
latitude: str | None = None
longitude: str | None = None
categories: list[str] = []
background_image_url: str | None = None
social_media_links: list[str] = []
opening_hours_en: str | None = None
opening_hours_fr: str | None = None
opening_hours_de: str | None = None
representative_name: str | None = None
representative_title: str | None = None
is_active: bool = True
is_claimed: bool = False
claimed_by_vendor_id: int | None = None
claimed_at: datetime | None = None
last_synced_at: datetime | None = None
letzshop_url: str
class LetzshopVendorDirectoryStats(BaseModel):
"""Schema for vendor directory cache statistics."""
total_vendors: int = 0
active_vendors: int = 0
claimed_vendors: int = 0
unclaimed_vendors: int = 0
unique_cities: int = 0
last_synced_at: str | None = None
class LetzshopVendorDirectoryStatsResponse(BaseModel):
"""Response schema for vendor directory stats endpoint."""
success: bool = True
stats: LetzshopVendorDirectoryStats
class LetzshopCachedVendorListResponse(BaseModel):
"""Response schema for vendor directory list endpoint."""
success: bool = True
vendors: list[LetzshopCachedVendorItem]
total: int
page: int
limit: int
has_more: bool
class LetzshopCachedVendorDetailResponse(BaseModel):
"""Response schema for vendor directory detail endpoint."""
success: bool = True
vendor: LetzshopCachedVendorDetail
class LetzshopVendorDirectorySyncResponse(BaseModel):
"""Response schema for vendor directory sync trigger."""
success: bool = True
message: str
task_id: str | None = None
mode: str = "celery"
class LetzshopCreateVendorFromCacheResponse(BaseModel):
"""Response schema for creating vendor from Letzshop cache."""
success: bool = True
message: str
vendor: dict[str, Any] | None = None
letzshop_vendor_slug: str

View File

@@ -0,0 +1,291 @@
# app/modules/marketplace/schemas/onboarding.py
"""
Pydantic schemas for Vendor Onboarding operations.
Schemas include:
- OnboardingStatusResponse: Current onboarding status with all step states
- CompanyProfileRequest/Response: Step 1 - Company profile data
- LetzshopApiConfigRequest/Response: Step 2 - API configuration
- ProductImportConfigRequest/Response: Step 3 - CSV URL configuration
- OrderSyncTriggerResponse: Step 4 - Job trigger response
- OrderSyncProgressResponse: Step 4 - Progress polling response
"""
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
# =============================================================================
# STEP STATUS MODELS
# =============================================================================
class StepStatus(BaseModel):
"""Status for a single onboarding step."""
completed: bool = False
completed_at: datetime | None = None
class CompanyProfileStepStatus(StepStatus):
"""Step 1 status with saved data."""
data: dict | None = None
class LetzshopApiStepStatus(StepStatus):
"""Step 2 status with connection verification."""
connection_verified: bool = False
class ProductImportStepStatus(StepStatus):
"""Step 3 status with CSV URL flag."""
csv_url_set: bool = False
class OrderSyncStepStatus(StepStatus):
"""Step 4 status with job tracking."""
job_id: int | None = None
# =============================================================================
# ONBOARDING STATUS RESPONSE
# =============================================================================
class OnboardingStatusResponse(BaseModel):
"""Full onboarding status with all step information."""
model_config = ConfigDict(from_attributes=True)
id: int
vendor_id: int
status: str # not_started, in_progress, completed, skipped
current_step: str # company_profile, letzshop_api, product_import, order_sync
# Step statuses
company_profile: CompanyProfileStepStatus
letzshop_api: LetzshopApiStepStatus
product_import: ProductImportStepStatus
order_sync: OrderSyncStepStatus
# Progress tracking
completion_percentage: int
completed_steps_count: int
total_steps: int = 4
# Completion info
is_completed: bool
started_at: datetime | None = None
completed_at: datetime | None = None
# Admin override info
skipped_by_admin: bool = False
skipped_at: datetime | None = None
skipped_reason: str | None = None
# =============================================================================
# STEP 1: COMPANY PROFILE
# =============================================================================
class CompanyProfileRequest(BaseModel):
"""Request to save company profile during onboarding Step 1."""
# Company name is already set during signup, but can be updated
company_name: str | None = Field(None, min_length=2, max_length=255)
# Vendor/brand name
brand_name: str | None = Field(None, min_length=2, max_length=255)
description: str | None = Field(None, max_length=2000)
# Contact information
contact_email: str | None = Field(None, max_length=255)
contact_phone: str | None = Field(None, max_length=50)
website: str | None = Field(None, max_length=255)
business_address: str | None = Field(None, max_length=500)
tax_number: str | None = Field(None, max_length=100)
# Language preferences
default_language: str = Field("fr", pattern="^(en|fr|de|lb)$")
dashboard_language: str = Field("fr", pattern="^(en|fr|de|lb)$")
class CompanyProfileResponse(BaseModel):
"""Response after saving company profile."""
success: bool
step_completed: bool
next_step: str | None = None
message: str | None = None
# =============================================================================
# STEP 2: LETZSHOP API CONFIGURATION
# =============================================================================
class LetzshopApiConfigRequest(BaseModel):
"""Request to configure Letzshop API credentials."""
api_key: str = Field(..., min_length=10, description="Letzshop API key")
shop_slug: str = Field(
...,
min_length=2,
max_length=100,
description="Letzshop shop URL slug (e.g., 'my-shop')",
)
vendor_id: str | None = Field(
None,
max_length=100,
description="Letzshop vendor ID (optional, auto-detected if not provided)",
)
class LetzshopApiTestRequest(BaseModel):
"""Request to test Letzshop API connection."""
api_key: str = Field(..., min_length=10)
shop_slug: str = Field(..., min_length=2, max_length=100)
class LetzshopApiTestResponse(BaseModel):
"""Response from Letzshop API connection test."""
success: bool
message: str
vendor_name: str | None = None
vendor_id: str | None = None
shop_slug: str | None = None
class LetzshopApiConfigResponse(BaseModel):
"""Response after saving Letzshop API configuration."""
success: bool
step_completed: bool
next_step: str | None = None
message: str | None = None
connection_verified: bool = False
# =============================================================================
# STEP 3: PRODUCT & ORDER IMPORT CONFIGURATION
# =============================================================================
class ProductImportConfigRequest(BaseModel):
"""Request to configure product import settings."""
# CSV feed URLs for each language
csv_url_fr: str | None = Field(None, max_length=500)
csv_url_en: str | None = Field(None, max_length=500)
csv_url_de: str | None = Field(None, max_length=500)
# Letzshop feed settings
default_tax_rate: int = Field(
17,
ge=0,
le=17,
description="Default VAT rate: 0, 3, 8, 14, or 17",
)
delivery_method: str = Field(
"package_delivery",
description="Delivery method: nationwide, package_delivery, self_collect",
)
preorder_days: int = Field(1, ge=0, le=30)
class ProductImportConfigResponse(BaseModel):
"""Response after saving product import configuration."""
success: bool
step_completed: bool
next_step: str | None = None
message: str | None = None
csv_urls_configured: int = 0
# =============================================================================
# STEP 4: ORDER SYNC
# =============================================================================
class OrderSyncTriggerRequest(BaseModel):
"""Request to trigger historical order import."""
# How far back to import orders (days)
days_back: int = Field(90, ge=1, le=365, description="Days of order history to import")
include_products: bool = Field(True, description="Also import products from Letzshop")
class OrderSyncTriggerResponse(BaseModel):
"""Response after triggering order sync job."""
success: bool
message: str
job_id: int | None = None
estimated_duration_minutes: int | None = None
class OrderSyncProgressResponse(BaseModel):
"""Response for order sync progress polling."""
job_id: int
status: str # pending, running, completed, failed
progress_percentage: int = 0
current_phase: str | None = None # products, orders, finalizing
# Counts
orders_imported: int = 0
orders_total: int | None = None
products_imported: int = 0
# Timing
started_at: datetime | None = None
completed_at: datetime | None = None
estimated_remaining_seconds: int | None = None
# Error info if failed
error_message: str | None = None
class OrderSyncCompleteRequest(BaseModel):
"""Request to mark order sync as complete."""
job_id: int
class OrderSyncCompleteResponse(BaseModel):
"""Response after completing order sync step."""
success: bool
step_completed: bool
onboarding_completed: bool = False
message: str | None = None
redirect_url: str | None = None
# =============================================================================
# ADMIN SKIP
# =============================================================================
class OnboardingSkipRequest(BaseModel):
"""Request to skip onboarding (admin only)."""
reason: str = Field(..., min_length=10, max_length=500)
class OnboardingSkipResponse(BaseModel):
"""Response after skipping onboarding."""
success: bool
message: str
vendor_id: int
skipped_at: datetime

View File

@@ -14,7 +14,7 @@ from app.modules.marketplace.models import (
)
from models.database.user import User
from models.database.vendor import Vendor
from models.schema.marketplace_import_job import (
from app.modules.marketplace.schemas import (
AdminMarketplaceImportJobResponse,
MarketplaceImportJobRequest,
MarketplaceImportJobResponse,

View File

@@ -36,7 +36,7 @@ from app.modules.marketplace.models import (
MarketplaceProductTranslation,
)
from app.modules.inventory.schemas import InventoryLocationResponse, InventorySummaryResponse
from models.schema.marketplace_product import (
from app.modules.marketplace.schemas import (
MarketplaceProductCreate,
MarketplaceProductUpdate,
)

View File

@@ -3,10 +3,33 @@
Payments module Pydantic schemas.
"""
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field
from app.modules.payments.schemas.payment import (
# Configuration
PaymentConfigResponse,
PaymentConfigUpdate,
PaymentConfigUpdateResponse,
# Stripe
StripeConnectRequest,
StripeConnectResponse,
StripeDisconnectResponse,
# Methods
PaymentMethodInfo,
PaymentMethodsResponse,
# Transactions
TransactionInfo,
TransactionsResponse,
# Balance
PaymentBalanceResponse,
# Refunds (config version)
PaymentRefundRequest,
PaymentRefundResponse,
)
class PaymentRequest(BaseModel):
"""Request to process a payment."""
@@ -83,6 +106,7 @@ class GatewayResponse(BaseModel):
__all__ = [
# Core payment schemas
"PaymentRequest",
"PaymentResponse",
"RefundRequest",
@@ -90,4 +114,23 @@ __all__ = [
"PaymentMethodCreate",
"PaymentMethodResponse",
"GatewayResponse",
# Configuration schemas
"PaymentConfigResponse",
"PaymentConfigUpdate",
"PaymentConfigUpdateResponse",
# Stripe integration
"StripeConnectRequest",
"StripeConnectResponse",
"StripeDisconnectResponse",
# Payment methods info
"PaymentMethodInfo",
"PaymentMethodsResponse",
# Transactions
"TransactionInfo",
"TransactionsResponse",
# Balance
"PaymentBalanceResponse",
# Refunds (config version)
"PaymentRefundRequest",
"PaymentRefundResponse",
]

View File

@@ -0,0 +1,167 @@
# app/modules/payments/schemas/payment.py
"""
Payment Pydantic schemas for API validation and responses.
This module provides schemas for:
- Payment configuration
- Stripe integration
- Payment methods
- Transactions and balance
- Refunds
"""
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field
# ============================================================================
# PAYMENT CONFIGURATION SCHEMAS
# ============================================================================
class PaymentConfigResponse(BaseModel):
"""Response for payment configuration."""
payment_gateway: str | None = None
accepted_methods: list[str] = []
currency: str = "EUR"
stripe_connected: bool = False
stripe_account_id: str | None = None
paypal_connected: bool = False
message: str | None = None
class PaymentConfigUpdate(BaseModel):
"""Request model for updating payment configuration."""
payment_gateway: str | None = Field(None, max_length=50)
accepted_methods: list[str] | None = None
currency: str | None = Field(None, max_length=3)
class PaymentConfigUpdateResponse(BaseModel):
"""Response for payment configuration update."""
success: bool = False
message: str | None = None
# ============================================================================
# STRIPE INTEGRATION SCHEMAS
# ============================================================================
class StripeConnectRequest(BaseModel):
"""Request model for connecting Stripe account."""
authorization_code: str | None = None
state: str | None = None
class StripeConnectResponse(BaseModel):
"""Response for Stripe connection."""
connected: bool = False
stripe_account_id: str | None = None
message: str | None = None
class StripeDisconnectResponse(BaseModel):
"""Response for Stripe disconnection."""
disconnected: bool = False
message: str | None = None
# ============================================================================
# PAYMENT METHODS SCHEMAS
# ============================================================================
class PaymentMethodInfo(BaseModel):
"""Information about a payment method."""
id: str
name: str
type: str # credit_card, paypal, bank_transfer, etc.
enabled: bool = True
icon: str | None = None
class PaymentMethodsResponse(BaseModel):
"""Response for payment methods listing."""
methods: list[PaymentMethodInfo] = []
message: str | None = None
# ============================================================================
# TRANSACTION SCHEMAS
# ============================================================================
class TransactionInfo(BaseModel):
"""Information about a payment transaction."""
id: int
order_id: int | None = None
amount: float
currency: str = "EUR"
status: str # pending, completed, failed, refunded
payment_method: str | None = None
customer_email: str | None = None
created_at: datetime
completed_at: datetime | None = None
metadata: dict[str, Any] | None = None
class TransactionsResponse(BaseModel):
"""Response for payment transactions listing."""
transactions: list[TransactionInfo] = []
total: int = 0
skip: int = 0
limit: int = 50
message: str | None = None
# ============================================================================
# BALANCE SCHEMAS
# ============================================================================
class PaymentBalanceResponse(BaseModel):
"""Response for payment balance information."""
available_balance: float = 0.0
pending_balance: float = 0.0
currency: str = "EUR"
next_payout_date: datetime | None = None
last_payout_date: datetime | None = None
last_payout_amount: float | None = None
message: str | None = None
# ============================================================================
# REFUND SCHEMAS (for payment config endpoints)
# ============================================================================
class PaymentRefundRequest(BaseModel):
"""Request model for processing a refund (payment module version)."""
amount: float | None = Field(
None, gt=0, description="Partial refund amount, or None for full refund"
)
reason: str | None = Field(None, max_length=500)
class PaymentRefundResponse(BaseModel):
"""Response for refund operation (payment module version)."""
refund_id: int | None = None
payment_id: int | None = None
amount: float | None = None
status: str | None = None # pending, completed, failed
message: str | None = None