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

678 lines
26 KiB
Python

"""
Commands for account-related operations.
"""
from typing import Any, Dict, List, Optional
from datetime import datetime
from backend.core.models.accounts.accounts import Account
from backend.core.repositories.accounts.accounts import AccountRepository
from backend.core.repositories.customers.customers import CustomerRepository
from backend.core.utils.validators import (
is_valid_uuid, is_valid_email, is_valid_phone, is_valid_date,
validate_required_fields, validate_model_exists
)
from backend.core.utils.helpers import generate_uuid, parse_date
from backend.core.commands.base import Command, CommandResult
class CreateAccountCommand(Command):
"""
Command to create a new account.
"""
def __init__(
self,
account_repo: AccountRepository,
customer_repo: CustomerRepository,
customer_id: str,
name: str,
street_address: str,
city: str,
state: str,
zip_code: str,
start_date: str,
primary_contact_first_name: str, # Changed from contact_first_name
primary_contact_last_name: str, # Changed from contact_last_name
primary_contact_phone: str, # Changed from contact_phone
primary_contact_email: str, # Changed from contact_email
secondary_contact_first_name: Optional[str] = None, # Added
secondary_contact_last_name: Optional[str] = None, # Added
secondary_contact_phone: Optional[str] = None, # Added
secondary_contact_email: Optional[str] = None, # Added
end_date: Optional[str] = None
):
"""
Initialize the create account command.
Args:
account_repo: Repository for account operations.
customer_repo: Repository for customer operations.
customer_id: ID of the customer this account belongs to.
name: Name of the account.
street_address: Street address of the account.
city: City of the account.
state: State of the account.
zip_code: ZIP code of the account.
primary_contact_first_name: First name of the primary contact.
primary_contact_last_name: Last name of the primary contact.
primary_contact_phone: Phone number of the primary contact.
primary_contact_email: Email of the primary contact.
secondary_contact_first_name: First name of the secondary contact (optional).
secondary_contact_last_name: Last name of the secondary contact (optional).
secondary_contact_phone: Phone number of the secondary contact (optional).
secondary_contact_email: Email of the secondary contact (optional).
start_date: Start date of the account (YYYY-MM-DD).
end_date: End date of the account (YYYY-MM-DD, optional).
"""
self.account_repo = account_repo
self.customer_repo = customer_repo
self.customer_id = customer_id
self.name = name
self.street_address = street_address
self.city = city
self.state = state
self.zip_code = zip_code
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.start_date = start_date
self.end_date = end_date
def validate(self) -> Dict[str, Any]:
"""
Validate the account creation data.
Returns:
Dict[str, Any]: Validation result with 'is_valid' and optional 'errors'.
"""
errors = []
# Check required fields
required_fields = [
'customer_id', 'name', 'street_address', 'city', 'state', 'zip_code',
'primary_contact_first_name', 'primary_contact_last_name',
'primary_contact_phone', 'primary_contact_email', 'start_date'
]
field_values = {
'customer_id': self.customer_id,
'name': self.name,
'street_address': self.street_address,
'city': self.city,
'state': self.state,
'zip_code': self.zip_code,
'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,
'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 customer exists
if not errors and self.customer_id:
customer_validation = validate_model_exists(
self.customer_id, 'customer', self.customer_repo.get_by_id
)
if not customer_validation['valid']:
errors.append(customer_validation['error'])
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.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_email and not is_valid_email(self.secondary_contact_email):
errors.append("Invalid secondary contact email 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.")
# Check if customer is active
if not errors:
customer = self.customer_repo.get_by_id(self.customer_id)
if customer and customer.end_date:
customer_end_date = parse_date(customer.end_date)
today = datetime.now().date()
if customer_end_date and customer_end_date < today:
errors.append(f"Cannot create account for inactive customer")
return {
'is_valid': len(errors) == 0,
'errors': errors
}
def execute(self) -> CommandResult[Account]:
"""
Execute the account creation command.
Returns:
CommandResult[Account]: Result of the command execution.
"""
# Validate command data
validation = self.validate()
if not validation['is_valid']:
return CommandResult.failure_result(validation['errors'])
try:
# Create account data
account_id = generate_uuid()
# Create account data dictionary
account_data = {
'id': account_id,
'customer_id': self.customer_id,
'name': self.name,
'street_address': self.street_address,
'city': self.city,
'state': self.state,
'zip_code': self.zip_code,
'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,
'start_date': self.start_date,
'end_date': self.end_date
}
# Save to repository
created_account = self.account_repo.create(account_data)
return CommandResult.success_result(
created_account,
f"Account {self.name} created successfully for customer {self.customer_id}"
)
except Exception as e:
return CommandResult.failure_result(
str(e),
"Failed to create account"
)
class UpdateAccountCommand(Command):
"""
Command to update an existing account.
"""
def __init__(
self,
account_repo: AccountRepository,
account_id: str,
name: Optional[str] = None,
street_address: Optional[str] = None,
city: Optional[str] = None,
state: Optional[str] = None,
zip_code: 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,
start_date: Optional[str] = None,
end_date: Optional[str] = None
):
"""
Initialize the update account command.
Args:
account_repo: Repository for account operations.
account_id: ID of the account to update.
name: New name for the account.
street_address: New street address for the account.
city: New city for the account.
state: New state for the account.
zip_code: New ZIP code for the account.
primary_contact_first_name: New first name for the primary contact.
primary_contact_last_name: New last name for the primary contact.
primary_contact_phone: New phone number for the primary contact.
primary_contact_email: New email for the primary contact.
secondary_contact_first_name: New first name for the secondary contact.
secondary_contact_last_name: New last name for the secondary contact.
secondary_contact_phone: New phone number for the secondary contact.
secondary_contact_email: New email for the secondary contact.
start_date: New start date for the account.
end_date: New end date for the account.
"""
self.account_repo = account_repo
self.account_id = account_id
self.name = name
self.street_address = street_address
self.city = city
self.state = state
self.zip_code = zip_code
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.start_date = start_date
self.end_date = end_date
def validate(self) -> Dict[str, Any]:
"""
Validate the account update data.
Returns:
Dict[str, Any]: Validation result with 'is_valid' and optional 'errors'.
"""
errors = []
# Validate account exists
if not is_valid_uuid(self.account_id):
errors.append("Invalid account ID format")
else:
account = self.account_repo.get_by_id(self.account_id)
if not account:
errors.append(f"Account with ID {self.account_id} not found")
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.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_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.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:
account = self.account_repo.get_by_id(self.account_id)
if account:
end = parse_date(self.end_date)
start = parse_date(account.start_date)
if end and start and start > end:
errors.append("End date must be after the existing start date.")
return {
'is_valid': len(errors) == 0,
'errors': errors
}
def execute(self) -> CommandResult[Account]:
"""
Execute the account update command.
Returns:
CommandResult[Account]: 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.street_address is not None:
update_data['street_address'] = self.street_address
if self.city is not None:
update_data['city'] = self.city
if self.state is not None:
update_data['state'] = self.state
if self.zip_code is not None:
update_data['zip_code'] = self.zip_code
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.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 account with the data dictionary
updated_account = self.account_repo.update(self.account_id, update_data)
return CommandResult.success_result(
updated_account,
f"Account {self.account_id} updated successfully"
)
except Exception as e:
return CommandResult.failure_result(
str(e),
"Failed to update account"
)
class DeleteAccountCommand(Command):
"""
Command to delete an account.
"""
def __init__(
self,
account_repo: AccountRepository,
account_id: str
):
"""
Initialize the delete account command.
Args:
account_repo: Repository for account operations.
account_id: ID of the account to delete.
"""
self.account_repo = account_repo
self.account_id = account_id
def validate(self) -> Dict[str, Any]:
"""
Validate the account deletion request.
Returns:
Dict[str, Any]: Validation result with 'is_valid' and optional 'errors'.
"""
errors = []
# Validate account exists
if not is_valid_uuid(self.account_id):
errors.append("Invalid account ID format")
else:
account = self.account_repo.get_by_id(self.account_id)
if not account:
errors.append(f"Account with ID {self.account_id} not found")
# Check if account has associated services or projects
if not errors:
account_with_relations = self.account_repo.get_with_all_related(self.account_id)
if account_with_relations:
if hasattr(account_with_relations, 'services') and account_with_relations.services.exists():
errors.append(f"Cannot delete account with associated services")
if hasattr(account_with_relations, 'projects') and account_with_relations.projects.exists():
errors.append(f"Cannot delete account with associated projects")
return {
'is_valid': len(errors) == 0,
'errors': errors
}
def execute(self) -> CommandResult[bool]:
"""
Execute the account 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 account
success = self.account_repo.delete(self.account_id)
if success:
return CommandResult.success_result(
True,
f"Account {self.account_id} deleted successfully"
)
else:
return CommandResult.failure_result(
"Failed to delete account",
f"Account {self.account_id} could not be deleted"
)
except Exception as e:
return CommandResult.failure_result(
str(e),
"Failed to delete account"
)
class MarkAccountInactiveCommand(Command):
"""
Command to mark an account as inactive.
"""
def __init__(
self,
account_repo: AccountRepository,
account_id: str,
end_date: Optional[str] = None
):
"""
Initialize the mark account inactive command.
Args:
account_repo: Repository for account operations.
account_id: ID of the account to mark as inactive.
end_date: End date for the account (defaults to today if not provided).
"""
self.account_repo = account_repo
self.account_id = account_id
self.end_date = end_date
def validate(self) -> Dict[str, Any]:
"""
Validate the mark account inactive request.
Returns:
Dict[str, Any]: Validation result with 'is_valid' and optional 'errors'.
"""
errors = []
# Validate account exists
if not is_valid_uuid(self.account_id):
errors.append("Invalid account ID format")
else:
account = self.account_repo.get_by_id(self.account_id)
if not account:
errors.append(f"Account with ID {self.account_id} not found")
elif account.end_date is not None:
errors.append(f"Account 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:
account = self.account_repo.get_by_id(self.account_id)
if account:
end = parse_date(self.end_date)
start = parse_date(account.start_date)
if end and start and start > end:
errors.append("End date must be after the start date.")
return {
'is_valid': len(errors) == 0,
'errors': errors
}
def execute(self) -> CommandResult[Account]:
"""
Execute the mark account inactive command.
Returns:
CommandResult[Account]: 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 account as inactive
end_date = self.end_date or datetime.now().strftime('%Y-%m-%d')
updated_account = self.account_repo.update(
self.account_id,
{'end_date': end_date}
)
if updated_account:
return CommandResult.success_result(
updated_account,
f"Account {self.account_id} marked as inactive successfully"
)
else:
return CommandResult.failure_result(
"Failed to mark account as inactive",
f"Account {self.account_id} could not be marked as inactive"
)
except Exception as e:
return CommandResult.failure_result(
str(e),
"Failed to mark account as inactive"
)
class GetAccountRevenueCommand(Command):
"""
Command to get the revenue information for an account.
"""
def __init__(
self,
account_repo: AccountRepository,
account_id: str
):
"""
Initialize the get account revenue command.
Args:
account_repo: Repository for account operations.
account_id: ID of the account to get revenue for.
"""
self.account_repo = account_repo
self.account_id = account_id
def validate(self) -> Dict[str, Any]:
"""
Validate the get account revenue request.
Returns:
Dict[str, Any]: Validation result with 'is_valid' and optional 'errors'.
"""
errors = []
# Validate account exists
if not is_valid_uuid(self.account_id):
errors.append("Invalid account ID format")
else:
account = self.account_repo.get_by_id(self.account_id)
if not account:
errors.append(f"Account with ID {self.account_id} not found")
return {
'is_valid': len(errors) == 0,
'errors': errors
}
def execute(self) -> CommandResult[List[Any]]:
"""
Execute the get account revenue command.
Returns:
CommandResult[List[Any]]: Result of the command execution with revenue data.
"""
# Validate command data
validation = self.validate()
if not validation['is_valid']:
return CommandResult.failure_result(validation['errors'])
try:
# Retrieve the account's revenue information
account_with_revenues = self.account_repo.get_with_revenues(self.account_id)
if account_with_revenues and hasattr(account_with_revenues, 'revenues'):
revenues = list(account_with_revenues.revenues.all())
return CommandResult.success_result(
revenues,
f"Retrieved revenue data for account {self.account_id}"
)
else:
return CommandResult.failure_result(
"No revenue data found",
f"No revenue data found for account {self.account_id}"
)
except Exception as e:
return CommandResult.failure_result(
str(e),
"Failed to retrieve account revenue data"
)