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)