# app/api/v1/vendor/settings.py """ Vendor settings and configuration endpoints. Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern). The get_current_vendor_api dependency guarantees token_vendor_id is present. """ import logging from fastapi import APIRouter, Depends from pydantic import BaseModel, Field, field_validator from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_api from app.core.database import get_db from app.services.platform_settings_service import platform_settings_service from app.services.vendor_service import vendor_service from models.database.user import User router = APIRouter(prefix="/settings") logger = logging.getLogger(__name__) # Supported languages for dropdown SUPPORTED_LANGUAGES = [ {"code": "en", "name": "English"}, {"code": "fr", "name": "Français"}, {"code": "de", "name": "Deutsch"}, {"code": "lb", "name": "Lëtzebuergesch"}, ] # Supported locales for currency/number formatting SUPPORTED_LOCALES = [ {"code": "fr-LU", "name": "Luxembourg (French)", "example": "29,99 €"}, {"code": "de-LU", "name": "Luxembourg (German)", "example": "29,99 €"}, {"code": "de-DE", "name": "Germany", "example": "29,99 €"}, {"code": "fr-FR", "name": "France", "example": "29,99 €"}, {"code": "en-GB", "name": "United Kingdom", "example": "€29.99"}, ] # Valid language codes for validation VALID_LANGUAGE_CODES = {"en", "fr", "de", "lb"} # Valid locale codes for validation VALID_LOCALE_CODES = {"fr-LU", "de-LU", "de-DE", "fr-FR", "en-GB"} class LocalizationSettingsUpdate(BaseModel): """Schema for updating localization settings.""" default_language: str | None = Field( None, description="Default language for vendor content" ) dashboard_language: str | None = Field( None, description="Language for vendor dashboard UI" ) storefront_language: str | None = Field( None, description="Default language for customer storefront" ) storefront_languages: list[str] | None = Field( None, description="Enabled languages for storefront selector" ) storefront_locale: str | None = Field( None, description="Locale for currency/number formatting" ) @field_validator("default_language", "dashboard_language", "storefront_language") @classmethod def validate_language(cls, v: str | None) -> str | None: if v is not None and v not in VALID_LANGUAGE_CODES: raise ValueError(f"Invalid language: {v}. Must be one of: {sorted(VALID_LANGUAGE_CODES)}") return v @field_validator("storefront_languages") @classmethod def validate_storefront_languages(cls, v: list[str] | None) -> list[str] | None: if v is not None: for lang in v: if lang not in VALID_LANGUAGE_CODES: raise ValueError(f"Invalid language: {lang}. Must be one of: {sorted(VALID_LANGUAGE_CODES)}") return v @field_validator("storefront_locale") @classmethod def validate_locale(cls, v: str | None) -> str | None: if v is not None and v not in VALID_LOCALE_CODES: raise ValueError(f"Invalid locale: {v}. Must be one of: {sorted(VALID_LOCALE_CODES)}") return v class BusinessInfoUpdate(BaseModel): """Schema for updating business info (can override company values).""" name: str | None = Field(None, description="Store/brand name") description: str | None = Field(None, description="Store description") contact_email: str | None = Field(None, description="Contact email (null = inherit from company)") contact_phone: str | None = Field(None, description="Contact phone (null = inherit from company)") website: str | None = Field(None, description="Website URL (null = inherit from company)") business_address: str | None = Field(None, description="Business address (null = inherit from company)") tax_number: str | None = Field(None, description="Tax/VAT number (null = inherit from company)") reset_to_company: list[str] | None = Field( None, description="List of fields to reset to company values (e.g., ['contact_email', 'website'])" ) # Valid Letzshop tax rates VALID_TAX_RATES = [0, 3, 8, 14, 17] # Valid delivery methods VALID_DELIVERY_METHODS = ["nationwide", "package_delivery", "self_collect"] class LetzshopFeedSettingsUpdate(BaseModel): """Schema for updating Letzshop feed settings.""" letzshop_csv_url_fr: str | None = Field(None, description="French CSV feed URL") letzshop_csv_url_en: str | None = Field(None, description="English CSV feed URL") letzshop_csv_url_de: str | None = Field(None, description="German CSV feed URL") letzshop_default_tax_rate: int | None = Field(None, description="Default VAT rate (0, 3, 8, 14, 17)") letzshop_boost_sort: str | None = Field(None, description="Sort priority (0.0-10.0)") letzshop_delivery_method: str | None = Field(None, description="Delivery method") letzshop_preorder_days: int | None = Field(None, ge=0, description="Pre-order lead time in days") @field_validator("letzshop_default_tax_rate") @classmethod def validate_tax_rate(cls, v: int | None) -> int | None: if v is not None and v not in VALID_TAX_RATES: raise ValueError(f"Invalid tax rate. Must be one of: {VALID_TAX_RATES}") return v @field_validator("letzshop_delivery_method") @classmethod def validate_delivery_method(cls, v: str | None) -> str | None: if v is not None: methods = v.split(",") for method in methods: if method.strip() not in VALID_DELIVERY_METHODS: raise ValueError(f"Invalid delivery method. Must be one of: {VALID_DELIVERY_METHODS}") return v @field_validator("letzshop_boost_sort") @classmethod def validate_boost_sort(cls, v: str | None) -> str | None: if v is not None: try: boost = float(v) if boost < 0.0 or boost > 10.0: raise ValueError("Boost sort must be between 0.0 and 10.0") except ValueError as e: if "could not convert" in str(e).lower(): raise ValueError("Boost sort must be a valid number") raise return v @router.get("") def get_vendor_settings( current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """Get comprehensive vendor settings and configuration.""" vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # Get platform defaults for display platform_config = platform_settings_service.get_storefront_config(db) # Get business info with inheritance flags contact_info = vendor.get_contact_info_with_inheritance() # Get invoice settings if exists invoice_settings = None if vendor.invoice_settings: inv = vendor.invoice_settings invoice_settings = { "company_name": inv.company_name, "company_address": inv.company_address, "company_city": inv.company_city, "company_postal_code": inv.company_postal_code, "company_country": inv.company_country, "vat_number": inv.vat_number, "is_vat_registered": inv.is_vat_registered, "invoice_prefix": inv.invoice_prefix, "invoice_next_number": inv.invoice_next_number, "payment_terms": inv.payment_terms, "bank_name": inv.bank_name, "bank_iban": inv.bank_iban, "bank_bic": inv.bank_bic, "footer_text": inv.footer_text, "default_vat_rate": inv.default_vat_rate, } # Get theme settings if exists theme_settings = None if vendor.vendor_theme: theme = vendor.vendor_theme theme_settings = { "theme_name": theme.theme_name, "colors": theme.colors, "font_family_heading": theme.font_family_heading, "font_family_body": theme.font_family_body, "logo_url": theme.logo_url, "logo_dark_url": theme.logo_dark_url, "favicon_url": theme.favicon_url, "banner_url": theme.banner_url, "layout_style": theme.layout_style, "header_style": theme.header_style, "product_card_style": theme.product_card_style, "social_links": theme.social_links, "custom_css": theme.custom_css, } # Get domains (read-only) domains = [] for domain in vendor.domains: domains.append({ "id": domain.id, "domain": domain.domain, "is_primary": domain.is_primary, "is_active": domain.is_active, "ssl_status": domain.ssl_status, "is_verified": domain.is_verified, }) # Get Stripe info from subscription (read-only, masked) stripe_info = None if vendor.subscription and vendor.subscription.stripe_customer_id: stripe_info = { "has_stripe_customer": True, "customer_id_masked": f"cus_***{vendor.subscription.stripe_customer_id[-4:]}", } return { # General info "vendor_code": vendor.vendor_code, "subdomain": vendor.subdomain, "name": vendor.name, "description": vendor.description, "is_active": vendor.is_active, "is_verified": vendor.is_verified, # Business info with inheritance (values + flags) "business_info": { "contact_email": contact_info["contact_email"], "contact_email_inherited": contact_info["contact_email_inherited"], "contact_email_override": vendor.contact_email, # Raw override value "contact_phone": contact_info["contact_phone"], "contact_phone_inherited": contact_info["contact_phone_inherited"], "contact_phone_override": vendor.contact_phone, "website": contact_info["website"], "website_inherited": contact_info["website_inherited"], "website_override": vendor.website, "business_address": contact_info["business_address"], "business_address_inherited": contact_info["business_address_inherited"], "business_address_override": vendor.business_address, "tax_number": contact_info["tax_number"], "tax_number_inherited": contact_info["tax_number_inherited"], "tax_number_override": vendor.tax_number, "company_name": vendor.company.name if vendor.company else None, }, # Localization settings "localization": { "default_language": vendor.default_language, "dashboard_language": vendor.dashboard_language, "storefront_language": vendor.storefront_language, "storefront_languages": vendor.storefront_languages or ["fr", "de", "en"], "storefront_locale": vendor.storefront_locale, "platform_default_locale": platform_config["locale"], "platform_currency": platform_config["currency"], }, # Letzshop marketplace settings "letzshop": { "csv_url_fr": vendor.letzshop_csv_url_fr, "csv_url_en": vendor.letzshop_csv_url_en, "csv_url_de": vendor.letzshop_csv_url_de, "default_tax_rate": vendor.letzshop_default_tax_rate, "boost_sort": vendor.letzshop_boost_sort, "delivery_method": vendor.letzshop_delivery_method, "preorder_days": vendor.letzshop_preorder_days, "vendor_id": vendor.letzshop_vendor_id, "vendor_slug": vendor.letzshop_vendor_slug, "has_credentials": vendor.letzshop_credentials is not None, "auto_sync_enabled": vendor.letzshop_credentials.auto_sync_enabled if vendor.letzshop_credentials else False, }, # Invoice settings "invoice_settings": invoice_settings, # Theme/branding settings "theme_settings": theme_settings, # Domains (read-only) "domains": domains, "default_subdomain": f"{vendor.subdomain}.letzshop.lu", # Stripe info (read-only) "stripe_info": stripe_info, # Options for dropdowns "options": { "supported_languages": SUPPORTED_LANGUAGES, "supported_locales": SUPPORTED_LOCALES, "tax_rates": [ {"value": 0, "label": "0% (Exempt)"}, {"value": 3, "label": "3% (Super-reduced)"}, {"value": 8, "label": "8% (Reduced)"}, {"value": 14, "label": "14% (Intermediate)"}, {"value": 17, "label": "17% (Standard)"}, ], "delivery_methods": [ {"value": "nationwide", "label": "Nationwide (all methods)"}, {"value": "package_delivery", "label": "Package Delivery"}, {"value": "self_collect", "label": "Self Collect"}, ], }, } @router.put("/business-info") def update_business_info( business_info: BusinessInfoUpdate, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Update vendor business info. Fields can be set to override company values, or reset to inherit from company. Use reset_to_company list to reset specific fields to inherit from company. """ vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) update_data = business_info.model_dump(exclude_unset=True) # Handle reset_to_company - set those fields to None reset_fields = update_data.pop("reset_to_company", None) or [] for field in reset_fields: if field in ["contact_email", "contact_phone", "website", "business_address", "tax_number"]: setattr(vendor, field, None) logger.info(f"Reset {field} to inherit from company for vendor {vendor.id}") # Update other fields for key, value in update_data.items(): if key in ["name", "description", "contact_email", "contact_phone", "website", "business_address", "tax_number"]: setattr(vendor, key, value) db.commit() db.refresh(vendor) logger.info(f"Business info updated for vendor {vendor.id}") # Return updated info with inheritance contact_info = vendor.get_contact_info_with_inheritance() return { "message": "Business info updated", "name": vendor.name, "description": vendor.description, "business_info": contact_info, } @router.put("/letzshop") def update_letzshop_settings( letzshop_config: LetzshopFeedSettingsUpdate, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """Update Letzshop marketplace feed settings. Validation is handled by Pydantic model validators. """ vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) update_data = letzshop_config.model_dump(exclude_unset=True) # Apply updates (validation already done by Pydantic) for key, value in update_data.items(): setattr(vendor, key, value) db.commit() db.refresh(vendor) logger.info(f"Letzshop settings updated for vendor {vendor.id}") return { "message": "Letzshop settings updated", "csv_url_fr": vendor.letzshop_csv_url_fr, "csv_url_en": vendor.letzshop_csv_url_en, "csv_url_de": vendor.letzshop_csv_url_de, "default_tax_rate": vendor.letzshop_default_tax_rate, "boost_sort": vendor.letzshop_boost_sort, "delivery_method": vendor.letzshop_delivery_method, "preorder_days": vendor.letzshop_preorder_days, } @router.put("/localization") def update_localization_settings( localization_config: LocalizationSettingsUpdate, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Update vendor localization settings. Allows vendors to configure: - default_language: Default language for vendor content - dashboard_language: UI language for vendor dashboard - storefront_language: Default language for customer storefront - storefront_languages: Enabled languages for storefront selector - storefront_locale: Locale for currency/number formatting (or null for platform default) Validation is handled by Pydantic model validators. """ vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # Update only provided fields (validation already done by Pydantic) update_data = localization_config.model_dump(exclude_unset=True) # Apply updates for key, value in update_data.items(): setattr(vendor, key, value) db.commit() db.refresh(vendor) logger.info( f"Localization settings updated for vendor {vendor.id}", extra={"vendor_id": vendor.id, "updated_fields": list(update_data.keys())}, ) return { "message": "Localization settings updated", "default_language": vendor.default_language, "dashboard_language": vendor.dashboard_language, "storefront_language": vendor.storefront_language, "storefront_languages": vendor.storefront_languages, "storefront_locale": vendor.storefront_locale, }