Refactor code for improved readability and consistency
Test / test (push) Successful in 15s

- 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.
This commit is contained in:
Artem Kashaev
2025-12-01 16:18:03 +05:00
parent eecb74c523
commit 5fcb574aca
62 changed files with 765 additions and 476 deletions
+132 -120
View File
@@ -1,4 +1,5 @@
"""Business logic for deals."""
from __future__ import annotations
from collections.abc import Iterable
@@ -16,162 +17,173 @@ from app.repositories.deal_repo import DealRepository
from app.services.analytics_service import invalidate_analytics_cache
from app.services.organization_service import OrganizationContext
STAGE_ORDER = {
stage: index
for index, stage in enumerate(
[
DealStage.QUALIFICATION,
DealStage.PROPOSAL,
DealStage.NEGOTIATION,
DealStage.CLOSED,
]
)
stage: index
for index, stage in enumerate(
[
DealStage.QUALIFICATION,
DealStage.PROPOSAL,
DealStage.NEGOTIATION,
DealStage.CLOSED,
],
)
}
class DealServiceError(Exception):
"""Base class for deal service errors."""
"""Base class for deal service errors."""
class DealOrganizationMismatchError(DealServiceError):
"""Raised when attempting to use resources from another organization."""
"""Raised when attempting to use resources from another organization."""
class DealStageTransitionError(DealServiceError):
"""Raised when stage transition violates business rules."""
"""Raised when stage transition violates business rules."""
class DealStatusValidationError(DealServiceError):
"""Raised when invalid status transitions are requested."""
"""Raised when invalid status transitions are requested."""
class ContactHasDealsError(DealServiceError):
"""Raised when attempting to delete a contact with active deals."""
"""Raised when attempting to delete a contact with active deals."""
@dataclass(slots=True)
class DealUpdateData:
"""Structured container for deal update operations."""
"""Structured container for deal update operations."""
status: DealStatus | None = None
stage: DealStage | None = None
amount: Decimal | None = None
currency: str | None = None
status: DealStatus | None = None
stage: DealStage | None = None
amount: Decimal | None = None
currency: str | None = None
class DealService:
"""Encapsulates deal workflows and validations."""
"""Encapsulates deal workflows and validations."""
def __init__(
self,
repository: DealRepository,
cache: Redis | None = None,
*,
cache_backoff_ms: int = 0,
) -> None:
self._repository = repository
self._cache = cache
self._cache_backoff_ms = cache_backoff_ms
def __init__(
self,
repository: DealRepository,
cache: Redis | None = None,
*,
cache_backoff_ms: int = 0,
) -> None:
self._repository = repository
self._cache = cache
self._cache_backoff_ms = cache_backoff_ms
async def create_deal(self, data: DealCreate, *, context: OrganizationContext) -> Deal:
self._ensure_same_organization(data.organization_id, context)
await self._ensure_contact_in_organization(data.contact_id, context.organization_id)
deal = await self._repository.create(data=data, role=context.role, user_id=context.user_id)
await invalidate_analytics_cache(self._cache, context.organization_id, self._cache_backoff_ms)
return deal
async def create_deal(self, data: DealCreate, *, context: OrganizationContext) -> Deal:
self._ensure_same_organization(data.organization_id, context)
await self._ensure_contact_in_organization(data.contact_id, context.organization_id)
deal = await self._repository.create(data=data, role=context.role, user_id=context.user_id)
await invalidate_analytics_cache(
self._cache, context.organization_id, self._cache_backoff_ms
)
return deal
async def update_deal(
self,
deal: Deal,
updates: DealUpdateData,
*,
context: OrganizationContext,
) -> Deal:
self._ensure_same_organization(deal.organization_id, context)
changes: dict[str, object] = {}
stage_activity: tuple[ActivityType, dict[str, str]] | None = None
status_activity: tuple[ActivityType, dict[str, str]] | None = None
async def update_deal(
self,
deal: Deal,
updates: DealUpdateData,
*,
context: OrganizationContext,
) -> Deal:
self._ensure_same_organization(deal.organization_id, context)
changes: dict[str, object] = {}
stage_activity: tuple[ActivityType, dict[str, str]] | None = None
status_activity: tuple[ActivityType, dict[str, str]] | None = None
if updates.amount is not None:
changes["amount"] = updates.amount
if updates.currency is not None:
changes["currency"] = updates.currency
if updates.amount is not None:
changes["amount"] = updates.amount
if updates.currency is not None:
changes["currency"] = updates.currency
if updates.stage is not None and updates.stage != deal.stage:
self._validate_stage_transition(deal.stage, updates.stage, context.role)
changes["stage"] = updates.stage
stage_activity = (
ActivityType.STAGE_CHANGED,
{"old_stage": deal.stage, "new_stage": updates.stage},
)
if updates.stage is not None and updates.stage != deal.stage:
self._validate_stage_transition(deal.stage, updates.stage, context.role)
changes["stage"] = updates.stage
stage_activity = (
ActivityType.STAGE_CHANGED,
{"old_stage": deal.stage, "new_stage": updates.stage},
)
if updates.status is not None and updates.status != deal.status:
self._validate_status_transition(deal, updates)
changes["status"] = updates.status
status_activity = (
ActivityType.STATUS_CHANGED,
{"old_status": deal.status, "new_status": updates.status},
)
if updates.status is not None and updates.status != deal.status:
self._validate_status_transition(deal, updates)
changes["status"] = updates.status
status_activity = (
ActivityType.STATUS_CHANGED,
{"old_status": deal.status, "new_status": updates.status},
)
if not changes:
return deal
if not changes:
return deal
updated = await self._repository.update(deal, changes, role=context.role, user_id=context.user_id)
await self._log_activities(
deal_id=deal.id,
author_id=context.user_id,
activities=[activity for activity in [stage_activity, status_activity] if activity],
)
await invalidate_analytics_cache(self._cache, context.organization_id, self._cache_backoff_ms)
return updated
updated = await self._repository.update(
deal, changes, role=context.role, user_id=context.user_id
)
await self._log_activities(
deal_id=deal.id,
author_id=context.user_id,
activities=[activity for activity in [stage_activity, status_activity] if activity],
)
await invalidate_analytics_cache(
self._cache, context.organization_id, self._cache_backoff_ms
)
return updated
async def ensure_contact_can_be_deleted(self, contact_id: int) -> None:
stmt = select(func.count()).select_from(Deal).where(Deal.contact_id == contact_id)
count = await self._repository.session.scalar(stmt)
if count and count > 0:
raise ContactHasDealsError("Contact has related deals and cannot be deleted")
async def ensure_contact_can_be_deleted(self, contact_id: int) -> None:
stmt = select(func.count()).select_from(Deal).where(Deal.contact_id == contact_id)
count = await self._repository.session.scalar(stmt)
if count and count > 0:
raise ContactHasDealsError("Contact has related deals and cannot be deleted")
async def _log_activities(
self,
*,
deal_id: int,
author_id: int,
activities: Iterable[tuple[ActivityType, dict[str, str]]],
) -> None:
entries = list(activities)
if not entries:
return
for activity_type, payload in entries:
activity = Activity(deal_id=deal_id, author_id=author_id, type=activity_type, payload=payload)
self._repository.session.add(activity)
await self._repository.session.flush()
async def _log_activities(
self,
*,
deal_id: int,
author_id: int,
activities: Iterable[tuple[ActivityType, dict[str, str]]],
) -> None:
entries = list(activities)
if not entries:
return
for activity_type, payload in entries:
activity = Activity(
deal_id=deal_id, author_id=author_id, type=activity_type, payload=payload
)
self._repository.session.add(activity)
await self._repository.session.flush()
def _ensure_same_organization(self, organization_id: int, context: OrganizationContext) -> None:
if organization_id != context.organization_id:
raise DealOrganizationMismatchError("Operation targets a different organization")
def _ensure_same_organization(self, organization_id: int, context: OrganizationContext) -> None:
if organization_id != context.organization_id:
raise DealOrganizationMismatchError("Operation targets a different organization")
async def _ensure_contact_in_organization(self, contact_id: int, organization_id: int) -> Contact:
contact = await self._repository.session.get(Contact, contact_id)
if contact is None or contact.organization_id != organization_id:
raise DealOrganizationMismatchError("Contact belongs to another organization")
return contact
async def _ensure_contact_in_organization(
self, contact_id: int, organization_id: int
) -> Contact:
contact = await self._repository.session.get(Contact, contact_id)
if contact is None or contact.organization_id != organization_id:
raise DealOrganizationMismatchError("Contact belongs to another organization")
return contact
def _validate_stage_transition(
self,
current_stage: DealStage,
new_stage: DealStage,
role: OrganizationRole,
) -> None:
if STAGE_ORDER[new_stage] < STAGE_ORDER[current_stage] and role not in {
OrganizationRole.OWNER,
OrganizationRole.ADMIN,
}:
raise DealStageTransitionError("Stage rollback requires owner or admin role")
def _validate_stage_transition(
self,
current_stage: DealStage,
new_stage: DealStage,
role: OrganizationRole,
) -> None:
if STAGE_ORDER[new_stage] < STAGE_ORDER[current_stage] and role not in {
OrganizationRole.OWNER,
OrganizationRole.ADMIN,
}:
raise DealStageTransitionError("Stage rollback requires owner or admin role")
def _validate_status_transition(self, deal: Deal, updates: DealUpdateData) -> None:
if updates.status != DealStatus.WON:
return
effective_amount = updates.amount if updates.amount is not None else deal.amount
if effective_amount is None or Decimal(effective_amount) <= Decimal("0"):
raise DealStatusValidationError("Amount must be greater than zero to mark a deal as won")
def _validate_status_transition(self, deal: Deal, updates: DealUpdateData) -> None:
if updates.status != DealStatus.WON:
return
effective_amount = updates.amount if updates.amount is not None else deal.amount
if effective_amount is None or Decimal(effective_amount) <= Decimal("0"):
raise DealStatusValidationError(
"Amount must be greater than zero to mark a deal as won"
)