import logging import json from datetime import datetime from typing import Any, Dict, List, Optional from fastapi import APIRouter, HTTPException, Query, Request from pydantic import BaseModel, Field from app.modules.orders.backend.economic_export import ordre_economic_export_service from app.modules.orders.backend.service import aggregate_order_lines logger = logging.getLogger(__name__) router = APIRouter() class OrdreLineInput(BaseModel): line_key: str source_type: str source_id: int description: str quantity: float = Field(gt=0) unit_price: float = Field(ge=0) discount_percentage: float = Field(default=0, ge=0, le=100) unit: Optional[str] = None product_id: Optional[int] = None selected: bool = True class OrdreExportRequest(BaseModel): customer_id: int lines: List[OrdreLineInput] notes: Optional[str] = None layout_number: Optional[int] = None draft_id: Optional[int] = None class OrdreDraftUpsertRequest(BaseModel): title: str = Field(min_length=1, max_length=120) customer_id: Optional[int] = None lines: List[Dict[str, Any]] = Field(default_factory=list) notes: Optional[str] = None layout_number: Optional[int] = None def _safe_json_field(value: Any) -> Any: if value is None: return None if isinstance(value, (dict, list)): return value if isinstance(value, str): try: return json.loads(value) except json.JSONDecodeError: return value return value def _get_user_id_from_request(http_request: Request) -> Optional[int]: state_user_id = getattr(http_request.state, "user_id", None) if state_user_id is None: return None try: return int(state_user_id) except (TypeError, ValueError): return None @router.get("/ordre/aggregate") async def get_ordre_aggregate( customer_id: Optional[int] = Query(None), sag_id: Optional[int] = Query(None), q: Optional[str] = Query(None), ): """Aggregate global ordre lines from subscriptions, hardware and sales.""" try: return aggregate_order_lines(customer_id=customer_id, sag_id=sag_id, q=q) except Exception as e: logger.error("❌ Error aggregating ordre lines: %s", e, exc_info=True) raise HTTPException(status_code=500, detail="Failed to aggregate ordre lines") @router.get("/ordre/config") async def get_ordre_config(): """Return ordre module safety config for frontend banner.""" return { "economic_read_only": ordre_economic_export_service.read_only, "economic_dry_run": ordre_economic_export_service.dry_run, "default_layout": ordre_economic_export_service.default_layout, "default_product": ordre_economic_export_service.default_product, } @router.post("/ordre/export") async def export_ordre(request: OrdreExportRequest, http_request: Request): """Export selected ordre lines to e-conomic draft order.""" try: user_id = _get_user_id_from_request(http_request) line_payload = [line.model_dump() for line in request.lines] export_result = await ordre_economic_export_service.export_order( customer_id=request.customer_id, lines=line_payload, notes=request.notes, layout_number=request.layout_number, user_id=user_id, ) exported_line_keys = [line.get("line_key") for line in line_payload if line.get("line_key")] export_result["exported_line_keys"] = exported_line_keys if request.draft_id: from app.core.database import execute_query_single, execute_query existing = execute_query_single("SELECT export_status_json FROM ordre_drafts WHERE id = %s", (request.draft_id,)) existing_status = _safe_json_field((existing or {}).get("export_status_json")) or {} if not isinstance(existing_status, dict): existing_status = {} line_status = "dry-run" if export_result.get("dry_run") else "exported" for line_key in exported_line_keys: existing_status[line_key] = { "status": line_status, "timestamp": datetime.utcnow().isoformat(), } execute_query( """ UPDATE ordre_drafts SET export_status_json = %s::jsonb, last_exported_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = %s """, (json.dumps(existing_status, ensure_ascii=False), request.draft_id), ) return export_result except HTTPException: raise except Exception as e: logger.error("❌ Error exporting ordre to e-conomic: %s", e, exc_info=True) raise HTTPException(status_code=500, detail="Failed to export ordre") @router.get("/ordre/drafts") async def list_ordre_drafts( http_request: Request, limit: int = Query(25, ge=1, le=100) ): """List all ordre drafts (no user filtering).""" try: query = """ SELECT id, title, customer_id, notes, layout_number, created_by_user_id, created_at, updated_at, last_exported_at FROM ordre_drafts ORDER BY updated_at DESC, id DESC LIMIT %s """ params = (limit,) from app.core.database import execute_query return execute_query(query, params) or [] except Exception as e: logger.error("❌ Error listing ordre drafts: %s", e, exc_info=True) raise HTTPException(status_code=500, detail="Failed to list ordre drafts") @router.get("/ordre/drafts/{draft_id}") async def get_ordre_draft(draft_id: int, http_request: Request): """Get single ordre draft with lines payload (no user filtering).""" try: query = "SELECT * FROM ordre_drafts WHERE id = %s LIMIT 1" params = (draft_id,) from app.core.database import execute_query_single draft = execute_query_single(query, params) if not draft: raise HTTPException(status_code=404, detail="Draft not found") draft["lines_json"] = _safe_json_field(draft.get("lines_json")) or [] draft["export_status_json"] = _safe_json_field(draft.get("export_status_json")) or {} return draft except HTTPException: raise except Exception as e: logger.error("❌ Error fetching ordre draft: %s", e, exc_info=True) raise HTTPException(status_code=500, detail="Failed to fetch ordre draft") @router.post("/ordre/drafts") async def create_ordre_draft(request: OrdreDraftUpsertRequest, http_request: Request): """Create a new ordre draft.""" try: user_id = _get_user_id_from_request(http_request) from app.core.database import execute_query query = """ INSERT INTO ordre_drafts ( title, customer_id, lines_json, notes, layout_number, created_by_user_id, export_status_json, updated_at ) VALUES (%s, %s, %s::jsonb, %s, %s, %s, %s::jsonb, CURRENT_TIMESTAMP) RETURNING * """ params = ( request.title, request.customer_id, json.dumps(request.lines, ensure_ascii=False), request.notes, request.layout_number, user_id, json.dumps({}, ensure_ascii=False), ) result = execute_query(query, params) return result[0] except Exception as e: logger.error("❌ Error creating ordre draft: %s", e, exc_info=True) raise HTTPException(status_code=500, detail="Failed to create ordre draft") @router.patch("/ordre/drafts/{draft_id}") async def update_ordre_draft(draft_id: int, request: OrdreDraftUpsertRequest, http_request: Request): """Update existing ordre draft.""" try: from app.core.database import execute_query query = """ UPDATE ordre_drafts SET title = %s, customer_id = %s, lines_json = %s::jsonb, notes = %s, layout_number = %s, updated_at = CURRENT_TIMESTAMP WHERE id = %s RETURNING * """ params = ( request.title, request.customer_id, json.dumps(request.lines, ensure_ascii=False), request.notes, request.layout_number, draft_id, ) result = execute_query(query, params) if not result: raise HTTPException(status_code=404, detail="Draft not found") return result[0] except HTTPException: raise except Exception as e: logger.error("❌ Error updating ordre draft: %s", e, exc_info=True) raise HTTPException(status_code=500, detail="Failed to update ordre draft") @router.delete("/ordre/drafts/{draft_id}") async def delete_ordre_draft(draft_id: int, http_request: Request): """Delete ordre draft.""" try: from app.core.database import execute_query query = "DELETE FROM ordre_drafts WHERE id = %s RETURNING id" params = (draft_id,) result = execute_query(query, params) if not result: raise HTTPException(status_code=404, detail="Draft not found") return {"status": "deleted", "id": draft_id} except HTTPException: raise except Exception as e: logger.error("❌ Error deleting ordre draft: %s", e, exc_info=True) raise HTTPException(status_code=500, detail="Failed to delete ordre draft")