Files
TimeTracker/app/services/client_service.py
T
Dries Peeters ef9b66f5e7 fix(api): align client search, OpenAPI version, and Client construction
Global search referenced Client.company, which is not a column on Client, so client matches failed at runtime. Legacy and v1 search, plus search_clients(), now filter on name, email, description, and contact_person; result descriptions use the same fields. Legacy /api/search returns count: 0 for queries shorter than two characters so responses stay consistent.

OpenAPI info.version is taken from get_version_from_setup(), with a config fallback when the resolved version is unknown. get_version_from_setup() also honors TIMETRACKER_VERSION and APP_VERSION for CI and container builds.

Client.__init__ accepts custom_fields. ClientService no longer passes status= into Client(), which the initializer does not support.

Tests add HTTP route contract checks and OpenAPI version alignment, fix subcontractor search fixtures (Client/Task construction and v1 client fixture naming), and update related API integration tests.
2026-04-15 12:57:01 +02:00

107 lines
3.2 KiB
Python

"""
Service for client business logic.
"""
from decimal import Decimal
from typing import Any, Dict, List, Optional
from app import db
from app.models import Client
from app.repositories import ClientRepository
from app.utils.db import safe_commit
class ClientService:
"""Service for client operations"""
def __init__(self):
self.client_repo = ClientRepository()
def get_by_id(self, client_id: int) -> Optional[Client]:
"""
Get a client by its ID.
Returns:
Client instance or None if not found
"""
return self.client_repo.get_by_id(client_id)
def get_by_name(self, name: str) -> Optional[Client]:
"""
Get a client by name.
Returns:
Client instance or None if not found
"""
return self.client_repo.get_by_name(name)
def create_client(
self,
name: str,
created_by: int,
email: Optional[str] = None,
company: Optional[str] = None,
phone: Optional[str] = None,
address: Optional[str] = None,
default_hourly_rate: Optional[Decimal] = None,
custom_fields: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Create a new client.
Returns:
dict with 'success', 'message', and 'client' keys
"""
# Check for duplicate name
existing = self.client_repo.get_by_name(name)
if existing:
return {"success": False, "message": "A client with this name already exists", "error": "duplicate_client"}
# Create client
client = self.client_repo.create(
name=name,
email=email,
company=company,
phone=phone,
address=address,
default_hourly_rate=default_hourly_rate,
custom_fields=custom_fields,
)
if not safe_commit("create_client", {"name": name, "created_by": created_by}):
return {
"success": False,
"message": "Could not create client due to a database error",
"error": "database_error",
}
return {"success": True, "message": "Client created successfully", "client": client}
def update_client(self, client_id: int, user_id: int, **kwargs) -> Dict[str, Any]:
"""
Update a client.
Returns:
dict with 'success', 'message', and 'client' keys
"""
client = self.client_repo.get_by_id(client_id)
if not client:
return {"success": False, "message": "Client not found", "error": "not_found"}
# Update fields
self.client_repo.update(client, **kwargs)
if not safe_commit("update_client", {"client_id": client_id, "user_id": user_id}):
return {
"success": False,
"message": "Could not update client due to a database error",
"error": "database_error",
}
return {"success": True, "message": "Client updated successfully", "client": client}
def get_active_clients(self) -> List[Client]:
"""Get all active clients"""
return self.client_repo.get_active_clients()