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