""" Webshop Module - API Router Backend endpoints for webshop administration og konfiguration """ from fastapi import APIRouter, HTTPException, UploadFile, File, Form from typing import List, Optional from pydantic import BaseModel, field_validator from datetime import datetime import logging import os import shutil from app.core.database import execute_query, execute_insert, execute_update, execute_query_single logger = logging.getLogger(__name__) # APIRouter instance (module_loader kigger efter denne) router = APIRouter() # Upload directory for logos LOGO_UPLOAD_DIR = "/app/uploads/webshop_logos" os.makedirs(LOGO_UPLOAD_DIR, exist_ok=True) # ============================================================================ # PYDANTIC MODELS # ============================================================================ class WebshopConfigCreate(BaseModel): customer_id: int name: str allowed_email_domains: str # Comma-separated header_text: Optional[str] = None intro_text: Optional[str] = None primary_color: str = "#0f4c75" accent_color: str = "#3282b8" default_margin_percent: float = 10.0 min_order_amount: float = 0.0 shipping_cost: float = 0.0 enabled: bool = True class WebshopConfigUpdate(BaseModel): name: Optional[str] = None allowed_email_domains: Optional[str] = None header_text: Optional[str] = None intro_text: Optional[str] = None primary_color: Optional[str] = None accent_color: Optional[str] = None default_margin_percent: Optional[float] = None min_order_amount: Optional[float] = None shipping_cost: Optional[float] = None enabled: Optional[bool] = None class WebshopProductCreate(BaseModel): webshop_config_id: int product_number: str ean: Optional[str] = None name: str description: Optional[str] = None unit: str = "stk" base_price: float category: Optional[str] = None custom_margin_percent: Optional[float] = None visible: bool = True sort_order: int = 0 @field_validator('base_price') @classmethod def validate_base_price(cls, v): if v <= 0: raise ValueError('Basispris må ikke være 0 eller negativ') return v # ============================================================================ # WEBSHOP CONFIG ENDPOINTS # ============================================================================ @router.get("/webshop/configs") async def get_all_webshop_configs(): """ Hent alle webshop konfigurationer med kunde info """ try: query = """ SELECT wc.*, c.name as customer_name, c.cvr_number as customer_cvr, (SELECT COUNT(*) FROM webshop_products WHERE webshop_config_id = wc.id) as product_count FROM webshop_configs wc LEFT JOIN customers c ON c.id = wc.customer_id ORDER BY wc.created_at DESC """ configs = execute_query(query) return { "success": True, "configs": configs } except Exception as e: logger.error(f"❌ Error fetching webshop configs: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/webshop/configs/{config_id}") async def get_webshop_config(config_id: int): """ Hent enkelt webshop konfiguration """ try: query = """ SELECT wc.*, c.name as customer_name, c.cvr_number as customer_cvr, c.email as customer_email FROM webshop_configs wc LEFT JOIN customers c ON c.id = wc.customer_id WHERE wc.id = %s """ config = execute_query_single(query, (config_id,)) if not config: raise HTTPException(status_code=404, detail="Webshop config not found") # Hent produkter products_query = """ SELECT * FROM webshop_products WHERE webshop_config_id = %s ORDER BY sort_order, name """ products = execute_query(products_query, (config_id,)) return { "success": True, "config": config, "products": products } except HTTPException: raise except Exception as e: logger.error(f"❌ Error fetching webshop config {config_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/webshop/configs") async def create_webshop_config(config: WebshopConfigCreate): """ Opret ny webshop konfiguration """ try: # Check if customer already has a webshop existing = execute_query_single( "SELECT id FROM webshop_configs WHERE customer_id = %s", (config.customer_id,) ) if existing: raise HTTPException( status_code=400, detail="Customer already has a webshop configuration" ) query = """ INSERT INTO webshop_configs ( customer_id, name, allowed_email_domains, header_text, intro_text, primary_color, accent_color, default_margin_percent, min_order_amount, shipping_cost, enabled ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING * """ result = execute_query_single( query, ( config.customer_id, config.name, config.allowed_email_domains, config.header_text, config.intro_text, config.primary_color, config.accent_color, config.default_margin_percent, config.min_order_amount, config.shipping_cost, config.enabled ) ) logger.info(f"✅ Created webshop config {result['id']} for customer {config.customer_id}") return { "success": True, "config": result, "message": "Webshop configuration created successfully" } except HTTPException: raise except Exception as e: logger.error(f"❌ Error creating webshop config: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.put("/webshop/configs/{config_id}") async def update_webshop_config(config_id: int, config: WebshopConfigUpdate): """ Opdater webshop konfiguration """ try: # Build dynamic update query update_fields = [] params = [] if config.name is not None: update_fields.append("name = %s") params.append(config.name) if config.allowed_email_domains is not None: update_fields.append("allowed_email_domains = %s") params.append(config.allowed_email_domains) if config.header_text is not None: update_fields.append("header_text = %s") params.append(config.header_text) if config.intro_text is not None: update_fields.append("intro_text = %s") params.append(config.intro_text) if config.primary_color is not None: update_fields.append("primary_color = %s") params.append(config.primary_color) if config.accent_color is not None: update_fields.append("accent_color = %s") params.append(config.accent_color) if config.default_margin_percent is not None: update_fields.append("default_margin_percent = %s") params.append(config.default_margin_percent) if config.min_order_amount is not None: update_fields.append("min_order_amount = %s") params.append(config.min_order_amount) if config.shipping_cost is not None: update_fields.append("shipping_cost = %s") params.append(config.shipping_cost) if config.enabled is not None: update_fields.append("enabled = %s") params.append(config.enabled) if not update_fields: raise HTTPException(status_code=400, detail="No fields to update") params.append(config_id) query = f""" UPDATE webshop_configs SET {', '.join(update_fields)} WHERE id = %s RETURNING * """ result = execute_query_single(query, tuple(params)) if not result: raise HTTPException(status_code=404, detail="Webshop config not found") logger.info(f"✅ Updated webshop config {config_id}") return { "success": True, "config": result, "message": "Webshop configuration updated successfully" } except HTTPException: raise except Exception as e: logger.error(f"❌ Error updating webshop config {config_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/webshop/configs/{config_id}/upload-logo") async def upload_webshop_logo(config_id: int, logo: UploadFile = File(...)): """ Upload logo til webshop """ try: # Validate file type if not logo.content_type.startswith("image/"): raise HTTPException(status_code=400, detail="File must be an image") # Generate filename ext = logo.filename.split(".")[-1] filename = f"webshop_{config_id}.{ext}" filepath = os.path.join(LOGO_UPLOAD_DIR, filename) # Save file with open(filepath, 'wb') as f: shutil.copyfileobj(logo.file, f) # Update database query = """ UPDATE webshop_configs SET logo_filename = %s WHERE id = %s RETURNING * """ result = execute_query_single(query, (filename, config_id)) if not result: raise HTTPException(status_code=404, detail="Webshop config not found") logger.info(f"✅ Uploaded logo for webshop {config_id}: {filename}") return { "success": True, "filename": filename, "config": result, "message": "Logo uploaded successfully" } except HTTPException: raise except Exception as e: logger.error(f"❌ Error uploading logo for webshop {config_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/webshop/configs/{config_id}") async def delete_webshop_config(config_id: int): """ Slet webshop konfiguration (soft delete - disable) """ try: query = """ UPDATE webshop_configs SET enabled = FALSE WHERE id = %s RETURNING * """ result = execute_query_single(query, (config_id,)) if not result: raise HTTPException(status_code=404, detail="Webshop config not found") logger.info(f"✅ Disabled webshop config {config_id}") return { "success": True, "message": "Webshop configuration disabled" } except HTTPException: raise except Exception as e: logger.error(f"❌ Error deleting webshop config {config_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) # ========================================================================== # PRODUCT SEARCH HELPERS # ========================================================================== @router.get("/webshop/products/search") async def search_webshop_products( search: Optional[str] = None, limit: int = 20, config_id: Optional[int] = None ): try: params: List = [] filters = "WHERE visible = TRUE" if config_id is not None: filters += " AND webshop_config_id = %s" params.append(config_id) if search: pattern = f"%{search}%" filters += " AND (name ILIKE %s OR product_number ILIKE %s OR category ILIKE %s)" params.extend([pattern, pattern, pattern]) query = f"SELECT * FROM webshop_products {filters} ORDER BY updated_at DESC LIMIT %s" params.append(limit) return {"products": execute_query(query, tuple(params)) or []} except Exception as e: logger.error(f"❌ Error searching webshop products: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # WEBSHOP PRODUCT ENDPOINTS # ============================================================================ @router.post("/webshop/products") async def add_webshop_product(product: WebshopProductCreate): """ Tilføj produkt til webshop whitelist """ try: # Validate price is not zero if product.base_price <= 0: raise HTTPException( status_code=400, detail="Basispris må ikke være 0 eller negativ. Angiv en gyldig pris." ) query = """ INSERT INTO webshop_products ( webshop_config_id, product_number, ean, name, description, unit, base_price, category, custom_margin_percent, visible, sort_order ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING * """ result = execute_query_single( query, ( product.webshop_config_id, product.product_number, product.ean, product.name, product.description, product.unit, product.base_price, product.category, product.custom_margin_percent, product.visible, product.sort_order ) ) logger.info(f"✅ Added product {product.product_number} to webshop {product.webshop_config_id}") return { "success": True, "product": result, "message": "Product added to webshop" } except Exception as e: if "unique" in str(e).lower(): raise HTTPException(status_code=400, detail="Product already exists in this webshop") logger.error(f"❌ Error adding product to webshop: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/webshop/products/{product_id}") async def remove_webshop_product(product_id: int): """ Fjern produkt fra webshop whitelist """ try: query = "DELETE FROM webshop_products WHERE id = %s RETURNING *" result = execute_query_single(query, (product_id,)) if not result: raise HTTPException(status_code=404, detail="Product not found") logger.info(f"✅ Removed product {product_id} from webshop") return { "success": True, "message": "Product removed from webshop" } except HTTPException: raise except Exception as e: logger.error(f"❌ Error removing product {product_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # PUBLISH TO GATEWAY # ============================================================================ @router.post("/webshop/configs/{config_id}/publish") async def publish_webshop_to_gateway(config_id: int): """ Push webshop konfiguration til API Gateway """ try: from app.core.config import settings import aiohttp # Hent config og produkter config_data = await get_webshop_config(config_id) if not config_data["config"]["enabled"]: raise HTTPException(status_code=400, detail="Cannot publish disabled webshop") # Byg payload payload = { "config_id": config_id, "customer_id": config_data["config"]["customer_id"], "company_name": config_data["config"]["customer_name"], "config_version": config_data["config"]["config_version"].isoformat(), "branding": { "logo_url": f"{settings.BASE_URL}/uploads/webshop_logos/{config_data['config']['logo_filename']}" if config_data['config'].get('logo_filename') else None, "header_text": config_data["config"]["header_text"], "intro_text": config_data["config"]["intro_text"], "primary_color": config_data["config"]["primary_color"], "accent_color": config_data["config"]["accent_color"] }, "allowed_email_domains": config_data["config"]["allowed_email_domains"].split(","), "products": [ { "id": p["id"], "product_number": p["product_number"], "ean": p["ean"], "name": p["name"], "description": p["description"], "unit": p["unit"], "base_price": float(p["base_price"]), "calculated_price": float(p["base_price"]) * (1 + (float(p.get("custom_margin_percent") or config_data["config"]["default_margin_percent"]) / 100)), "margin_percent": float(p.get("custom_margin_percent") or config_data["config"]["default_margin_percent"]), "category": p["category"], "visible": p["visible"] } for p in config_data["products"] ], "settings": { "min_order_amount": float(config_data["config"]["min_order_amount"]), "shipping_cost": float(config_data["config"]["shipping_cost"]), "default_margin_percent": float(config_data["config"]["default_margin_percent"]) } } # Send til Gateway gateway_url = "https://apigateway.bmcnetworks.dk/webshop/ingest" # TODO: Uncomment når Gateway er klar # async with aiohttp.ClientSession() as session: # async with session.post(gateway_url, json=payload) as response: # if response.status != 200: # error_text = await response.text() # raise HTTPException(status_code=500, detail=f"Gateway error: {error_text}") # # gateway_response = await response.json() # Mock response for now logger.warning("⚠️ Gateway push disabled - mock response returned") gateway_response = {"success": True, "message": "Config received (MOCK)"} # Update last_published timestamps update_query = """ UPDATE webshop_configs SET last_published_at = CURRENT_TIMESTAMP, last_published_version = config_version WHERE id = %s RETURNING * """ updated_config = execute_query_single(update_query, (config_id,)) logger.info(f"✅ Published webshop {config_id} to Gateway") return { "success": True, "config": updated_config, "payload": payload, "gateway_response": gateway_response, "message": "Webshop configuration published to Gateway" } except HTTPException: raise except Exception as e: logger.error(f"❌ Error publishing webshop {config_id} to Gateway: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # ORDERS FROM GATEWAY # ============================================================================ @router.get("/webshop/orders") async def get_webshop_orders(config_id: Optional[int] = None, status: Optional[str] = None): """ Hent importerede ordrer fra Gateway """ try: query = """ SELECT wo.*, wc.name as webshop_name, c.name as customer_name FROM webshop_orders wo LEFT JOIN webshop_configs wc ON wc.id = wo.webshop_config_id LEFT JOIN customers c ON c.id = wo.customer_id WHERE 1=1 """ params = [] if config_id: query += " AND wo.webshop_config_id = %s" params.append(config_id) if status: query += " AND wo.status = %s" params.append(status) query += " ORDER BY wo.created_at DESC" orders = execute_query(query, tuple(params) if params else None) return { "success": True, "orders": orders } except Exception as e: logger.error(f"❌ Error fetching webshop orders: {e}") raise HTTPException(status_code=500, detail=str(e))