feat: implement company-based ownership architecture

- 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
This commit is contained in:
2025-12-01 21:50:09 +01:00
parent 281181d7ea
commit 4ca738dc7f
7 changed files with 1018 additions and 19 deletions

128
app/exceptions/company.py Normal file
View File

@@ -0,0 +1,128 @@
# app/exceptions/company.py
"""
Company management specific exceptions.
"""
from typing import Any
from .base import (
AuthorizationException,
BusinessLogicException,
ConflictException,
ResourceNotFoundException,
ValidationException,
)
class CompanyNotFoundException(ResourceNotFoundException):
"""Raised when a company is not found."""
def __init__(self, company_identifier: str | int, identifier_type: str = "id"):
if identifier_type.lower() == "id":
message = f"Company with ID '{company_identifier}' not found"
else:
message = f"Company with name '{company_identifier}' not found"
super().__init__(
resource_type="Company",
identifier=str(company_identifier),
message=message,
error_code="COMPANY_NOT_FOUND",
)
class CompanyAlreadyExistsException(ConflictException):
"""Raised when trying to create a company that already exists."""
def __init__(self, company_name: str):
super().__init__(
message=f"Company with name '{company_name}' already exists",
error_code="COMPANY_ALREADY_EXISTS",
details={"company_name": company_name},
)
class CompanyNotActiveException(BusinessLogicException):
"""Raised when trying to perform operations on inactive company."""
def __init__(self, company_id: int):
super().__init__(
message=f"Company with ID '{company_id}' is not active",
error_code="COMPANY_NOT_ACTIVE",
details={"company_id": company_id},
)
class CompanyNotVerifiedException(BusinessLogicException):
"""Raised when trying to perform operations requiring verified company."""
def __init__(self, company_id: int):
super().__init__(
message=f"Company with ID '{company_id}' is not verified",
error_code="COMPANY_NOT_VERIFIED",
details={"company_id": company_id},
)
class UnauthorizedCompanyAccessException(AuthorizationException):
"""Raised when user tries to access company they don't own."""
def __init__(self, company_id: int, user_id: int | None = None):
details = {"company_id": company_id}
if user_id:
details["user_id"] = user_id
super().__init__(
message=f"Unauthorized access to company with ID '{company_id}'",
error_code="UNAUTHORIZED_COMPANY_ACCESS",
details=details,
)
class InvalidCompanyDataException(ValidationException):
"""Raised when company data is invalid or incomplete."""
def __init__(
self,
message: str = "Invalid company data",
field: str | None = None,
details: dict[str, Any] | None = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_COMPANY_DATA"
class CompanyValidationException(ValidationException):
"""Raised when company validation fails."""
def __init__(
self,
message: str = "Company validation failed",
field: str | None = None,
validation_errors: dict[str, str] | None = None,
):
details = {}
if validation_errors:
details["validation_errors"] = validation_errors
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "COMPANY_VALIDATION_FAILED"
class CompanyHasVendorsException(BusinessLogicException):
"""Raised when trying to delete a company that still has active vendors."""
def __init__(self, company_id: int, vendor_count: int):
super().__init__(
message=f"Cannot delete company with ID '{company_id}' because it has {vendor_count} associated vendor(s)",
error_code="COMPANY_HAS_VENDORS",
details={"company_id": company_id, "vendor_count": vendor_count},
)