2026-01-26 10:30:49 -05:00

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"
)