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

445 lines
16 KiB
Python

"""
Commands for project-related operations.
"""
from typing import Any, Dict, List, Optional, Union
from decimal import Decimal
from backend.core.models.projects.projects import Project
from backend.core.repositories.projects.projects import ProjectRepository
from backend.core.repositories.customers.customers import CustomerRepository
from backend.core.repositories.accounts.accounts import AccountRepository
from backend.core.repositories.profiles.profiles import ProfileRepository
from backend.core.utils.validators import (
is_valid_uuid, is_valid_date, validate_required_fields,
validate_model_exists, validate_decimal_amount
)
from backend.core.utils.helpers import generate_uuid
from backend.core.commands.base import Command, CommandResult
class CreateProjectCommand(Command):
"""
Command to create a new project.
"""
def __init__(
self,
project_repo: ProjectRepository,
customer_repo: CustomerRepository,
account_repo: AccountRepository,
profile_repo: ProfileRepository,
customer_id: str,
date: str,
labor: Union[float, str, Decimal],
status: str = 'planned',
account_id: Optional[str] = None,
team_member_ids: Optional[List[str]] = None,
notes: Optional[str] = None,
amount: Optional[Union[float, str, Decimal]] = None,
):
"""
Initialize the create project command.
Args:
project_repo: Repository for project operations.
customer_repo: Repository for customer operations.
account_repo: Repository for account operations.
profile_repo: Repository for profile operations.
customer_id: ID of the customer the project is for.
date: Date of the project (YYYY-MM-DD).
labor: Labor cost for the project.
status: Status of the project ('planned', 'in_progress', 'completed', 'cancelled').
account_id: Optional ID of the account the project is for.
team_member_ids: List of profile IDs for team members assigned to the project.
notes: Additional notes about the project.
amount: Billing amount for the project.
"""
self.project_repo = project_repo
self.customer_repo = customer_repo
self.account_repo = account_repo
self.profile_repo = profile_repo
self.customer_id = customer_id
self.date = date
self.labor = labor
self.status = status
self.account_id = account_id
self.team_member_ids = team_member_ids or []
self.notes = notes
self.amount = amount or labor # Default billing amount to labor cost if not specified
def validate(self) -> Dict[str, Any]:
"""
Validate the project creation data.
Returns:
Dict[str, Any]: Validation result with 'is_valid' and optional 'errors'.
"""
errors = []
# Check required fields
missing_fields = validate_required_fields(
{
'customer_id': self.customer_id,
'date': self.date,
'labor': self.labor
},
['customer_id', 'date', 'labor']
)
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'])
# Validate account exists if provided
if not errors and self.account_id:
account_validation = validate_model_exists(
self.account_id, 'account', self.account_repo.get_by_id
)
if not account_validation['valid']:
errors.append(account_validation['error'])
# Validate date format
if not errors and self.date and not is_valid_date(self.date):
errors.append("Invalid date format. Use YYYY-MM-DD.")
# Validate labor amount
if not errors:
labor_validation = validate_decimal_amount(self.labor, 'labor')
if not labor_validation['valid']:
errors.append(labor_validation['error'])
# Validate billing amount if provided
if not errors and self.amount is not None:
amount_validation = validate_decimal_amount(self.amount, 'amount')
if not amount_validation['valid']:
errors.append(amount_validation['error'])
# Validate status
valid_statuses = ['planned', 'in_progress', 'completed', 'cancelled']
if not errors and self.status not in valid_statuses:
errors.append(f"Invalid status. Must be one of: {', '.join(valid_statuses)}")
# Validate team member IDs
if not errors and self.team_member_ids:
for member_id in self.team_member_ids:
if not is_valid_uuid(member_id):
errors.append(f"Invalid team member ID format: {member_id}")
elif not self.profile_repo.get_by_id(member_id):
errors.append(f"Team member with ID {member_id} not found")
return {
'is_valid': len(errors) == 0,
'errors': errors
}
def execute(self) -> CommandResult[Project]:
"""
Execute the project creation command.
Returns:
CommandResult[Project]: Result of the command execution.
"""
# Validate command data
validation = self.validate()
if not validation['is_valid']:
return CommandResult.failure_result(validation['errors'])
try:
# Create project data
project_id = generate_uuid()
# Normalize decimal values
labor = float(self.labor)
amount = float(self.amount) if self.amount is not None else labor
# Create project data dictionary instead of Project object
project_data = {
'id': project_id,
'customer_id': self.customer_id,
'account_id': self.account_id,
'date': self.date,
'status': self.status,
'notes': self.notes,
'labor': labor,
'amount': amount
}
# Save to repository and handle team members
if self.team_member_ids:
# If there's a create_with_team_members method like in ServiceRepository
created_project = self.project_repo.create_with_team_members(
project_data,
self.team_member_ids
)
else:
# Otherwise create the project first, then assign team members
created_project = self.project_repo.create(project_data)
# Assign team members if any
if self.team_member_ids:
team_members = []
for member_id in self.team_member_ids:
member = self.profile_repo.get_by_id(member_id)
if member:
team_members.append(member)
# Assuming there's a method to assign team members
self.project_repo.assign_team_members(project_id, self.team_member_ids)
return CommandResult.success_result(
created_project,
f"Project for customer {self.customer_id} created successfully"
)
except Exception as e:
return CommandResult.failure_result(
str(e),
"Failed to create project"
)
class UpdateProjectCommand(Command):
"""
Command to update an existing project.
"""
def __init__(
self,
project_repo: ProjectRepository,
customer_repo: CustomerRepository,
account_repo: AccountRepository,
profile_repo: ProfileRepository,
project_id: str,
status: Optional[str] = None,
date: Optional[str] = None,
labor: Optional[Union[float, str, Decimal]] = None,
account_id: Optional[str] = None,
team_member_ids: Optional[List[str]] = None,
notes: Optional[str] = None,
amount: Optional[Union[float, str, Decimal]] = None
):
"""
Initialize the update project command.
Args:
project_repo: Repository for project operations.
customer_repo: Repository for customer operations.
account_repo: Repository for account operations.
profile_repo: Repository for profile operations.
project_id: ID of the project to update.
status: New status for the project.
date: New date for the project.
labor: New labor cost for the project.
account_id: New account ID for the project.
team_member_ids: New list of team member IDs.
notes: New notes for the project.
amount: New billing amount for the project.
"""
self.project_repo = project_repo
self.customer_repo = customer_repo
self.account_repo = account_repo
self.profile_repo = profile_repo
self.project_id = project_id
self.status = status
self.date = date
self.labor = labor
self.account_id = account_id
self.team_member_ids = team_member_ids
self.notes = notes
self.amount = amount
def validate(self) -> Dict[str, Any]:
"""
Validate the project update data.
Returns:
Dict[str, Any]: Validation result with 'is_valid' and optional 'errors'.
"""
errors = []
# Validate project exists
if not is_valid_uuid(self.project_id):
errors.append("Invalid project ID format")
else:
project = self.project_repo.get_by_id(self.project_id)
if not project:
errors.append(f"Project with ID {self.project_id} not found")
# Validate date format if provided
if not errors and self.date and not is_valid_date(self.date):
errors.append("Invalid date format. Use YYYY-MM-DD.")
# Validate labor amount if provided
if not errors and self.labor is not None:
labor_validation = validate_decimal_amount(self.labor, 'labor')
if not labor_validation['valid']:
errors.append(labor_validation['error'])
# Validate billing amount if provided
if not errors and self.amount is not None:
amount_validation = validate_decimal_amount(self.amount, 'amount')
if not amount_validation['valid']:
errors.append(amount_validation['error'])
# Validate status if provided
if not errors and self.status:
valid_statuses = ['planned', 'in_progress', 'completed', 'cancelled']
if self.status not in valid_statuses:
errors.append(f"Invalid status. Must be one of: {', '.join(valid_statuses)}")
# Validate account exists if provided
if not errors and self.account_id:
account_validation = validate_model_exists(
self.account_id, 'account', self.account_repo.get_by_id
)
if not account_validation['valid']:
errors.append(account_validation['error'])
# Validate team member IDs if provided
if not errors and self.team_member_ids is not None:
for member_id in self.team_member_ids:
if not is_valid_uuid(member_id):
errors.append(f"Invalid team member ID format: {member_id}")
elif not self.profile_repo.get_by_id(member_id):
errors.append(f"Team member with ID {member_id} not found")
return {
'is_valid': len(errors) == 0,
'errors': errors
}
def execute(self) -> CommandResult[Project]:
"""
Execute the project update command.
Returns:
CommandResult[Project]: 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.status is not None:
update_data['status'] = self.status
if self.date is not None:
update_data['date'] = self.date
if self.labor is not None:
update_data['labor'] = float(self.labor)
if self.account_id is not None:
update_data['account_id'] = self.account_id
if self.notes is not None:
update_data['notes'] = self.notes
if self.amount is not None:
update_data['amount'] = float(self.amount)
# Update the project with the data dictionary
updated_project = self.project_repo.update(self.project_id, update_data)
# Update team members if provided
if self.team_member_ids is not None:
# Assuming there's a method to assign team members
updated_project = self.project_repo.assign_team_members(
self.project_id,
self.team_member_ids
)
return CommandResult.success_result(
updated_project,
f"Project {self.project_id} updated successfully"
)
except Exception as e:
return CommandResult.failure_result(
str(e),
"Failed to update project"
)
class DeleteProjectCommand(Command):
"""
Command to delete a project.
"""
def __init__(self, project_repo: ProjectRepository, project_id: str):
"""
Initialize the delete project command.
Args:
project_repo: Repository for project operations.
project_id: ID of the project to delete.
"""
self.project_repo = project_repo
self.project_id = project_id
def validate(self) -> Dict[str, Any]:
"""
Validate the project deletion request.
Returns:
Dict[str, Any]: Validation result with 'is_valid' and optional 'errors'.
"""
errors = []
# Validate project exists
if not is_valid_uuid(self.project_id):
errors.append("Invalid project ID format")
else:
project = self.project_repo.get_by_id(self.project_id)
if not project:
errors.append(f"Project with ID {self.project_id} not found")
return {
'is_valid': len(errors) == 0,
'errors': errors
}
def execute(self) -> CommandResult[bool]:
"""
Execute the project 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 project
success = self.project_repo.delete(self.project_id)
if success:
return CommandResult.success_result(
True,
f"Project {self.project_id} deleted successfully"
)
else:
return CommandResult.failure_result(
"Failed to delete project",
f"Project {self.project_id} could not be deleted"
)
except Exception as e:
return CommandResult.failure_result(
str(e),
"Failed to delete project"
)