445 lines
15 KiB
Python
445 lines
15 KiB
Python
# 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_api
|
|
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_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: 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_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, 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_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}
|