630 lines
25 KiB
Python
630 lines
25 KiB
Python
"""
|
|
Commands for customer-related operations.
|
|
"""
|
|
from typing import Any, Dict, Optional, Union, List
|
|
|
|
from graphene import UUID
|
|
|
|
from backend.core.models.customers.customers import Customer
|
|
from backend.core.repositories.customers.customers import CustomerRepository
|
|
from backend.core.repositories.accounts.accounts import AccountRepository
|
|
from backend.core.utils.validators import (
|
|
is_valid_uuid, is_valid_email, is_valid_phone, is_valid_date,
|
|
validate_required_fields
|
|
)
|
|
from backend.core.utils.helpers import generate_uuid, parse_date
|
|
from backend.core.commands.base import Command, CommandResult
|
|
|
|
|
|
class CreateCustomerCommand(Command):
|
|
"""
|
|
Command to create a new customer.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
customer_repo: CustomerRepository,
|
|
name: str,
|
|
primary_contact_first_name: str,
|
|
primary_contact_last_name: str,
|
|
primary_contact_phone: str,
|
|
primary_contact_email: str,
|
|
billing_contact_first_name: str,
|
|
billing_contact_last_name: str,
|
|
billing_street_address: str,
|
|
billing_city: str,
|
|
billing_state: str,
|
|
billing_zip_code: str,
|
|
billing_email: str,
|
|
billing_terms: str,
|
|
start_date: str,
|
|
secondary_contact_first_name: Optional[str] = None,
|
|
secondary_contact_last_name: Optional[str] = None,
|
|
secondary_contact_phone: Optional[str] = None,
|
|
secondary_contact_email: Optional[str] = None,
|
|
end_date: Optional[str] = None
|
|
):
|
|
"""
|
|
Initialize the create customer command.
|
|
|
|
Args:
|
|
customer_repo: Repository for customer operations.
|
|
name: Name of the customer.
|
|
primary_contact_first_name: First name of primary contact.
|
|
primary_contact_last_name: Last name of primary contact.
|
|
primary_contact_phone: Phone number of primary contact.
|
|
primary_contact_email: Email of primary contact.
|
|
billing_contact_first_name: First name of billing contact.
|
|
billing_contact_last_name: Last name of billing contact.
|
|
billing_street_address: Street address for billing.
|
|
billing_city: City for billing.
|
|
billing_state: State for billing.
|
|
billing_zip_code: ZIP code for billing.
|
|
billing_email: Email for billing.
|
|
billing_terms: Terms for billing.
|
|
start_date: Start date of customer relationship (YYYY-MM-DD).
|
|
secondary_contact_first_name: First name of secondary contact (optional).
|
|
secondary_contact_last_name: Last name of secondary contact (optional).
|
|
secondary_contact_phone: Phone number of secondary contact (optional).
|
|
secondary_contact_email: Email of secondary contact (optional).
|
|
end_date: End date of customer relationship (YYYY-MM-DD, optional).
|
|
"""
|
|
self.customer_repo = customer_repo
|
|
self.name = name
|
|
self.primary_contact_first_name = primary_contact_first_name
|
|
self.primary_contact_last_name = primary_contact_last_name
|
|
self.primary_contact_phone = primary_contact_phone
|
|
self.primary_contact_email = primary_contact_email
|
|
self.secondary_contact_first_name = secondary_contact_first_name
|
|
self.secondary_contact_last_name = secondary_contact_last_name
|
|
self.secondary_contact_phone = secondary_contact_phone
|
|
self.secondary_contact_email = secondary_contact_email
|
|
self.billing_contact_first_name = billing_contact_first_name
|
|
self.billing_contact_last_name = billing_contact_last_name
|
|
self.billing_street_address = billing_street_address
|
|
self.billing_city = billing_city
|
|
self.billing_state = billing_state
|
|
self.billing_zip_code = billing_zip_code
|
|
self.billing_email = billing_email
|
|
self.billing_terms = billing_terms
|
|
self.start_date = start_date
|
|
self.end_date = end_date
|
|
|
|
def validate(self) -> Dict[str, Any]:
|
|
"""
|
|
Validate the customer creation data.
|
|
|
|
Returns:
|
|
Dict[str, Any]: Validation result with 'is_valid' and optional 'errors'.
|
|
"""
|
|
errors = []
|
|
|
|
# Check required fields
|
|
required_fields = [
|
|
'name', 'primary_contact_first_name', 'primary_contact_last_name',
|
|
'primary_contact_phone', 'primary_contact_email',
|
|
'billing_contact_first_name', 'billing_contact_last_name',
|
|
'billing_street_address', 'billing_city', 'billing_state',
|
|
'billing_zip_code', 'billing_email', 'billing_terms', 'start_date'
|
|
]
|
|
|
|
field_values = {
|
|
'name': self.name,
|
|
'primary_contact_first_name': self.primary_contact_first_name,
|
|
'primary_contact_last_name': self.primary_contact_last_name,
|
|
'primary_contact_phone': self.primary_contact_phone,
|
|
'primary_contact_email': self.primary_contact_email,
|
|
'billing_contact_first_name': self.billing_contact_first_name,
|
|
'billing_contact_last_name': self.billing_contact_last_name,
|
|
'billing_street_address': self.billing_street_address,
|
|
'billing_city': self.billing_city,
|
|
'billing_state': self.billing_state,
|
|
'billing_zip_code': self.billing_zip_code,
|
|
'billing_email': self.billing_email,
|
|
'billing_terms': self.billing_terms,
|
|
'start_date': self.start_date
|
|
}
|
|
|
|
missing_fields = validate_required_fields(field_values, required_fields)
|
|
|
|
if missing_fields:
|
|
errors.append(f"Required fields missing: {', '.join(missing_fields)}")
|
|
|
|
# Validate email formats
|
|
if not errors and self.primary_contact_email and not is_valid_email(self.primary_contact_email):
|
|
errors.append("Invalid primary contact email format.")
|
|
|
|
if not errors and self.secondary_contact_email and not is_valid_email(self.secondary_contact_email):
|
|
errors.append("Invalid secondary contact email format.")
|
|
|
|
if not errors and self.billing_email and not is_valid_email(self.billing_email):
|
|
errors.append("Invalid billing email format.")
|
|
|
|
# Validate phone formats
|
|
if not errors and self.primary_contact_phone and not is_valid_phone(self.primary_contact_phone):
|
|
errors.append("Invalid primary contact phone format.")
|
|
|
|
if not errors and self.secondary_contact_phone and not is_valid_phone(self.secondary_contact_phone):
|
|
errors.append("Invalid secondary contact phone format.")
|
|
|
|
# Validate date formats
|
|
if not errors and self.start_date and not is_valid_date(self.start_date):
|
|
errors.append("Invalid start date format. Use YYYY-MM-DD.")
|
|
|
|
if not errors and self.end_date and not is_valid_date(self.end_date):
|
|
errors.append("Invalid end date format. Use YYYY-MM-DD.")
|
|
|
|
# Validate start date is before end date if both provided
|
|
if not errors and self.start_date and self.end_date:
|
|
start = parse_date(self.start_date)
|
|
end = parse_date(self.end_date)
|
|
if start and end and start > end:
|
|
errors.append("Start date must be before end date.")
|
|
|
|
return {
|
|
'is_valid': len(errors) == 0,
|
|
'errors': errors
|
|
}
|
|
|
|
def execute(self) -> CommandResult[Customer]:
|
|
"""
|
|
Execute the customer creation command.
|
|
|
|
Returns:
|
|
CommandResult[Customer]: Result of the command execution.
|
|
"""
|
|
# Validate command data
|
|
validation = self.validate()
|
|
if not validation['is_valid']:
|
|
return CommandResult.failure_result(validation['errors'])
|
|
|
|
try:
|
|
# Create customer data
|
|
customer_id = generate_uuid()
|
|
|
|
# Create customer data dictionary
|
|
customer_data = {
|
|
'id': customer_id,
|
|
'name': self.name,
|
|
'primary_contact_first_name': self.primary_contact_first_name,
|
|
'primary_contact_last_name': self.primary_contact_last_name,
|
|
'primary_contact_phone': self.primary_contact_phone,
|
|
'primary_contact_email': self.primary_contact_email,
|
|
'secondary_contact_first_name': self.secondary_contact_first_name,
|
|
'secondary_contact_last_name': self.secondary_contact_last_name,
|
|
'secondary_contact_phone': self.secondary_contact_phone,
|
|
'secondary_contact_email': self.secondary_contact_email,
|
|
'billing_contact_first_name': self.billing_contact_first_name,
|
|
'billing_contact_last_name': self.billing_contact_last_name,
|
|
'billing_street_address': self.billing_street_address,
|
|
'billing_city': self.billing_city,
|
|
'billing_state': self.billing_state,
|
|
'billing_zip_code': self.billing_zip_code,
|
|
'billing_email': self.billing_email,
|
|
'billing_terms': self.billing_terms,
|
|
'start_date': self.start_date,
|
|
'end_date': self.end_date
|
|
}
|
|
|
|
# Save to repository
|
|
created_customer = self.customer_repo.create(customer_data)
|
|
|
|
return CommandResult.success_result(
|
|
created_customer,
|
|
f"Customer {self.name} created successfully"
|
|
)
|
|
|
|
except Exception as e:
|
|
return CommandResult.failure_result(
|
|
str(e),
|
|
"Failed to create customer"
|
|
)
|
|
|
|
|
|
class UpdateCustomerCommand(Command):
|
|
"""
|
|
Command to update an existing customer.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
customer_repo: CustomerRepository,
|
|
id: UUID,
|
|
name: Optional[str] = None,
|
|
primary_contact_first_name: Optional[str] = None,
|
|
primary_contact_last_name: Optional[str] = None,
|
|
primary_contact_phone: Optional[str] = None,
|
|
primary_contact_email: Optional[str] = None,
|
|
secondary_contact_first_name: Optional[str] = None,
|
|
secondary_contact_last_name: Optional[str] = None,
|
|
secondary_contact_phone: Optional[str] = None,
|
|
secondary_contact_email: Optional[str] = None,
|
|
billing_contact_first_name: Optional[str] = None,
|
|
billing_contact_last_name: Optional[str] = None,
|
|
billing_street_address: Optional[str] = None,
|
|
billing_city: Optional[str] = None,
|
|
billing_state: Optional[str] = None,
|
|
billing_zip_code: Optional[str] = None,
|
|
billing_email: Optional[str] = None,
|
|
billing_terms: Optional[str] = None,
|
|
start_date: Optional[str] = None,
|
|
end_date: Optional[str] = None
|
|
):
|
|
"""
|
|
Initialize the update customer command.
|
|
|
|
Args:
|
|
customer_repo: Repository for customer operations.
|
|
id: ID of the customer to update.
|
|
name: New name for the customer.
|
|
primary_contact_first_name: New first name of primary contact.
|
|
primary_contact_last_name: New last name of primary contact.
|
|
primary_contact_phone: New phone number of primary contact.
|
|
primary_contact_email: New email of primary contact.
|
|
secondary_contact_first_name: New first name of secondary contact.
|
|
secondary_contact_last_name: New last name of secondary contact.
|
|
secondary_contact_phone: New phone number of secondary contact.
|
|
secondary_contact_email: New email of secondary contact.
|
|
billing_contact_first_name: New first name of billing contact.
|
|
billing_contact_last_name: New last name of billing contact.
|
|
billing_street_address: New street address for billing.
|
|
billing_city: New city for billing.
|
|
billing_state: New state for billing.
|
|
billing_zip_code: New ZIP code for billing.
|
|
billing_email: New email for billing.
|
|
billing_terms: New terms for billing.
|
|
start_date: New start date of customer relationship.
|
|
end_date: New end date of customer relationship.
|
|
"""
|
|
self.customer_repo = customer_repo
|
|
self.id = str(id)
|
|
self.name = name
|
|
self.primary_contact_first_name = primary_contact_first_name
|
|
self.primary_contact_last_name = primary_contact_last_name
|
|
self.primary_contact_phone = primary_contact_phone
|
|
self.primary_contact_email = primary_contact_email
|
|
self.secondary_contact_first_name = secondary_contact_first_name
|
|
self.secondary_contact_last_name = secondary_contact_last_name
|
|
self.secondary_contact_phone = secondary_contact_phone
|
|
self.secondary_contact_email = secondary_contact_email
|
|
self.billing_contact_first_name = billing_contact_first_name
|
|
self.billing_contact_last_name = billing_contact_last_name
|
|
self.billing_street_address = billing_street_address
|
|
self.billing_city = billing_city
|
|
self.billing_state = billing_state
|
|
self.billing_zip_code = billing_zip_code
|
|
self.billing_email = billing_email
|
|
self.billing_terms = billing_terms
|
|
self.start_date = start_date
|
|
self.end_date = end_date
|
|
|
|
def validate(self) -> Dict[str, Union[bool, List[str]]]:
|
|
"""
|
|
Validate the customer update data.
|
|
|
|
Returns:
|
|
Dict[str, Union[bool, List[str]]]: Validation result with 'is_valid' and 'errors'.
|
|
"""
|
|
errors = []
|
|
|
|
# Validate customer exists
|
|
if not is_valid_uuid(self.id):
|
|
errors.append("Invalid customer ID format")
|
|
else:
|
|
customer = self.customer_repo.get_by_id(self.id)
|
|
if not customer:
|
|
errors.append(f"Customer with ID {self.id} not found")
|
|
|
|
# Validate email formats if provided
|
|
if not errors and self.primary_contact_email is not None and not is_valid_email(self.primary_contact_email):
|
|
errors.append("Invalid primary contact email format.")
|
|
|
|
if not errors and self.secondary_contact_email is not None and not is_valid_email(self.secondary_contact_email):
|
|
errors.append("Invalid secondary contact email format.")
|
|
|
|
if not errors and self.billing_email is not None and not is_valid_email(self.billing_email):
|
|
errors.append("Invalid billing email format.")
|
|
|
|
# Validate phone formats if provided
|
|
if not errors and self.primary_contact_phone is not None and not is_valid_phone(self.primary_contact_phone):
|
|
errors.append("Invalid primary contact phone format.")
|
|
|
|
if not errors and self.secondary_contact_phone is not None and not is_valid_phone(self.secondary_contact_phone):
|
|
errors.append("Invalid secondary contact phone format.")
|
|
|
|
# Validate date formats if provided
|
|
if not errors and self.start_date is not None and not is_valid_date(self.start_date):
|
|
errors.append("Invalid start date format. Use YYYY-MM-DD.")
|
|
|
|
if not errors and self.end_date is not None and not is_valid_date(self.end_date):
|
|
errors.append("Invalid end date format. Use YYYY-MM-DD.")
|
|
|
|
# Validate start date is before end date if both provided
|
|
if not errors and self.start_date and self.end_date:
|
|
start = parse_date(self.start_date)
|
|
end = parse_date(self.end_date)
|
|
if start and end and start > end:
|
|
errors.append("Start date must be before end date.")
|
|
|
|
# If only updating end_date, validate it's after the existing start_date
|
|
if not errors and self.end_date and not self.start_date:
|
|
customer = self.customer_repo.get_by_id(self.id)
|
|
if customer:
|
|
end = parse_date(self.end_date)
|
|
if end and customer.start_date > end:
|
|
errors.append("End date must be after the existing start date.")
|
|
|
|
return {
|
|
'is_valid': len(errors) == 0,
|
|
'errors': errors
|
|
}
|
|
|
|
def execute(self) -> CommandResult[Customer]:
|
|
"""
|
|
Execute the customer update command.
|
|
|
|
Returns:
|
|
CommandResult[Customer]: Result of the command execution.
|
|
"""
|
|
# Validate command data
|
|
validation = self.validate()
|
|
if not validation['is_valid']:
|
|
return CommandResult.failure_result(validation['errors'])
|
|
|
|
try:
|
|
# Create a dictionary of fields to update
|
|
update_data = {}
|
|
|
|
# Add fields to update_data if they were provided
|
|
if self.name is not None:
|
|
update_data['name'] = self.name
|
|
|
|
if self.primary_contact_first_name is not None:
|
|
update_data['primary_contact_first_name'] = self.primary_contact_first_name
|
|
|
|
if self.primary_contact_last_name is not None:
|
|
update_data['primary_contact_last_name'] = self.primary_contact_last_name
|
|
|
|
if self.primary_contact_phone is not None:
|
|
update_data['primary_contact_phone'] = self.primary_contact_phone
|
|
|
|
if self.primary_contact_email is not None:
|
|
update_data['primary_contact_email'] = self.primary_contact_email
|
|
|
|
if self.secondary_contact_first_name is not None:
|
|
update_data['secondary_contact_first_name'] = self.secondary_contact_first_name
|
|
|
|
if self.secondary_contact_last_name is not None:
|
|
update_data['secondary_contact_last_name'] = self.secondary_contact_last_name
|
|
|
|
if self.secondary_contact_phone is not None:
|
|
update_data['secondary_contact_phone'] = self.secondary_contact_phone
|
|
|
|
if self.secondary_contact_email is not None:
|
|
update_data['secondary_contact_email'] = self.secondary_contact_email
|
|
|
|
if self.billing_contact_first_name is not None:
|
|
update_data['billing_contact_first_name'] = self.billing_contact_first_name
|
|
|
|
if self.billing_contact_last_name is not None:
|
|
update_data['billing_contact_last_name'] = self.billing_contact_last_name
|
|
|
|
if self.billing_street_address is not None:
|
|
update_data['billing_street_address'] = self.billing_street_address
|
|
|
|
if self.billing_city is not None:
|
|
update_data['billing_city'] = self.billing_city
|
|
|
|
if self.billing_state is not None:
|
|
update_data['billing_state'] = self.billing_state
|
|
|
|
if self.billing_zip_code is not None:
|
|
update_data['billing_zip_code'] = self.billing_zip_code
|
|
|
|
if self.billing_email is not None:
|
|
update_data['billing_email'] = self.billing_email
|
|
|
|
if self.billing_terms is not None:
|
|
update_data['billing_terms'] = self.billing_terms
|
|
|
|
if self.start_date is not None:
|
|
update_data['start_date'] = self.start_date
|
|
|
|
if self.end_date is not None:
|
|
update_data['end_date'] = self.end_date
|
|
|
|
# Update the customer with the data dictionary
|
|
updated_customer = self.customer_repo.update(self.id, update_data)
|
|
|
|
return CommandResult.success_result(
|
|
updated_customer,
|
|
f"Customer {self.id} updated successfully"
|
|
)
|
|
|
|
except Exception as e:
|
|
return CommandResult.failure_result(
|
|
str(e),
|
|
"Failed to update customer"
|
|
)
|
|
|
|
|
|
class DeleteCustomerCommand(Command):
|
|
"""
|
|
Command to delete a customer.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
customer_repo: CustomerRepository,
|
|
account_repo: AccountRepository,
|
|
customer_id: str
|
|
):
|
|
"""
|
|
Initialize the delete customer command.
|
|
|
|
Args:
|
|
customer_repo: Repository for customer operations.
|
|
account_repo: Repository for account operations.
|
|
customer_id: ID of the customer to delete.
|
|
"""
|
|
self.customer_repo = customer_repo
|
|
self.account_repo = account_repo
|
|
self.customer_id = customer_id
|
|
|
|
def validate(self) -> Dict[str, Any]:
|
|
"""
|
|
Validate the customer deletion request.
|
|
|
|
Returns:
|
|
Dict[str, Any]: Validation result with 'is_valid' and optional 'errors'.
|
|
"""
|
|
errors = []
|
|
|
|
# Validate customer exists
|
|
if not is_valid_uuid(self.customer_id):
|
|
errors.append("Invalid customer ID format")
|
|
else:
|
|
customer = self.customer_repo.get_by_id(self.customer_id)
|
|
if not customer:
|
|
errors.append(f"Customer with ID {self.customer_id} not found")
|
|
|
|
# Check if customer has associated accounts
|
|
if not errors:
|
|
customer_with_accounts = self.customer_repo.get_with_accounts(self.customer_id)
|
|
# First check if customer_with_accounts is not None
|
|
if customer_with_accounts is not None:
|
|
# Now we can safely access the accounts attribute
|
|
if hasattr(customer_with_accounts, 'accounts') and customer_with_accounts.accounts.exists():
|
|
errors.append(f"Cannot delete customer with associated accounts")
|
|
|
|
return {
|
|
'is_valid': len(errors) == 0,
|
|
'errors': errors
|
|
}
|
|
|
|
def execute(self) -> CommandResult[bool]:
|
|
"""
|
|
Execute the customer deletion command.
|
|
|
|
Returns:
|
|
CommandResult[bool]: Result of the command execution.
|
|
"""
|
|
# Validate command data
|
|
validation = self.validate()
|
|
if not validation['is_valid']:
|
|
return CommandResult.failure_result(validation['errors'])
|
|
|
|
try:
|
|
# Delete the customer
|
|
success = self.customer_repo.delete(self.customer_id)
|
|
|
|
if success:
|
|
return CommandResult.success_result(
|
|
True,
|
|
f"Customer {self.customer_id} deleted successfully"
|
|
)
|
|
else:
|
|
return CommandResult.failure_result(
|
|
"Failed to delete customer",
|
|
f"Customer {self.customer_id} could not be deleted"
|
|
)
|
|
|
|
except Exception as e:
|
|
return CommandResult.failure_result(
|
|
str(e),
|
|
"Failed to delete customer"
|
|
)
|
|
|
|
|
|
class MarkCustomerInactiveCommand(Command):
|
|
"""
|
|
Command to mark a customer as inactive.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
customer_repo: CustomerRepository,
|
|
customer_id: str,
|
|
end_date: Optional[str] = None
|
|
):
|
|
"""
|
|
Initialize the mark customer inactive command.
|
|
|
|
Args:
|
|
customer_repo: Repository for customer operations.
|
|
customer_id: ID of the customer to mark as inactive.
|
|
end_date: End date for the customer relationship (defaults to today if not provided).
|
|
"""
|
|
self.customer_repo = customer_repo
|
|
self.customer_id = customer_id
|
|
self.end_date = end_date
|
|
|
|
def validate(self) -> Dict[str, Any]:
|
|
"""
|
|
Validate the mark customer inactive request.
|
|
|
|
Returns:
|
|
Dict[str, Any]: Validation result with 'is_valid' and optional 'errors'.
|
|
"""
|
|
errors = []
|
|
|
|
# Validate customer exists
|
|
if not is_valid_uuid(self.customer_id):
|
|
errors.append("Invalid customer ID format")
|
|
else:
|
|
customer = self.customer_repo.get_by_id(self.customer_id)
|
|
if not customer:
|
|
errors.append(f"Customer with ID {self.customer_id} not found")
|
|
elif customer.end_date is not None:
|
|
errors.append(f"Customer is already marked as inactive")
|
|
|
|
# Validate end date format if provided
|
|
if not errors and self.end_date and not is_valid_date(self.end_date):
|
|
errors.append("Invalid end date format. Use YYYY-MM-DD.")
|
|
|
|
# Validate end date is after start date if provided
|
|
if not errors and self.end_date:
|
|
customer = self.customer_repo.get_by_id(self.customer_id)
|
|
if customer:
|
|
end = parse_date(self.end_date)
|
|
if end and customer.start_date > end:
|
|
errors.append("End date must be after the start date.")
|
|
|
|
return {
|
|
'is_valid': len(errors) == 0,
|
|
'errors': errors
|
|
}
|
|
|
|
def execute(self) -> CommandResult[Customer]:
|
|
"""
|
|
Execute the mark customer inactive command.
|
|
|
|
Returns:
|
|
CommandResult[Customer]: Result of the command execution.
|
|
"""
|
|
# Validate command data
|
|
validation = self.validate()
|
|
if not validation['is_valid']:
|
|
return CommandResult.failure_result(validation['errors'])
|
|
|
|
try:
|
|
# Mark the customer as inactive
|
|
updated_customer = self.customer_repo.mark_inactive(self.customer_id)
|
|
|
|
if updated_customer:
|
|
return CommandResult.success_result(
|
|
updated_customer,
|
|
f"Customer {self.customer_id} marked as inactive successfully"
|
|
)
|
|
else:
|
|
return CommandResult.failure_result(
|
|
"Failed to mark customer as inactive",
|
|
f"Customer {self.customer_id} could not be marked as inactive"
|
|
)
|
|
|
|
except Exception as e:
|
|
return CommandResult.failure_result(
|
|
str(e),
|
|
"Failed to mark customer as inactive"
|
|
)
|