Some checks failed
Clean up 28 backward compatibility instances identified in the codebase. The app is not live, so all shims are replaced with the target architecture: - Remove legacy Inventory.location column (use bin_location exclusively) - Remove dashboard _extract_metric_value helper (use flat metrics dict) - Remove legacy stat field duplicates (total_stores, total_imports, etc.) - Remove 13 re-export shims and class aliases across modules - Remove module-enabling JSON fallback (use PlatformModule junction table) - Remove menu_to_legacy_format() conversion (return dataclasses directly) - Remove title/description from MarketplaceProductBase schema - Clean billing convenience method docstrings - Clean test fixtures and backward-compat comments - Add PlatformModule seeding to init_production.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
333 lines
11 KiB
Python
333 lines
11 KiB
Python
# app/modules/tenancy/schemas/store.py
|
|
"""
|
|
Pydantic schemas for Store-related operations.
|
|
|
|
Schemas include:
|
|
- StoreCreate: For creating stores under merchants
|
|
- StoreUpdate: For updating store information (Admin only)
|
|
- StoreResponse: Standard store response
|
|
- StoreDetailResponse: Store response with merchant/owner details
|
|
- StoreCreateResponse: Response after store creation
|
|
- StoreListResponse: Paginated store list
|
|
- StoreSummary: Lightweight store info
|
|
|
|
Note: Ownership transfer is handled at the Merchant level.
|
|
See models/schema/merchant.py for MerchantTransferOwnership.
|
|
"""
|
|
|
|
import re
|
|
from datetime import datetime
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
|
|
|
class StoreCreate(BaseModel):
|
|
"""
|
|
Schema for creating a new store (storefront/brand) under an existing merchant.
|
|
|
|
Contact info is inherited from the parent merchant by default.
|
|
Optionally, provide contact fields to override from the start.
|
|
"""
|
|
|
|
# Parent merchant
|
|
merchant_id: int = Field(..., description="ID of the parent merchant", gt=0)
|
|
|
|
# Basic Information
|
|
store_code: str = Field(
|
|
...,
|
|
description="Unique store identifier (e.g., TECHSTORE)",
|
|
min_length=2,
|
|
max_length=50,
|
|
)
|
|
subdomain: str = Field(
|
|
..., description="Unique subdomain for the store", min_length=2, max_length=100
|
|
)
|
|
name: str = Field(
|
|
...,
|
|
description="Display name of the store/brand",
|
|
min_length=2,
|
|
max_length=255,
|
|
)
|
|
description: str | None = Field(None, description="Store/brand description")
|
|
|
|
# Platform assignments (optional - store can be on multiple platforms)
|
|
platform_ids: list[int] | None = Field(
|
|
None, description="List of platform IDs to assign the store to"
|
|
)
|
|
|
|
# Marketplace URLs (brand-specific multi-language support)
|
|
letzshop_csv_url_fr: str | None = Field(None, description="French CSV URL")
|
|
letzshop_csv_url_en: str | None = Field(None, description="English CSV URL")
|
|
letzshop_csv_url_de: str | None = Field(None, description="German CSV URL")
|
|
|
|
# Contact Info (optional - if not provided, inherited from merchant)
|
|
contact_email: str | None = Field(
|
|
None, description="Override merchant contact email"
|
|
)
|
|
contact_phone: str | None = Field(
|
|
None, description="Override merchant contact phone"
|
|
)
|
|
website: str | None = Field(None, description="Override merchant website")
|
|
business_address: str | None = Field(
|
|
None, description="Override merchant business address"
|
|
)
|
|
tax_number: str | None = Field(None, description="Override merchant tax number")
|
|
|
|
# Language Settings
|
|
default_language: str | None = Field(
|
|
"fr", description="Default language for content (en, fr, de, lb)"
|
|
)
|
|
dashboard_language: str | None = Field(
|
|
"fr", description="Store dashboard UI language"
|
|
)
|
|
storefront_language: str | None = Field(
|
|
"fr", description="Default storefront language for customers"
|
|
)
|
|
storefront_languages: list[str] | None = Field(
|
|
default=["fr", "de", "en"], description="Enabled languages for storefront"
|
|
)
|
|
storefront_locale: str | None = Field(
|
|
None,
|
|
description="Locale for currency/number formatting (e.g., 'fr-LU', 'de-DE'). NULL = inherit from platform default",
|
|
max_length=10,
|
|
)
|
|
|
|
@field_validator("subdomain")
|
|
@classmethod
|
|
def validate_subdomain(cls, v):
|
|
"""Validate subdomain format: lowercase alphanumeric with hyphens."""
|
|
if v and not re.match(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$", v):
|
|
raise ValueError(
|
|
"Subdomain must contain only lowercase letters, numbers, and hyphens"
|
|
)
|
|
return v.lower() if v else v
|
|
|
|
@field_validator("store_code")
|
|
@classmethod
|
|
def validate_store_code(cls, v):
|
|
"""Ensure store code is uppercase for consistency."""
|
|
return v.upper() if v else v
|
|
|
|
|
|
class StoreUpdate(BaseModel):
|
|
"""
|
|
Schema for updating store information (Admin only).
|
|
|
|
Contact fields can be overridden at the store level.
|
|
Set to null/empty to reset to merchant default (inherit).
|
|
"""
|
|
|
|
# Basic Information
|
|
name: str | None = Field(None, min_length=2, max_length=255)
|
|
description: str | None = None
|
|
subdomain: str | None = Field(None, min_length=2, max_length=100)
|
|
|
|
# Marketplace URLs (brand-specific)
|
|
letzshop_csv_url_fr: str | None = None
|
|
letzshop_csv_url_en: str | None = None
|
|
letzshop_csv_url_de: str | None = None
|
|
|
|
# Status (Admin only)
|
|
is_active: bool | None = None
|
|
is_verified: bool | None = None
|
|
|
|
# Contact Info (set value to override, set to empty string to reset to inherit)
|
|
contact_email: str | None = Field(
|
|
None, description="Override merchant contact email"
|
|
)
|
|
contact_phone: str | None = Field(
|
|
None, description="Override merchant contact phone"
|
|
)
|
|
website: str | None = Field(None, description="Override merchant website")
|
|
business_address: str | None = Field(
|
|
None, description="Override merchant business address"
|
|
)
|
|
tax_number: str | None = Field(None, description="Override merchant tax number")
|
|
|
|
# Special flag to reset contact fields to inherit from merchant
|
|
reset_contact_to_merchant: bool | None = Field(
|
|
None, description="If true, reset all contact fields to inherit from merchant"
|
|
)
|
|
|
|
# Language Settings
|
|
default_language: str | None = Field(
|
|
None, description="Default language for content (en, fr, de, lb)"
|
|
)
|
|
dashboard_language: str | None = Field(
|
|
None, description="Store dashboard UI language"
|
|
)
|
|
storefront_language: str | None = Field(
|
|
None, description="Default storefront language for customers"
|
|
)
|
|
storefront_languages: list[str] | None = Field(
|
|
None, description="Enabled languages for storefront"
|
|
)
|
|
storefront_locale: str | None = Field(
|
|
None,
|
|
description="Locale for currency/number formatting (e.g., 'fr-LU', 'de-DE'). NULL = inherit from platform default",
|
|
max_length=10,
|
|
)
|
|
|
|
@field_validator("subdomain")
|
|
@classmethod
|
|
def subdomain_lowercase(cls, v):
|
|
"""Normalize subdomain to lowercase."""
|
|
return v.lower().strip() if v else v
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class StoreResponse(BaseModel):
|
|
"""
|
|
Standard schema for store response data.
|
|
|
|
Note: Business contact info (contact_email, contact_phone, website,
|
|
business_address, tax_number) is now at the Merchant level.
|
|
Use merchant_id to look up merchant details.
|
|
"""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
store_code: str
|
|
subdomain: str
|
|
name: str
|
|
description: str | None
|
|
|
|
# Merchant relationship
|
|
merchant_id: int
|
|
|
|
# Marketplace URLs (brand-specific)
|
|
letzshop_csv_url_fr: str | None
|
|
letzshop_csv_url_en: str | None
|
|
letzshop_csv_url_de: str | None
|
|
|
|
# Status Flags
|
|
is_active: bool
|
|
is_verified: bool
|
|
|
|
# Language Settings (optional with sensible defaults)
|
|
default_language: str = "fr"
|
|
dashboard_language: str = "fr"
|
|
storefront_language: str = "fr"
|
|
storefront_languages: list[str] = ["fr", "de", "en"]
|
|
|
|
# Currency/number formatting locale (NULL = inherit from platform default)
|
|
storefront_locale: str | None = None
|
|
|
|
# Timestamps
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class StoreDetailResponse(StoreResponse):
|
|
"""
|
|
Extended store response including merchant information and resolved contact info.
|
|
|
|
Contact fields show the effective value (store override or merchant default)
|
|
with flags indicating if the value is inherited from the parent merchant.
|
|
"""
|
|
|
|
# Merchant info
|
|
merchant_name: str = Field(..., description="Name of the parent merchant")
|
|
|
|
# Owner info (at merchant level)
|
|
owner_email: str = Field(
|
|
..., description="Email of the merchant owner (for login/authentication)"
|
|
)
|
|
owner_username: str = Field(..., description="Username of the merchant owner")
|
|
|
|
# Resolved contact info (store override or merchant default)
|
|
contact_email: str | None = Field(None, description="Effective contact email")
|
|
contact_phone: str | None = Field(None, description="Effective contact phone")
|
|
website: str | None = Field(None, description="Effective website")
|
|
business_address: str | None = Field(None, description="Effective business address")
|
|
tax_number: str | None = Field(None, description="Effective tax number")
|
|
|
|
# Inheritance flags (True = value is inherited from merchant, not overridden)
|
|
contact_email_inherited: bool = Field(
|
|
False, description="True if contact_email is from merchant"
|
|
)
|
|
contact_phone_inherited: bool = Field(
|
|
False, description="True if contact_phone is from merchant"
|
|
)
|
|
website_inherited: bool = Field(
|
|
False, description="True if website is from merchant"
|
|
)
|
|
business_address_inherited: bool = Field(
|
|
False, description="True if business_address is from merchant"
|
|
)
|
|
tax_number_inherited: bool = Field(
|
|
False, description="True if tax_number is from merchant"
|
|
)
|
|
|
|
# Original merchant values (for reference in UI)
|
|
merchant_contact_email: str | None = Field(
|
|
None, description="Merchant's contact email"
|
|
)
|
|
merchant_contact_phone: str | None = Field(
|
|
None, description="Merchant's phone number"
|
|
)
|
|
merchant_website: str | None = Field(None, description="Merchant's website URL")
|
|
merchant_business_address: str | None = Field(
|
|
None, description="Merchant's business address"
|
|
)
|
|
merchant_tax_number: str | None = Field(None, description="Merchant's tax number")
|
|
|
|
|
|
class StoreCreateResponse(StoreDetailResponse):
|
|
"""
|
|
Response after creating store under an existing merchant.
|
|
|
|
The store is created under a merchant, so no new owner credentials are generated.
|
|
The merchant owner already has access to this store.
|
|
"""
|
|
|
|
login_url: str | None = Field(None, description="URL for store storefront")
|
|
|
|
|
|
class StoreListResponse(BaseModel):
|
|
"""Schema for paginated store list."""
|
|
|
|
stores: list[StoreResponse]
|
|
total: int
|
|
skip: int
|
|
limit: int
|
|
|
|
|
|
class StoreSummary(BaseModel):
|
|
"""Lightweight store summary for dropdowns and quick references."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
store_code: str
|
|
subdomain: str
|
|
name: str
|
|
merchant_id: int
|
|
is_active: bool
|
|
|
|
|
|
# NOTE: Store ownership transfer schemas have been removed.
|
|
# Ownership transfer is now handled at the Merchant level.
|
|
# See models/schema/merchant.py for MerchantTransferOwnership and MerchantTransferOwnershipResponse.
|
|
|
|
# NOTE: Letzshop export schemas have been moved to app.modules.marketplace.schemas.letzshop
|
|
# See LetzshopExportRequest, LetzshopExportFileInfo, LetzshopExportResponse
|
|
|
|
|
|
# Re-export StoreStatsResponse from core for convenience
|
|
# This allows tenancy routes to use this schema without importing from core directly
|
|
from app.modules.core.schemas.dashboard import StoreStatsResponse
|
|
|
|
__all__ = [
|
|
"StoreCreate",
|
|
"StoreUpdate",
|
|
"StoreResponse",
|
|
"StoreDetailResponse",
|
|
"StoreCreateResponse",
|
|
"StoreListResponse",
|
|
"StoreSummary",
|
|
"StoreStatsResponse",
|
|
]
|