445 lines
16 KiB
Python
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"
|
|
)
|