# app/api/v1/admin/vendors.py """ Vendor management endpoints for admin. """ import logging from typing import Optional from fastapi import APIRouter, Depends, Query, Path, Body from sqlalchemy.orm import Session from app.api.deps import get_current_admin_user from app.core.database import get_db from app.services.admin_service import admin_service from app.services.stats_service import stats_service from app.exceptions import VendorNotFoundException, ConfirmationRequiredException from models.schema.stats import VendorStatsResponse from models.schema.vendor import ( VendorListResponse, VendorResponse, VendorDetailResponse, VendorCreate, VendorCreateResponse, VendorUpdate, VendorTransferOwnership, VendorTransferOwnershipResponse, ) from models.database.user import User from models.database.vendor import Vendor from sqlalchemy import func router = APIRouter(prefix="/vendors") logger = logging.getLogger(__name__) def _get_vendor_by_identifier(db: Session, identifier: str) -> Vendor: """ Helper to get vendor by ID or vendor_code. Follows the pattern from admin_service._get_vendor_by_id_or_raise. Args: db: Database session identifier: Either vendor ID (int as string) or vendor_code (string) Returns: Vendor object Raises: VendorNotFoundException: If vendor not found """ # Try as integer ID first try: vendor_id = int(identifier) return admin_service.get_vendor_by_id(db, vendor_id) except (ValueError, TypeError): # Not an integer, treat as vendor_code pass except VendorNotFoundException: # ID not found, try as vendor_code pass # Try as vendor_code (case-insensitive) vendor = db.query(Vendor).filter( func.upper(Vendor.vendor_code) == identifier.upper() ).first() if not vendor: raise VendorNotFoundException(identifier, identifier_type="code") return vendor @router.post("", response_model=VendorCreateResponse) def create_vendor_with_owner( vendor_data: VendorCreate, db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_user), ): """ Create a new vendor with owner user account (Admin only). This endpoint: 1. Creates a new vendor record 2. Creates an owner user account with owner_email 3. Sets contact_email (defaults to owner_email if not provided) 4. Sets up default roles (Owner, Manager, Editor, Viewer) 5. Returns credentials (temporary password shown ONCE) **Email Fields:** - `owner_email`: Used for owner's login/authentication (stored in users.email) - `contact_email`: Public business contact (stored in vendors.contact_email) Returns vendor details with owner credentials. """ vendor, owner_user, temp_password = admin_service.create_vendor_with_owner( db=db, vendor_data=vendor_data ) return VendorCreateResponse( # Vendor fields id=vendor.id, vendor_code=vendor.vendor_code, subdomain=vendor.subdomain, name=vendor.name, description=vendor.description, owner_user_id=vendor.owner_user_id, contact_email=vendor.contact_email, contact_phone=vendor.contact_phone, website=vendor.website, business_address=vendor.business_address, tax_number=vendor.tax_number, letzshop_csv_url_fr=vendor.letzshop_csv_url_fr, letzshop_csv_url_en=vendor.letzshop_csv_url_en, letzshop_csv_url_de=vendor.letzshop_csv_url_de, is_active=vendor.is_active, is_verified=vendor.is_verified, created_at=vendor.created_at, updated_at=vendor.updated_at, # Owner credentials owner_email=owner_user.email, owner_username=owner_user.username, temporary_password=temp_password, login_url=f"http://localhost:8000/vendor/{vendor.subdomain}/login" ) @router.get("", response_model=VendorListResponse) def get_all_vendors_admin( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), search: Optional[str] = Query(None, description="Search by name or vendor code"), is_active: Optional[bool] = Query(None), is_verified: Optional[bool] = Query(None), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_user), ): """Get all vendors with filtering (Admin only).""" vendors, total = admin_service.get_all_vendors( db=db, skip=skip, limit=limit, search=search, is_active=is_active, is_verified=is_verified ) return VendorListResponse(vendors=vendors, total=total, skip=skip, limit=limit) @router.get("/stats", response_model=VendorStatsResponse) def get_vendor_statistics_endpoint( db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_user), ): """Get vendor statistics for admin dashboard (Admin only).""" stats = stats_service.get_vendor_statistics(db) return VendorStatsResponse( total=stats.get("total_vendors", 0), verified=stats.get("verified_vendors", 0), pending=stats.get("pending_vendors", 0), inactive=stats.get("inactive_vendors", 0), ) @router.get("/{vendor_identifier}", response_model=VendorDetailResponse) def get_vendor_details( vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_user), ): """ Get detailed vendor information including owner details (Admin only). Accepts either vendor ID (integer) or vendor_code (string). Returns both: - `contact_email` (business contact) - `owner_email` (owner's authentication email) """ vendor = _get_vendor_by_identifier(db, vendor_identifier) return VendorDetailResponse( # Vendor fields id=vendor.id, vendor_code=vendor.vendor_code, subdomain=vendor.subdomain, name=vendor.name, description=vendor.description, owner_user_id=vendor.owner_user_id, contact_email=vendor.contact_email, contact_phone=vendor.contact_phone, website=vendor.website, business_address=vendor.business_address, tax_number=vendor.tax_number, letzshop_csv_url_fr=vendor.letzshop_csv_url_fr, letzshop_csv_url_en=vendor.letzshop_csv_url_en, letzshop_csv_url_de=vendor.letzshop_csv_url_de, is_active=vendor.is_active, is_verified=vendor.is_verified, created_at=vendor.created_at, updated_at=vendor.updated_at, # Owner details owner_email=vendor.owner.email, owner_username=vendor.owner.username, ) @router.put("/{vendor_identifier}", response_model=VendorDetailResponse) def update_vendor( vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"), vendor_update: VendorUpdate = Body(...), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_user), ): """ Update vendor information (Admin only). Accepts either vendor ID (integer) or vendor_code (string). **Can update:** - Basic info: name, description, subdomain - Business contact: contact_email, contact_phone, website - Business details: business_address, tax_number - Marketplace URLs - Status: is_active, is_verified **Cannot update:** - `owner_email` (use POST /vendors/{id}/transfer-ownership) - `vendor_code` (immutable) - `owner_user_id` (use POST /vendors/{id}/transfer-ownership) """ vendor = _get_vendor_by_identifier(db, vendor_identifier) vendor = admin_service.update_vendor(db, vendor.id, vendor_update) return VendorDetailResponse( id=vendor.id, vendor_code=vendor.vendor_code, subdomain=vendor.subdomain, name=vendor.name, description=vendor.description, owner_user_id=vendor.owner_user_id, contact_email=vendor.contact_email, contact_phone=vendor.contact_phone, website=vendor.website, business_address=vendor.business_address, tax_number=vendor.tax_number, letzshop_csv_url_fr=vendor.letzshop_csv_url_fr, letzshop_csv_url_en=vendor.letzshop_csv_url_en, letzshop_csv_url_de=vendor.letzshop_csv_url_de, is_active=vendor.is_active, is_verified=vendor.is_verified, created_at=vendor.created_at, updated_at=vendor.updated_at, owner_email=vendor.owner.email, owner_username=vendor.owner.username, ) @router.post("/{vendor_identifier}/transfer-ownership", response_model=VendorTransferOwnershipResponse) def transfer_vendor_ownership( vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"), transfer_data: VendorTransferOwnership = Body(...), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_user), ): """ Transfer vendor ownership to another user (Admin only). Accepts either vendor ID (integer) or vendor_code (string). **This is a critical operation that:** - Changes the owner_user_id - Assigns new owner to "Owner" role - Demotes old owner to "Manager" role (or removes them) - Creates audit trail ⚠️ **This action is logged and should be used carefully.** **Requires:** - `new_owner_user_id`: ID of user who will become owner - `confirm_transfer`: Must be true - `transfer_reason`: Optional reason for audit trail """ from datetime import datetime, timezone vendor = _get_vendor_by_identifier(db, vendor_identifier) vendor, old_owner, new_owner = admin_service.transfer_vendor_ownership( db, vendor.id, transfer_data ) return VendorTransferOwnershipResponse( message="Ownership transferred successfully", vendor_id=vendor.id, vendor_code=vendor.vendor_code, vendor_name=vendor.name, old_owner={ "id": old_owner.id, "username": old_owner.username, "email": old_owner.email, }, new_owner={ "id": new_owner.id, "username": new_owner.username, "email": new_owner.email, }, transferred_at=datetime.now(timezone.utc), transfer_reason=transfer_data.transfer_reason, ) @router.put("/{vendor_identifier}/verification", response_model=VendorDetailResponse) def toggle_vendor_verification( vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"), verification_data: dict = Body(..., example={"is_verified": True}), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_user), ): """ Toggle vendor verification status (Admin only). Accepts either vendor ID (integer) or vendor_code (string). Request body: { "is_verified": true/false } """ vendor = _get_vendor_by_identifier(db, vendor_identifier) # Use admin_service method if available, otherwise update directly if "is_verified" in verification_data: try: vendor, message = admin_service.verify_vendor(db, vendor.id) logger.info(f"Vendor verification toggled: {message}") except AttributeError: # If verify_vendor method doesn't exist, update directly vendor.is_verified = verification_data["is_verified"] db.commit() db.refresh(vendor) return VendorDetailResponse( id=vendor.id, vendor_code=vendor.vendor_code, subdomain=vendor.subdomain, name=vendor.name, description=vendor.description, owner_user_id=vendor.owner_user_id, contact_email=vendor.contact_email, contact_phone=vendor.contact_phone, website=vendor.website, business_address=vendor.business_address, tax_number=vendor.tax_number, letzshop_csv_url_fr=vendor.letzshop_csv_url_fr, letzshop_csv_url_en=vendor.letzshop_csv_url_en, letzshop_csv_url_de=vendor.letzshop_csv_url_de, is_active=vendor.is_active, is_verified=vendor.is_verified, created_at=vendor.created_at, updated_at=vendor.updated_at, owner_email=vendor.owner.email, owner_username=vendor.owner.username, ) @router.put("/{vendor_identifier}/status", response_model=VendorDetailResponse) def toggle_vendor_status( vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"), status_data: dict = Body(..., example={"is_active": True}), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_user), ): """ Toggle vendor active status (Admin only). Accepts either vendor ID (integer) or vendor_code (string). Request body: { "is_active": true/false } """ vendor = _get_vendor_by_identifier(db, vendor_identifier) # Use admin_service method if available, otherwise update directly if "is_active" in status_data: try: vendor, message = admin_service.toggle_vendor_status(db, vendor.id) logger.info(f"Vendor status toggled: {message}") except AttributeError: # If toggle_vendor_status method doesn't exist, update directly vendor.is_active = status_data["is_active"] db.commit() db.refresh(vendor) return VendorDetailResponse( id=vendor.id, vendor_code=vendor.vendor_code, subdomain=vendor.subdomain, name=vendor.name, description=vendor.description, owner_user_id=vendor.owner_user_id, contact_email=vendor.contact_email, contact_phone=vendor.contact_phone, website=vendor.website, business_address=vendor.business_address, tax_number=vendor.tax_number, letzshop_csv_url_fr=vendor.letzshop_csv_url_fr, letzshop_csv_url_en=vendor.letzshop_csv_url_en, letzshop_csv_url_de=vendor.letzshop_csv_url_de, is_active=vendor.is_active, is_verified=vendor.is_verified, created_at=vendor.created_at, updated_at=vendor.updated_at, owner_email=vendor.owner.email, owner_username=vendor.owner.username, ) @router.delete("/{vendor_identifier}") def delete_vendor( vendor_identifier: str = Path(..., description="Vendor ID or vendor_code"), confirm: bool = Query(False, description="Must be true to confirm deletion"), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_user), ): """ Delete vendor and all associated data (Admin only). Accepts either vendor ID (integer) or vendor_code (string). ⚠️ **WARNING: This is destructive and will delete:** - Vendor account - All products - All orders - All customers - All team members Requires confirmation parameter: `confirm=true` """ # Raise custom exception instead of HTTPException if not confirm: raise ConfirmationRequiredException( operation="delete_vendor", message="Deletion requires confirmation parameter: confirm=true" ) vendor = _get_vendor_by_identifier(db, vendor_identifier) message = admin_service.delete_vendor(db, vendor.id) return {"message": message}