- Add database migration to make vendor.owner_user_id nullable - Update Vendor model to support company-based ownership (DEPRECATED vendor.owner_user_id) - Implement company_service with singleton pattern (consistent with vendor_service) - Create Company model with proper relationships to vendors and users - Add company exception classes for proper error handling - Refactor companies API to use singleton service pattern Architecture Change: - OLD: Each vendor has its own owner (vendor.owner_user_id) - NEW: Vendors belong to a company, company has one owner (company.owner_user_id) - This allows one company owner to manage multiple vendor brands Technical Details: - Company service uses singleton pattern (not factory) - Company service accepts db: Session as parameter (follows SVC-003) - Uses AuthManager for password hashing (consistent with admin_service) - Added _generate_temp_password() helper method
298 lines
9.7 KiB
Python
298 lines
9.7 KiB
Python
# app/api/v1/admin/companies.py
|
|
"""
|
|
Company management endpoints for admin.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Body, Depends, Path, Query
|
|
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 CompanyHasVendorsException, ConfirmationRequiredException
|
|
from app.services.company_service import company_service
|
|
from models.database.user import User
|
|
from models.schema.company import (
|
|
CompanyCreate,
|
|
CompanyCreateResponse,
|
|
CompanyDetailResponse,
|
|
CompanyListResponse,
|
|
CompanyResponse,
|
|
CompanyUpdate,
|
|
)
|
|
|
|
router = APIRouter(prefix="/companies")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@router.post("", response_model=CompanyCreateResponse)
|
|
def create_company_with_owner(
|
|
company_data: CompanyCreate,
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""
|
|
Create a new company with owner user account (Admin only).
|
|
|
|
This endpoint:
|
|
1. Creates a new company record
|
|
2. Creates an owner user account with owner_email (if not exists)
|
|
3. Returns credentials (temporary password shown ONCE if new user created)
|
|
|
|
**Email Fields:**
|
|
- `owner_email`: Used for owner's login/authentication (stored in users.email)
|
|
- `contact_email`: Public business contact (stored in companies.contact_email)
|
|
|
|
Returns company details with owner credentials.
|
|
"""
|
|
company, owner_user, temp_password = company_service.create_company_with_owner(
|
|
db, company_data
|
|
)
|
|
|
|
db.commit()
|
|
|
|
return CompanyCreateResponse(
|
|
company=CompanyResponse(
|
|
id=company.id,
|
|
name=company.name,
|
|
description=company.description,
|
|
owner_user_id=company.owner_user_id,
|
|
contact_email=company.contact_email,
|
|
contact_phone=company.contact_phone,
|
|
website=company.website,
|
|
business_address=company.business_address,
|
|
tax_number=company.tax_number,
|
|
is_active=company.is_active,
|
|
is_verified=company.is_verified,
|
|
created_at=company.created_at.isoformat(),
|
|
updated_at=company.updated_at.isoformat(),
|
|
),
|
|
owner_user_id=owner_user.id,
|
|
owner_username=owner_user.username,
|
|
owner_email=owner_user.email,
|
|
temporary_password=temp_password or "N/A (Existing user)",
|
|
login_url=f"http://localhost:8000/admin/login",
|
|
)
|
|
|
|
|
|
@router.get("", response_model=CompanyListResponse)
|
|
def get_all_companies(
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(100, ge=1, le=1000),
|
|
search: str | None = Query(None, description="Search by company name"),
|
|
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 companies with filtering (Admin only)."""
|
|
companies, total = company_service.get_companies(
|
|
db,
|
|
skip=skip,
|
|
limit=limit,
|
|
search=search,
|
|
is_active=is_active,
|
|
is_verified=is_verified,
|
|
)
|
|
|
|
return CompanyListResponse(
|
|
companies=[
|
|
CompanyResponse(
|
|
id=c.id,
|
|
name=c.name,
|
|
description=c.description,
|
|
owner_user_id=c.owner_user_id,
|
|
contact_email=c.contact_email,
|
|
contact_phone=c.contact_phone,
|
|
website=c.website,
|
|
business_address=c.business_address,
|
|
tax_number=c.tax_number,
|
|
is_active=c.is_active,
|
|
is_verified=c.is_verified,
|
|
created_at=c.created_at.isoformat(),
|
|
updated_at=c.updated_at.isoformat(),
|
|
)
|
|
for c in companies
|
|
],
|
|
total=total,
|
|
skip=skip,
|
|
limit=limit,
|
|
)
|
|
|
|
|
|
@router.get("/{company_id}", response_model=CompanyDetailResponse)
|
|
def get_company_details(
|
|
company_id: int = Path(..., description="Company ID"),
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""
|
|
Get detailed company information including vendor counts (Admin only).
|
|
"""
|
|
company = company_service.get_company_by_id(db, company_id)
|
|
|
|
# Count vendors
|
|
vendor_count = len(company.vendors)
|
|
active_vendor_count = sum(1 for v in company.vendors if v.is_active)
|
|
|
|
return CompanyDetailResponse(
|
|
id=company.id,
|
|
name=company.name,
|
|
description=company.description,
|
|
owner_user_id=company.owner_user_id,
|
|
contact_email=company.contact_email,
|
|
contact_phone=company.contact_phone,
|
|
website=company.website,
|
|
business_address=company.business_address,
|
|
tax_number=company.tax_number,
|
|
is_active=company.is_active,
|
|
is_verified=company.is_verified,
|
|
created_at=company.created_at.isoformat(),
|
|
updated_at=company.updated_at.isoformat(),
|
|
vendor_count=vendor_count,
|
|
active_vendor_count=active_vendor_count,
|
|
)
|
|
|
|
|
|
@router.put("/{company_id}", response_model=CompanyResponse)
|
|
def update_company(
|
|
company_id: int = Path(..., description="Company ID"),
|
|
company_update: CompanyUpdate = Body(...),
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""
|
|
Update company information (Admin only).
|
|
|
|
**Can update:**
|
|
- Basic info: name, description
|
|
- Business contact: contact_email, contact_phone, website
|
|
- Business details: business_address, tax_number
|
|
- Status: is_active, is_verified
|
|
|
|
**Cannot update:**
|
|
- `owner_user_id` (would require ownership transfer feature)
|
|
"""
|
|
company = company_service.update_company(db, company_id, company_update)
|
|
db.commit()
|
|
|
|
return CompanyResponse(
|
|
id=company.id,
|
|
name=company.name,
|
|
description=company.description,
|
|
owner_user_id=company.owner_user_id,
|
|
contact_email=company.contact_email,
|
|
contact_phone=company.contact_phone,
|
|
website=company.website,
|
|
business_address=company.business_address,
|
|
tax_number=company.tax_number,
|
|
is_active=company.is_active,
|
|
is_verified=company.is_verified,
|
|
created_at=company.created_at.isoformat(),
|
|
updated_at=company.updated_at.isoformat(),
|
|
)
|
|
|
|
|
|
@router.put("/{company_id}/verification", response_model=CompanyResponse)
|
|
def toggle_company_verification(
|
|
company_id: int = Path(..., description="Company ID"),
|
|
verification_data: dict = Body(..., example={"is_verified": True}),
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""
|
|
Toggle company verification status (Admin only).
|
|
|
|
Request body: { "is_verified": true/false }
|
|
"""
|
|
is_verified = verification_data.get("is_verified", False)
|
|
company = company_service.toggle_verification(db, company_id, is_verified)
|
|
db.commit()
|
|
|
|
return CompanyResponse(
|
|
id=company.id,
|
|
name=company.name,
|
|
description=company.description,
|
|
owner_user_id=company.owner_user_id,
|
|
contact_email=company.contact_email,
|
|
contact_phone=company.contact_phone,
|
|
website=company.website,
|
|
business_address=company.business_address,
|
|
tax_number=company.tax_number,
|
|
is_active=company.is_active,
|
|
is_verified=company.is_verified,
|
|
created_at=company.created_at.isoformat(),
|
|
updated_at=company.updated_at.isoformat(),
|
|
)
|
|
|
|
|
|
@router.put("/{company_id}/status", response_model=CompanyResponse)
|
|
def toggle_company_status(
|
|
company_id: int = Path(..., description="Company ID"),
|
|
status_data: dict = Body(..., example={"is_active": True}),
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""
|
|
Toggle company active status (Admin only).
|
|
|
|
Request body: { "is_active": true/false }
|
|
"""
|
|
is_active = status_data.get("is_active", True)
|
|
company = company_service.toggle_active(db, company_id, is_active)
|
|
db.commit()
|
|
|
|
return CompanyResponse(
|
|
id=company.id,
|
|
name=company.name,
|
|
description=company.description,
|
|
owner_user_id=company.owner_user_id,
|
|
contact_email=company.contact_email,
|
|
contact_phone=company.contact_phone,
|
|
website=company.website,
|
|
business_address=company.business_address,
|
|
tax_number=company.tax_number,
|
|
is_active=company.is_active,
|
|
is_verified=company.is_verified,
|
|
created_at=company.created_at.isoformat(),
|
|
updated_at=company.updated_at.isoformat(),
|
|
)
|
|
|
|
|
|
@router.delete("/{company_id}")
|
|
def delete_company(
|
|
company_id: int = Path(..., description="Company ID"),
|
|
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 company and all associated vendors (Admin only).
|
|
|
|
⚠️ **WARNING: This is destructive and will delete:**
|
|
- Company account
|
|
- All vendors under this company
|
|
- All products under those vendors
|
|
- All orders, customers, team members
|
|
|
|
Requires confirmation parameter: `confirm=true`
|
|
"""
|
|
if not confirm:
|
|
raise ConfirmationRequiredException(
|
|
operation="delete_company",
|
|
message="Deletion requires confirmation parameter: confirm=true",
|
|
)
|
|
|
|
# Get company to check vendor count
|
|
company = company_service.get_company_by_id(db, company_id)
|
|
vendor_count = len(company.vendors)
|
|
|
|
if vendor_count > 0:
|
|
raise CompanyHasVendorsException(company_id, vendor_count)
|
|
|
|
company_service.delete_company(db, company_id)
|
|
db.commit()
|
|
|
|
return {"message": f"Company {company_id} deleted successfully"}
|