""" Vendors API Router Endpoints for managing suppliers and vendors """ from fastapi import APIRouter, HTTPException, Query from typing import List, Optional from pydantic import BaseModel from app.models.schemas import Vendor, VendorCreate, VendorUpdate from app.core.database import execute_query, execute_query_single, execute_update import logging logger = logging.getLogger(__name__) router = APIRouter() def _ensure_customer_supplier_tag(customer_id: int) -> None: """Ensure linked customers are tagged as suppliers.""" try: tag = execute_query_single( "SELECT id FROM tags WHERE LOWER(name) = 'supplier' AND type = 'category' LIMIT 1" ) if tag and tag.get("id") is not None: tag_id = int(tag["id"]) else: created = execute_query_single( """ INSERT INTO tags (name, type, description, color, is_active) VALUES (%s, %s, %s, %s, %s) ON CONFLICT (name, type) DO UPDATE SET is_active = TRUE, updated_at = CURRENT_TIMESTAMP RETURNING id """, ("Supplier", "category", "Customer also acts as supplier", "#0f4c75", True), ) tag_id = int(created["id"]) if created and created.get("id") is not None else None if not tag_id: return execute_query( """ INSERT INTO entity_tags (entity_type, entity_id, tag_id) VALUES (%s, %s, %s) ON CONFLICT (entity_type, entity_id, tag_id) DO NOTHING """, ("customer", customer_id, tag_id), ) except Exception as tag_error: logger.warning("⚠️ Could not ensure supplier tag for customer %s: %s", customer_id, tag_error) class VendorCustomerLinkCreate(BaseModel): customer_id: int relationship_type: Optional[str] = "supplier" @router.get("/vendors", response_model=List[Vendor], tags=["Vendors"]) async def list_vendors( search: Optional[str] = Query(None, description="Search by name, CVR, or domain"), category: Optional[str] = Query(None, description="Filter by category"), is_active: Optional[bool] = Query(None, description="Filter by active status"), skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100) ): """Get list of vendors with optional filtering""" query = "SELECT * FROM vendors WHERE 1=1" params = [] if search: query += " AND (name ILIKE %s OR cvr_number ILIKE %s OR domain ILIKE %s)" search_param = f"%{search}%" params.extend([search_param, search_param, search_param]) if category: query += " AND category = %s" params.append(category) if is_active is not None: query += " AND is_active = %s" params.append(is_active) query += " ORDER BY name LIMIT %s OFFSET %s" params.extend([limit, skip]) result = execute_query(query, tuple(params)) return result or [] @router.get("/vendors/{vendor_id}", response_model=Vendor, tags=["Vendors"]) async def get_vendor(vendor_id: int): """Get vendor by ID""" query = "SELECT * FROM vendors WHERE id = %s" result = execute_query(query, (vendor_id,)) if not result or len(result) == 0: raise HTTPException(status_code=404, detail="Vendor not found") return result[0] @router.post("/vendors", response_model=Vendor, tags=["Vendors"]) async def create_vendor(vendor: VendorCreate): """Create a new vendor""" try: query = """ INSERT INTO vendors ( name, cvr_number, email, phone, address, postal_code, city, website, domain, email_pattern, category, priority, notes, is_active ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING * """ params = ( vendor.name, vendor.cvr_number, vendor.email, vendor.phone, vendor.address, vendor.postal_code, vendor.city, vendor.website, vendor.domain, vendor.email_pattern, vendor.category, vendor.priority, vendor.notes, vendor.is_active ) result = execute_query(query, params) if not result or len(result) == 0: raise HTTPException(status_code=500, detail="Failed to create vendor") logger.info(f"✅ Created vendor: {vendor.name}") return result[0] except Exception as e: logger.error(f"❌ Error creating vendor: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.put("/vendors/{vendor_id}", response_model=Vendor, tags=["Vendors"]) async def update_vendor(vendor_id: int, vendor: VendorUpdate): """Update an existing vendor""" # Check if vendor exists existing = execute_query("SELECT id FROM vendors WHERE id = %s", (vendor_id,)) if not existing: raise HTTPException(status_code=404, detail="Vendor not found") # Build update query update_fields = [] params = [] if vendor.name is not None: update_fields.append("name = %s") params.append(vendor.name) if vendor.cvr_number is not None: update_fields.append("cvr_number = %s") params.append(vendor.cvr_number) if vendor.email is not None: update_fields.append("email = %s") params.append(vendor.email) if vendor.phone is not None: update_fields.append("phone = %s") params.append(vendor.phone) if vendor.address is not None: update_fields.append("address = %s") params.append(vendor.address) if vendor.postal_code is not None: update_fields.append("postal_code = %s") params.append(vendor.postal_code) if vendor.city is not None: update_fields.append("city = %s") params.append(vendor.city) if vendor.website is not None: update_fields.append("website = %s") params.append(vendor.website) if vendor.domain is not None: update_fields.append("domain = %s") params.append(vendor.domain) if vendor.email_pattern is not None: update_fields.append("email_pattern = %s") params.append(vendor.email_pattern) if vendor.category is not None: update_fields.append("category = %s") params.append(vendor.category) if vendor.priority is not None: update_fields.append("priority = %s") params.append(vendor.priority) if vendor.notes is not None: update_fields.append("notes = %s") params.append(vendor.notes) if vendor.is_active is not None: update_fields.append("is_active = %s") params.append(vendor.is_active) if not update_fields: raise HTTPException(status_code=400, detail="No fields to update") params.append(vendor_id) query = f"UPDATE vendors SET {', '.join(update_fields)} WHERE id = %s RETURNING *" try: result = execute_query(query, tuple(params)) if not result: raise HTTPException(status_code=500, detail="Failed to update vendor") logger.info(f"✅ Updated vendor: {vendor_id}") return result[0] except Exception as e: logger.error(f"❌ Error updating vendor: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/vendors/{vendor_id}", tags=["Vendors"]) async def delete_vendor(vendor_id: int): """Soft delete a vendor (set is_active = false)""" query = "UPDATE vendors SET is_active = false WHERE id = %s RETURNING id" result = execute_query(query, (vendor_id,)) if not result: raise HTTPException(status_code=404, detail="Vendor not found") logger.info(f"✅ Deleted vendor: {vendor_id}") return {"message": "Vendor deleted successfully"} @router.get("/vendors/{vendor_id}/customers", tags=["Vendors"]) async def list_vendor_customers(vendor_id: int): """List customers linked to a vendor.""" vendor = execute_query_single("SELECT id FROM vendors WHERE id = %s", (vendor_id,)) if not vendor: raise HTTPException(status_code=404, detail="Vendor not found") rows = execute_query( """ SELECT l.id, l.customer_id, l.vendor_id, l.relationship_type, l.created_at, l.updated_at, c.name AS customer_name, c.email AS customer_email, c.cvr_number AS customer_cvr FROM customer_vendor_links l JOIN customers c ON c.id = l.customer_id WHERE l.vendor_id = %s ORDER BY c.name ASC, l.id ASC """, (vendor_id,), ) or [] return rows @router.post("/vendors/{vendor_id}/customers", tags=["Vendors"]) async def link_vendor_to_customer(vendor_id: int, payload: VendorCustomerLinkCreate): """Create link between vendor and customer.""" vendor = execute_query_single("SELECT id FROM vendors WHERE id = %s", (vendor_id,)) if not vendor: raise HTTPException(status_code=404, detail="Vendor not found") customer = execute_query_single("SELECT id FROM customers WHERE id = %s", (payload.customer_id,)) if not customer: raise HTTPException(status_code=404, detail="Customer not found") relationship_type = str(payload.relationship_type or "supplier").strip().lower() if relationship_type not in {"supplier", "reseller", "partner"}: raise HTTPException(status_code=400, detail="relationship_type must be supplier, reseller, or partner") row = execute_query_single( """ INSERT INTO customer_vendor_links (customer_id, vendor_id, relationship_type) VALUES (%s, %s, %s) ON CONFLICT (customer_id, vendor_id) DO UPDATE SET relationship_type = EXCLUDED.relationship_type, updated_at = CURRENT_TIMESTAMP RETURNING id, customer_id, vendor_id, relationship_type, created_at, updated_at """, (payload.customer_id, vendor_id, relationship_type), ) _ensure_customer_supplier_tag(int(payload.customer_id)) return row @router.delete("/vendors/{vendor_id}/customers/{customer_id}", tags=["Vendors"]) async def unlink_vendor_from_customer(vendor_id: int, customer_id: int): """Delete link between vendor and customer.""" deleted = execute_update( "DELETE FROM customer_vendor_links WHERE vendor_id = %s AND customer_id = %s", (vendor_id, customer_id), ) if not deleted: raise HTTPException(status_code=404, detail="Link not found") return {"success": True, "vendor_id": vendor_id, "customer_id": customer_id}