Files
orion/app/modules/tenancy/schemas/store.py
Samir Boulahtit aad18c27ab
Some checks failed
CI / ruff (push) Successful in 11s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running
refactor: remove all backward compatibility code across 70 files
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>
2026-02-15 13:20:29 +01:00

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