""" 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 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 # ============================================================================ # 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)) # ============================================================================ # WEBSHOP PRODUCT ENDPOINTS # ============================================================================ @router.post("/webshop/products") async def add_webshop_product(product: WebshopProductCreate): """ Tilføj produkt til webshop whitelist """ try: 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))