# app/api/v1/admin/vendors.py """ Vendor management endpoints for admin. """ import logging from datetime import UTC from fastapi import APIRouter, Body, Depends, Path, Query from sqlalchemy import func from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db from app.exceptions import ConfirmationRequiredException, VendorNotFoundException from app.services.admin_service import admin_service from app.services.stats_service import stats_service from models.database.user import User from models.database.vendor import Vendor from models.schema.stats import VendorStatsResponse from models.schema.vendor import ( VendorCreate, VendorCreateResponse, VendorDetailResponse, VendorListResponse, VendorTransferOwnership, VendorTransferOwnershipResponse, VendorUpdate, ) 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_api), ): """ 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: str | None = Query(None, description="Search by name or vendor code"), is_active: bool | None = Query(None), is_verified: bool | None = Query(None), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """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_api), ): """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_api), ): """ 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_api), ): """ 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_api), ): """ 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 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(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_api), ): """ 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_api), ): """ 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_api), ): """ 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}