2026-01-25 03:29:28 +01:00
|
|
|
"""
|
|
|
|
|
Webshop Module - API Router
|
|
|
|
|
Backend endpoints for webshop administration og konfiguration
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
|
|
|
|
|
from typing import List, Optional
|
2026-02-06 10:47:14 +01:00
|
|
|
from pydantic import BaseModel, field_validator
|
2026-01-25 03:29:28 +01:00
|
|
|
from datetime import datetime
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
import shutil
|
|
|
|
|
|
|
|
|
|
from app.core.database import execute_query, execute_insert, execute_update, execute_query_single
|
2026-03-20 00:24:58 +01:00
|
|
|
from app.core.config import settings
|
2026-01-25 03:29:28 +01:00
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
# APIRouter instance (module_loader kigger efter denne)
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
2026-03-20 00:24:58 +01:00
|
|
|
# Upload directory for logos (works in both Docker and local development)
|
|
|
|
|
_logo_base_dir = os.path.abspath(settings.UPLOAD_DIR)
|
|
|
|
|
LOGO_UPLOAD_DIR = os.path.join(_logo_base_dir, "webshop_logos")
|
|
|
|
|
try:
|
|
|
|
|
os.makedirs(LOGO_UPLOAD_DIR, exist_ok=True)
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
if _logo_base_dir.startswith('/app/'):
|
|
|
|
|
_fallback_base = os.path.abspath('uploads')
|
|
|
|
|
LOGO_UPLOAD_DIR = os.path.join(_fallback_base, "webshop_logos")
|
|
|
|
|
os.makedirs(LOGO_UPLOAD_DIR, exist_ok=True)
|
|
|
|
|
logger.warning(
|
|
|
|
|
"⚠️ Webshop logo dir %s not writable (%s). Using fallback %s",
|
|
|
|
|
_logo_base_dir,
|
|
|
|
|
exc,
|
|
|
|
|
LOGO_UPLOAD_DIR,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
raise
|
2026-01-25 03:29:28 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================================
|
|
|
|
|
# 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
|
2026-02-06 10:47:14 +01:00
|
|
|
|
|
|
|
|
@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
|
2026-01-25 03:29:28 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================================
|
|
|
|
|
# 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))
|
|
|
|
|
|
|
|
|
|
|
2026-01-28 14:37:47 +01:00
|
|
|
# ==========================================================================
|
|
|
|
|
# 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))
|
|
|
|
|
|
|
|
|
|
|
2026-01-25 03:29:28 +01:00
|
|
|
# ============================================================================
|
|
|
|
|
# WEBSHOP PRODUCT ENDPOINTS
|
|
|
|
|
# ============================================================================
|
|
|
|
|
|
|
|
|
|
@router.post("/webshop/products")
|
|
|
|
|
async def add_webshop_product(product: WebshopProductCreate):
|
|
|
|
|
"""
|
|
|
|
|
Tilføj produkt til webshop whitelist
|
|
|
|
|
"""
|
|
|
|
|
try:
|
2026-02-06 10:47:14 +01:00
|
|
|
# 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."
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-25 03:29:28 +01:00
|
|
|
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))
|