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