diff --git a/app/api/v1/vendor/settings.py b/app/api/v1/vendor/settings.py index 84dff346..cea9646f 100644 --- a/app/api/v1/vendor/settings.py +++ b/app/api/v1/vendor/settings.py @@ -8,51 +8,440 @@ The get_current_vendor_api dependency guarantees token_vendor_id is present. import logging -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel, Field 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"}, +] + + +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" + ) + + +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'])" + ) + + +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, description="Pre-order lead time in days") + + +# Valid Letzshop tax rates +VALID_TAX_RATES = [0, 3, 8, 14, 17] + +# Valid delivery methods +VALID_DELIVERY_METHODS = ["nationwide", "package_delivery", "self_collect"] + @router.get("") def get_vendor_settings( current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): - """Get vendor settings and configuration.""" + """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, - "contact_email": vendor.contact_email, - "contact_phone": vendor.contact_phone, - "website": vendor.website, - "business_address": vendor.business_address, - "tax_number": vendor.tax_number, - "letzshop_csv_url_fr": vendor.letzshop_csv_url_fr, - "letzshop_csv_url_en": vendor.letzshop_csv_url_en, - "letzshop_csv_url_de": vendor.letzshop_csv_url_de, + "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("/marketplace") -def update_marketplace_settings( - marketplace_config: dict, +@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 marketplace integration settings.""" - # Service handles permission checking and raises InsufficientPermissionsException if needed - return vendor_service.update_marketplace_settings( - db, current_user.token_vendor_id, marketplace_config, current_user + """ + 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.""" + vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) + update_data = letzshop_config.model_dump(exclude_unset=True) + + # Validate tax rate + if "letzshop_default_tax_rate" in update_data: + if update_data["letzshop_default_tax_rate"] not in VALID_TAX_RATES: + raise HTTPException( + status_code=400, + detail=f"Invalid tax rate. Must be one of: {VALID_TAX_RATES}" + ) + + # Validate delivery method + if "letzshop_delivery_method" in update_data: + methods = update_data["letzshop_delivery_method"].split(",") + for method in methods: + if method.strip() not in VALID_DELIVERY_METHODS: + raise HTTPException( + status_code=400, + detail=f"Invalid delivery method. Must be one of: {VALID_DELIVERY_METHODS}" + ) + + # Validate boost_sort (0.0 - 10.0) + if "letzshop_boost_sort" in update_data: + try: + boost = float(update_data["letzshop_boost_sort"]) + if boost < 0.0 or boost > 10.0: + raise HTTPException( + status_code=400, + detail="Boost sort must be between 0.0 and 10.0" + ) + except ValueError: + raise HTTPException( + status_code=400, + detail="Boost sort must be a valid number" + ) + + # Validate preorder_days + if "letzshop_preorder_days" in update_data: + if update_data["letzshop_preorder_days"] < 0: + raise HTTPException( + status_code=400, + detail="Preorder days must be non-negative" + ) + + # Apply updates + 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) + """ + vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) + + # Update only provided fields + update_data = localization_config.model_dump(exclude_unset=True) + + # Validate language codes + valid_language_codes = {lang["code"] for lang in SUPPORTED_LANGUAGES} + valid_locale_codes = {loc["code"] for loc in SUPPORTED_LOCALES} + + if "default_language" in update_data and update_data["default_language"]: + if update_data["default_language"] not in valid_language_codes: + raise HTTPException( + status_code=400, detail=f"Invalid language: {update_data['default_language']}" + ) + + if "dashboard_language" in update_data and update_data["dashboard_language"]: + if update_data["dashboard_language"] not in valid_language_codes: + raise HTTPException( + status_code=400, detail=f"Invalid language: {update_data['dashboard_language']}" + ) + + if "storefront_language" in update_data and update_data["storefront_language"]: + if update_data["storefront_language"] not in valid_language_codes: + raise HTTPException( + status_code=400, detail=f"Invalid language: {update_data['storefront_language']}" + ) + + if "storefront_languages" in update_data and update_data["storefront_languages"]: + for lang in update_data["storefront_languages"]: + if lang not in valid_language_codes: + raise HTTPException(status_code=400, detail=f"Invalid language: {lang}") + + if "storefront_locale" in update_data and update_data["storefront_locale"]: + if update_data["storefront_locale"] not in valid_locale_codes: + raise HTTPException( + status_code=400, detail=f"Invalid locale: {update_data['storefront_locale']}" + ) + + # 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, + } diff --git a/app/templates/vendor/settings.html b/app/templates/vendor/settings.html index 8a5a3c7e..fd25d07f 100644 --- a/app/templates/vendor/settings.html +++ b/app/templates/vendor/settings.html @@ -18,15 +18,15 @@
-
- -
-
+
+ +
+