major refactoring adding vendor and customer features

This commit is contained in:
2025-10-11 09:09:25 +02:00
parent f569995883
commit dd16198276
126 changed files with 15109 additions and 3747 deletions

View File

@@ -38,12 +38,12 @@ from .marketplace_product import (
MarketplaceProductCSVImportException,
)
from .stock import (
StockNotFoundException,
InsufficientStockException,
InvalidStockOperationException,
StockValidationException,
NegativeStockException,
from .inventory import (
InventoryNotFoundException,
InsufficientInventoryException,
InvalidInventoryOperationException,
InventoryValidationException,
NegativeInventoryException,
InvalidQuantityException,
LocationNotFoundException
)
@@ -59,9 +59,50 @@ from .vendor import (
VendorValidationException,
)
from .customer import (
CustomerNotFoundException,
CustomerAlreadyExistsException,
DuplicateCustomerEmailException,
CustomerNotActiveException,
InvalidCustomerCredentialsException,
CustomerValidationException,
CustomerAuthorizationException,
)
from .team import (
TeamMemberNotFoundException,
TeamMemberAlreadyExistsException,
TeamInvitationNotFoundException,
TeamInvitationExpiredException,
TeamInvitationAlreadyAcceptedException,
UnauthorizedTeamActionException,
CannotRemoveOwnerException,
CannotModifyOwnRoleException,
RoleNotFoundException,
InvalidRoleException,
InsufficientPermissionsException,
MaxTeamMembersReachedException,
TeamValidationException,
InvalidInvitationDataException,
)
from .product import (
ProductNotFoundException,
ProductAlreadyExistsException,
ProductNotInCatalogException,
ProductNotActiveException,
InvalidProductDataException,
ProductValidationException,
CannotDeleteProductWithInventoryException,
CannotDeleteProductWithOrdersException,
)
from .order import (
OrderNotFoundException,
OrderAlreadyExistsException,
OrderValidationException,
InvalidOrderStatusException,
OrderCannotBeCancelledException,
)
from .marketplace_import_job import (
@@ -100,6 +141,7 @@ __all__ = [
"BusinessLogicException",
"ExternalServiceException",
"RateLimitException",
"ServiceUnavailableException",
# Auth exceptions
"InvalidCredentialsException",
@@ -110,20 +152,37 @@ __all__ = [
"AdminRequiredException",
"UserAlreadyExistsException",
# MarketplaceProduct exceptions
"MarketplaceProductNotFoundException",
"MarketplaceProductAlreadyExistsException",
"InvalidMarketplaceProductDataException",
"MarketplaceProductValidationException",
"InvalidGTINException",
"MarketplaceProductCSVImportException",
# Customer exceptions
"CustomerNotFoundException",
"CustomerAlreadyExistsException",
"DuplicateCustomerEmailException",
"CustomerNotActiveException",
"InvalidCustomerCredentialsException",
"CustomerValidationException",
"CustomerAuthorizationException",
# Stock exceptions
"StockNotFoundException",
"InsufficientStockException",
"InvalidStockOperationException",
"StockValidationException",
"NegativeStockException",
# Team exceptions
"TeamMemberNotFoundException",
"TeamMemberAlreadyExistsException",
"TeamInvitationNotFoundException",
"TeamInvitationExpiredException",
"TeamInvitationAlreadyAcceptedException",
"UnauthorizedTeamActionException",
"CannotRemoveOwnerException",
"CannotModifyOwnRoleException",
"RoleNotFoundException",
"InvalidRoleException",
"InsufficientPermissionsException",
"MaxTeamMembersReachedException",
"TeamValidationException",
"InvalidInvitationDataException",
# Inventory exceptions
"InventoryNotFoundException",
"InsufficientInventoryException",
"InvalidInventoryOperationException",
"InventoryValidationException",
"NegativeInventoryException",
"InvalidQuantityException",
"LocationNotFoundException",
@@ -138,8 +197,29 @@ __all__ = [
"VendorValidationException",
# Product exceptions
"ProductAlreadyExistsException",
"ProductNotFoundException",
"ProductAlreadyExistsException",
"ProductNotInCatalogException",
"ProductNotActiveException",
"InvalidProductDataException",
"ProductValidationException",
"CannotDeleteProductWithInventoryException",
"CannotDeleteProductWithOrdersException",
# Order exceptions
"OrderNotFoundException",
"OrderAlreadyExistsException",
"OrderValidationException",
"InvalidOrderStatusException",
"OrderCannotBeCancelledException",
# MarketplaceProduct exceptions
"MarketplaceProductNotFoundException",
"MarketplaceProductAlreadyExistsException",
"InvalidMarketplaceProductDataException",
"MarketplaceProductValidationException",
"InvalidGTINException",
"MarketplaceProductCSVImportException",
# Marketplace import exceptions
"MarketplaceImportException",

102
app/exceptions/customer.py Normal file
View File

@@ -0,0 +1,102 @@
# app/exceptions/customer.py
"""
Customer management specific exceptions.
"""
from typing import Any, Dict, Optional
from .base import (
ResourceNotFoundException,
ConflictException,
ValidationException,
AuthenticationException,
BusinessLogicException
)
class CustomerNotFoundException(ResourceNotFoundException):
"""Raised when a customer is not found."""
def __init__(self, customer_identifier: str):
super().__init__(
resource_type="Customer",
identifier=customer_identifier,
message=f"Customer '{customer_identifier}' not found",
error_code="CUSTOMER_NOT_FOUND"
)
class CustomerAlreadyExistsException(ConflictException):
"""Raised when trying to create a customer that already exists."""
def __init__(self, email: str):
super().__init__(
message=f"Customer with email '{email}' already exists",
error_code="CUSTOMER_ALREADY_EXISTS",
details={"email": email}
)
class DuplicateCustomerEmailException(ConflictException):
"""Raised when email already exists for vendor."""
def __init__(self, email: str, vendor_code: str):
super().__init__(
message=f"Email '{email}' is already registered for this vendor",
error_code="DUPLICATE_CUSTOMER_EMAIL",
details={
"email": email,
"vendor_code": vendor_code
}
)
class CustomerNotActiveException(BusinessLogicException):
"""Raised when trying to perform operations on inactive customer."""
def __init__(self, email: str):
super().__init__(
message=f"Customer account '{email}' is not active",
error_code="CUSTOMER_NOT_ACTIVE",
details={"email": email}
)
class InvalidCustomerCredentialsException(AuthenticationException):
"""Raised when customer credentials are invalid."""
def __init__(self):
super().__init__(
message="Invalid email or password",
error_code="INVALID_CUSTOMER_CREDENTIALS"
)
class CustomerValidationException(ValidationException):
"""Raised when customer data validation fails."""
def __init__(
self,
message: str = "Customer validation failed",
field: Optional[str] = None,
details: Optional[Dict[str, Any]] = None
):
super().__init__(
message=message,
field=field,
details=details
)
self.error_code = "CUSTOMER_VALIDATION_FAILED"
class CustomerAuthorizationException(BusinessLogicException):
"""Raised when customer is not authorized for operation."""
def __init__(self, customer_email: str, operation: str):
super().__init__(
message=f"Customer '{customer_email}' not authorized for: {operation}",
error_code="CUSTOMER_NOT_AUTHORIZED",
details={
"customer_email": customer_email,
"operation": operation
}
)

View File

@@ -1,31 +1,31 @@
# app/exceptions/stock.py
# app/exceptions/inventory.py
"""
Stock management specific exceptions.
Inventory management specific exceptions.
"""
from typing import Any, Dict, Optional
from .base import ResourceNotFoundException, ValidationException, BusinessLogicException
class StockNotFoundException(ResourceNotFoundException):
"""Raised when stock record is not found."""
class InventoryNotFoundException(ResourceNotFoundException):
"""Raised when inventory record is not found."""
def __init__(self, identifier: str, identifier_type: str = "ID"):
if identifier_type.lower() == "gtin":
message = f"No stock found for GTIN '{identifier}'"
message = f"No inventory found for GTIN '{identifier}'"
else:
message = f"Stock record with {identifier_type} '{identifier}' not found"
message = f"Inventory record with {identifier_type} '{identifier}' not found"
super().__init__(
resource_type="Stock",
resource_type="Inventory",
identifier=identifier,
message=message,
error_code="STOCK_NOT_FOUND",
error_code="INVENTORY_NOT_FOUND",
)
class InsufficientStockException(BusinessLogicException):
"""Raised when trying to remove more stock than available."""
class InsufficientInventoryException(BusinessLogicException):
"""Raised when trying to remove more inventory than available."""
def __init__(
self,
@@ -34,11 +34,11 @@ class InsufficientStockException(BusinessLogicException):
requested: int,
available: int,
):
message = f"Insufficient stock for GTIN '{gtin}' at '{location}'. Requested: {requested}, Available: {available}"
message = f"Insufficient inventory for GTIN '{gtin}' at '{location}'. Requested: {requested}, Available: {available}"
super().__init__(
message=message,
error_code="INSUFFICIENT_STOCK",
error_code="INSUFFICIENT_INVENTORY",
details={
"gtin": gtin,
"location": location,
@@ -48,8 +48,8 @@ class InsufficientStockException(BusinessLogicException):
)
class InvalidStockOperationException(ValidationException):
"""Raised when stock operation is invalid."""
class InvalidInventoryOperationException(ValidationException):
"""Raised when inventory operation is invalid."""
def __init__(
self,
@@ -67,15 +67,15 @@ class InvalidStockOperationException(ValidationException):
message=message,
details=details,
)
self.error_code = "INVALID_STOCK_OPERATION"
self.error_code = "INVALID_INVENTORY_OPERATION"
class StockValidationException(ValidationException):
"""Raised when stock data validation fails."""
class InventoryValidationException(ValidationException):
"""Raised when inventory data validation fails."""
def __init__(
self,
message: str = "Stock validation failed",
message: str = "Inventory validation failed",
field: Optional[str] = None,
validation_errors: Optional[Dict[str, str]] = None,
):
@@ -88,18 +88,18 @@ class StockValidationException(ValidationException):
field=field,
details=details,
)
self.error_code = "STOCK_VALIDATION_FAILED"
self.error_code = "INVENTORY_VALIDATION_FAILED"
class NegativeStockException(BusinessLogicException):
"""Raised when stock quantity would become negative."""
class NegativeInventoryException(BusinessLogicException):
"""Raised when inventory quantity would become negative."""
def __init__(self, gtin: str, location: str, resulting_quantity: int):
message = f"Stock operation would result in negative quantity ({resulting_quantity}) for GTIN '{gtin}' at '{location}'"
message = f"Inventory operation would result in negative quantity ({resulting_quantity}) for GTIN '{gtin}' at '{location}'"
super().__init__(
message=message,
error_code="NEGATIVE_STOCK_NOT_ALLOWED",
error_code="NEGATIVE_INVENTORY_NOT_ALLOWED",
details={
"gtin": gtin,
"location": location,
@@ -121,12 +121,12 @@ class InvalidQuantityException(ValidationException):
class LocationNotFoundException(ResourceNotFoundException):
"""Raised when stock location is not found."""
"""Raised when inventory location is not found."""
def __init__(self, location: str):
super().__init__(
resource_type="Location",
identifier=location,
message=f"Stock location '{location}' not found",
message=f"Inventory location '{location}' not found",
error_code="LOCATION_NOT_FOUND",
)

73
app/exceptions/order.py Normal file
View File

@@ -0,0 +1,73 @@
# app/exceptions/order.py
"""
Order management specific exceptions.
"""
from typing import Optional
from .base import (
ResourceNotFoundException,
ValidationException,
BusinessLogicException
)
class OrderNotFoundException(ResourceNotFoundException):
"""Raised when an order is not found."""
def __init__(self, order_identifier: str):
super().__init__(
resource_type="Order",
identifier=order_identifier,
message=f"Order '{order_identifier}' not found",
error_code="ORDER_NOT_FOUND"
)
class OrderAlreadyExistsException(ValidationException):
"""Raised when trying to create a duplicate order."""
def __init__(self, order_number: str):
super().__init__(
message=f"Order with number '{order_number}' already exists",
error_code="ORDER_ALREADY_EXISTS",
details={"order_number": order_number}
)
class OrderValidationException(ValidationException):
"""Raised when order data validation fails."""
def __init__(self, message: str, details: Optional[dict] = None):
super().__init__(
message=message,
error_code="ORDER_VALIDATION_FAILED",
details=details
)
class InvalidOrderStatusException(BusinessLogicException):
"""Raised when trying to set an invalid order status."""
def __init__(self, current_status: str, new_status: str):
super().__init__(
message=f"Cannot change order status from '{current_status}' to '{new_status}'",
error_code="INVALID_ORDER_STATUS_CHANGE",
details={
"current_status": current_status,
"new_status": new_status
}
)
class OrderCannotBeCancelledException(BusinessLogicException):
"""Raised when order cannot be cancelled."""
def __init__(self, order_number: str, reason: str):
super().__init__(
message=f"Order '{order_number}' cannot be cancelled: {reason}",
error_code="ORDER_CANNOT_BE_CANCELLED",
details={
"order_number": order_number,
"reason": reason
}
)

View File

@@ -1,34 +1,142 @@
# app/exceptions/vendor.py
# app/exceptions/product.py
"""
Vendor management specific exceptions.
Product (vendor catalog) specific exceptions.
"""
from typing import Optional
from .base import (
ResourceNotFoundException,
ConflictException
ConflictException,
ValidationException,
BusinessLogicException
)
class ProductAlreadyExistsException(ConflictException):
"""Raised when trying to add a product that already exists in vendor."""
def __init__(self, vendor_code: str, marketplace_product_id: str):
class ProductNotFoundException(ResourceNotFoundException):
"""Raised when a product is not found in vendor catalog."""
def __init__(self, product_id: int, vendor_id: Optional[int] = None):
details = {"product_id": product_id}
if vendor_id:
details["vendor_id"] = vendor_id
message = f"Product with ID '{product_id}' not found in vendor {vendor_id} catalog"
else:
message = f"Product with ID '{product_id}' not found"
super().__init__(
message=f"MarketplaceProduct '{marketplace_product_id}' already exists in vendor '{vendor_code}'",
resource_type="Product",
identifier=str(product_id),
message=message,
error_code="PRODUCT_NOT_FOUND",
details=details,
)
class ProductAlreadyExistsException(ConflictException):
"""Raised when trying to add a marketplace product that's already in vendor catalog."""
def __init__(self, vendor_id: int, marketplace_product_id: int):
super().__init__(
message=f"Marketplace product {marketplace_product_id} already exists in vendor {vendor_id} catalog",
error_code="PRODUCT_ALREADY_EXISTS",
details={
"vendor_code": vendor_code,
"vendor_id": vendor_id,
"marketplace_product_id": marketplace_product_id,
},
)
class ProductNotFoundException(ResourceNotFoundException):
"""Raised when a vendor product relationship is not found."""
class ProductNotInCatalogException(ResourceNotFoundException):
"""Raised when trying to access a product that's not in vendor's catalog."""
def __init__(self, vendor_code: str, marketplace_product_id: str):
def __init__(self, product_id: int, vendor_id: int):
super().__init__(
resource_type="Product",
identifier=f"{vendor_code}/{marketplace_product_id}",
message=f"MarketplaceProduct '{marketplace_product_id}' not found in vendor '{vendor_code}'",
error_code="PRODUCT_NOT_FOUND",
identifier=str(product_id),
message=f"Product {product_id} is not in vendor {vendor_id} catalog",
error_code="PRODUCT_NOT_IN_CATALOG",
details={
"product_id": product_id,
"vendor_id": vendor_id,
},
)
class ProductNotActiveException(BusinessLogicException):
"""Raised when trying to perform operations on inactive product."""
def __init__(self, product_id: int, vendor_id: int):
super().__init__(
message=f"Product {product_id} in vendor {vendor_id} catalog is not active",
error_code="PRODUCT_NOT_ACTIVE",
details={
"product_id": product_id,
"vendor_id": vendor_id,
},
)
class InvalidProductDataException(ValidationException):
"""Raised when product data is invalid."""
def __init__(
self,
message: str = "Invalid product data",
field: Optional[str] = None,
details: Optional[dict] = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_PRODUCT_DATA"
class ProductValidationException(ValidationException):
"""Raised when product validation fails."""
def __init__(
self,
message: str = "Product validation failed",
field: Optional[str] = None,
validation_errors: Optional[dict] = None,
):
details = {}
if validation_errors:
details["validation_errors"] = validation_errors
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "PRODUCT_VALIDATION_FAILED"
class CannotDeleteProductWithInventoryException(BusinessLogicException):
"""Raised when trying to delete a product that has inventory."""
def __init__(self, product_id: int, inventory_count: int):
super().__init__(
message=f"Cannot delete product {product_id} - it has {inventory_count} inventory entries",
error_code="CANNOT_DELETE_PRODUCT_WITH_INVENTORY",
details={
"product_id": product_id,
"inventory_count": inventory_count,
},
)
class CannotDeleteProductWithOrdersException(BusinessLogicException):
"""Raised when trying to delete a product that has been ordered."""
def __init__(self, product_id: int, order_count: int):
super().__init__(
message=f"Cannot delete product {product_id} - it has {order_count} associated orders",
error_code="CANNOT_DELETE_PRODUCT_WITH_ORDERS",
details={
"product_id": product_id,
"order_count": order_count,
},
)

236
app/exceptions/team.py Normal file
View File

@@ -0,0 +1,236 @@
# app/exceptions/team.py
"""
Team management specific exceptions.
"""
from typing import Any, Dict, Optional
from .base import (
ResourceNotFoundException,
ConflictException,
ValidationException,
AuthorizationException,
BusinessLogicException
)
class TeamMemberNotFoundException(ResourceNotFoundException):
"""Raised when a team member is not found."""
def __init__(self, user_id: int, vendor_id: Optional[int] = None):
details = {"user_id": user_id}
if vendor_id:
details["vendor_id"] = vendor_id
message = f"Team member with user ID '{user_id}' not found in vendor {vendor_id}"
else:
message = f"Team member with user ID '{user_id}' not found"
super().__init__(
resource_type="TeamMember",
identifier=str(user_id),
message=message,
error_code="TEAM_MEMBER_NOT_FOUND",
details=details,
)
class TeamMemberAlreadyExistsException(ConflictException):
"""Raised when trying to add a user who is already a team member."""
def __init__(self, user_id: int, vendor_id: int):
super().__init__(
message=f"User {user_id} is already a team member of vendor {vendor_id}",
error_code="TEAM_MEMBER_ALREADY_EXISTS",
details={
"user_id": user_id,
"vendor_id": vendor_id,
},
)
class TeamInvitationNotFoundException(ResourceNotFoundException):
"""Raised when a team invitation is not found."""
def __init__(self, invitation_token: str):
super().__init__(
resource_type="TeamInvitation",
identifier=invitation_token,
message=f"Team invitation with token '{invitation_token}' not found or expired",
error_code="TEAM_INVITATION_NOT_FOUND",
)
class TeamInvitationExpiredException(BusinessLogicException):
"""Raised when trying to accept an expired invitation."""
def __init__(self, invitation_token: str):
super().__init__(
message=f"Team invitation has expired",
error_code="TEAM_INVITATION_EXPIRED",
details={"invitation_token": invitation_token},
)
class TeamInvitationAlreadyAcceptedException(ConflictException):
"""Raised when trying to accept an already accepted invitation."""
def __init__(self, invitation_token: str):
super().__init__(
message="Team invitation has already been accepted",
error_code="TEAM_INVITATION_ALREADY_ACCEPTED",
details={"invitation_token": invitation_token},
)
class UnauthorizedTeamActionException(AuthorizationException):
"""Raised when user tries to perform team action without permission."""
def __init__(self, action: str, user_id: Optional[int] = None, required_permission: Optional[str] = None):
details = {"action": action}
if user_id:
details["user_id"] = user_id
if required_permission:
details["required_permission"] = required_permission
super().__init__(
message=f"Unauthorized to perform action: {action}",
error_code="UNAUTHORIZED_TEAM_ACTION",
details=details,
)
class CannotRemoveOwnerException(BusinessLogicException):
"""Raised when trying to remove the vendor owner from team."""
def __init__(self, user_id: int, vendor_id: int):
super().__init__(
message="Cannot remove vendor owner from team",
error_code="CANNOT_REMOVE_OWNER",
details={
"user_id": user_id,
"vendor_id": vendor_id,
},
)
class CannotModifyOwnRoleException(BusinessLogicException):
"""Raised when user tries to modify their own role."""
def __init__(self, user_id: int):
super().__init__(
message="Cannot modify your own role",
error_code="CANNOT_MODIFY_OWN_ROLE",
details={"user_id": user_id},
)
class RoleNotFoundException(ResourceNotFoundException):
"""Raised when a role is not found."""
def __init__(self, role_id: int, vendor_id: Optional[int] = None):
details = {"role_id": role_id}
if vendor_id:
details["vendor_id"] = vendor_id
message = f"Role with ID '{role_id}' not found in vendor {vendor_id}"
else:
message = f"Role with ID '{role_id}' not found"
super().__init__(
resource_type="Role",
identifier=str(role_id),
message=message,
error_code="ROLE_NOT_FOUND",
details=details,
)
class InvalidRoleException(ValidationException):
"""Raised when role data is invalid."""
def __init__(
self,
message: str = "Invalid role data",
field: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_ROLE_DATA"
class InsufficientPermissionsException(AuthorizationException):
"""Raised when user lacks required permissions for an action."""
def __init__(
self,
required_permission: str,
user_id: Optional[int] = None,
action: Optional[str] = None,
):
details = {"required_permission": required_permission}
if user_id:
details["user_id"] = user_id
if action:
details["action"] = action
message = f"Insufficient permissions. Required: {required_permission}"
super().__init__(
message=message,
error_code="INSUFFICIENT_PERMISSIONS",
details=details,
)
class MaxTeamMembersReachedException(BusinessLogicException):
"""Raised when vendor has reached maximum team members limit."""
def __init__(self, max_members: int, vendor_id: int):
super().__init__(
message=f"Maximum number of team members reached ({max_members})",
error_code="MAX_TEAM_MEMBERS_REACHED",
details={
"max_members": max_members,
"vendor_id": vendor_id,
},
)
class TeamValidationException(ValidationException):
"""Raised when team operation validation fails."""
def __init__(
self,
message: str = "Team operation validation failed",
field: Optional[str] = None,
validation_errors: Optional[Dict[str, str]] = None,
):
details = {}
if validation_errors:
details["validation_errors"] = validation_errors
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "TEAM_VALIDATION_FAILED"
class InvalidInvitationDataException(ValidationException):
"""Raised when team invitation data is invalid."""
def __init__(
self,
message: str = "Invalid invitation data",
field: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_INVITATION_DATA"