Files
tgex-backend/src/usecase/purchase/get_placements.py
Artem Tsyrulnikov 31b63e5a39 правки
2026-02-02 11:59:37 +03:00

109 lines
4.5 KiB
Python

import logging
import uuid
from typing import TYPE_CHECKING
from tortoise import timezone
from src import domain, dto
from .create_placements import _build_placement_output
from .get_placement import _build_placement_post_output
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.GetPlacementsOutput:
"""Get all placements for a project (formerly get_purchases)"""
context = await self.ensure_workspace_permission(
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
)
project = await self.database.get_project(input.workspace_id, project_id=input.project_id)
if not project:
raise domain.ProjectNotFound(input.project_id)
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, project.id)
# Prefetch project channel for output building
if project.channel is None:
project_channel = await self.database.get_channel(channel_id=project.channel_id)
project.channel = project_channel
placements = await self.database.get_project_placements(input.workspace_id, project.id)
log.debug('Fetched %s placements for project %s', len(placements), project.id)
placement_ids = [placement.id for placement in placements]
placement_posts = await self.database.get_placement_posts_by_placement_ids(
input.workspace_id,
placement_ids,
include_archived=True,
)
# Подсчёт подписок по placement_id (один Placement = одна ссылка = один счётчик подписок)
subscriptions_counts = await self.database.count_subscriptions_by_placement_batch(placement_ids)
post_ids = [placement_post.post.id for placement_post in placement_posts if placement_post.post]
views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {}
# Collect (channel_id, message_id) pairs for batch next post lookup
channel_message_pairs = [
(placement_post.post.channel_id, placement_post.post.message_id)
for placement_post in placement_posts
if placement_post.post
]
next_posts_map = await self.database.get_next_posts_after_batch(channel_message_pairs)
# Calculate time_on_top for each placement_post
time_on_top_map: dict[uuid.UUID, int] = {}
now = timezone.now()
for placement_post in placement_posts:
post = placement_post.post
if not post or not post.published_at:
continue
published_at = post.published_at
key = (post.channel_id, post.message_id)
next_post = next_posts_map.get(key)
if next_post and next_post.published_at:
time_on_top_map[placement_post.id] = int((next_post.published_at - published_at).total_seconds())
else:
time_on_top_map[placement_post.id] = int((now - published_at).total_seconds())
placement_posts_by_placement_id: dict[uuid.UUID, list[dto.PlacementPostOutput]] = {}
for placement_post in placement_posts:
views_count = None
time_on_top = time_on_top_map.get(placement_post.id)
if placement_post.post:
views_count = views_map.get(placement_post.post.id, (None,))[0]
placement_post_output = _build_placement_post_output(
placement_post,
subscriptions_counts.get(placement_post.placement_id, 0),
views_count,
time_on_top,
)
if placement_post_output is None:
continue
placement_posts_by_placement_id.setdefault(placement_post.placement_id, []).append(placement_post_output)
placement_outputs = []
for placement in placements:
placement_output = _build_placement_output(placement, project=project)
placement_post_output = None
placement_posts_for_placement = placement_posts_by_placement_id.get(placement.id, [])
if placement_posts_for_placement:
if len(placement_posts_for_placement) > 1:
log.warning(
'Placement %s has %s placement_posts, returning latest',
placement.id,
len(placement_posts_for_placement),
)
placement_post_output = placement_posts_for_placement[0]
placement_outputs.append(
dto.PlacementWithPostsOutput(
**placement_output.model_dump(),
placement_post=placement_post_output,
)
)
return dto.GetPlacementsOutput(placements=placement_outputs)