Files
test_task_crm/app/services/activity_service.py
T
Artem Kashaev 5fcb574aca
Test / test (push) Successful in 15s
Refactor code for improved readability and consistency
- Reformatted function signatures in `organization_service.py` and `task_service.py` for better alignment.
- Updated import statements across multiple files for consistency and organization.
- Enhanced test files by improving formatting and ensuring consistent use of async session factories.
- Added type hints and improved type safety in various service and test files.
- Adjusted `pyproject.toml` to include configuration for isort, mypy, and ruff for better code quality checks.
- Cleaned up unused imports and organized existing ones in several test files.
2025-12-01 16:18:03 +05:00

106 lines
3.3 KiB
Python

"""Business logic for timeline activities."""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any
from app.models.activity import Activity, ActivityCreate, ActivityType
from app.models.deal import Deal
from app.repositories.activity_repo import (
ActivityOrganizationMismatchError,
ActivityQueryParams,
ActivityRepository,
)
from app.services.organization_service import OrganizationContext
class ActivityServiceError(Exception):
"""Base class for activity service errors."""
class ActivityValidationError(ActivityServiceError):
"""Raised when payload does not satisfy business constraints."""
class ActivityForbiddenError(ActivityServiceError):
"""Raised when a user accesses activities from another organization."""
@dataclass(slots=True)
class ActivityListFilters:
"""Filtering helpers for listing activities."""
deal_id: int
limit: int | None = None
offset: int = 0
class ActivityService:
"""Encapsulates timeline-specific workflows."""
def __init__(self, repository: ActivityRepository) -> None:
self._repository = repository
async def list_activities(
self,
*,
filters: ActivityListFilters,
context: OrganizationContext,
) -> Sequence[Activity]:
await self._ensure_deal_in_context(filters.deal_id, context)
params = ActivityQueryParams(
organization_id=context.organization_id,
deal_id=filters.deal_id,
limit=filters.limit,
offset=max(filters.offset, 0),
)
return await self._repository.list(params=params)
async def add_comment(
self,
*,
deal_id: int,
author_id: int,
text: str,
context: OrganizationContext,
) -> Activity:
normalized = text.strip()
if not normalized:
raise ActivityValidationError("Comment text cannot be empty")
return await self.record_activity(
deal_id=deal_id,
activity_type=ActivityType.COMMENT,
payload={"text": normalized},
author_id=author_id,
context=context,
)
async def record_activity(
self,
*,
deal_id: int,
activity_type: ActivityType,
context: OrganizationContext,
payload: dict[str, Any] | None = None,
author_id: int | None = None,
) -> Activity:
await self._ensure_deal_in_context(deal_id, context)
data = ActivityCreate(
deal_id=deal_id,
author_id=author_id,
type=activity_type,
payload=payload or {},
)
try:
return await self._repository.create(data, organization_id=context.organization_id)
except ActivityOrganizationMismatchError as exc: # pragma: no cover - defensive
raise ActivityForbiddenError("Deal belongs to another organization") from exc
async def _ensure_deal_in_context(self, deal_id: int, context: OrganizationContext) -> Deal:
deal = await self._repository.session.get(Deal, deal_id)
if deal is None or deal.organization_id != context.organization_id:
raise ActivityForbiddenError("Deal not found in current organization")
return deal