import logging from typing import TYPE_CHECKING from tortoise import timezone from src import domain, dto from src.usecase.purchase.create_placements import _build_placement_output log = logging.getLogger(__name__) def _build_post_output(post: domain.Post) -> dto.PostOutput: channel = post.channel if not channel: raise ValueError(f'Post {post.id} has no channel') return dto.PostOutput( id=post.id, message_id=post.message_id, text=post.text, url=post.url, deleted_from_channel_at=post.deleted_from_channel_at, created_at=post.created_at, updated_at=post.updated_at, ) def _build_placement_post_output( placement_post: domain.PlacementPost, subscriptions_count: int, views_count: int | None, time_on_top: int | None = None, ) -> dto.PlacementPostOutput | None: if not placement_post.post: log.warning('PlacementPost %s missing post', placement_post.id) return None try: post_output = _build_post_output(placement_post.post) except ValueError: log.warning('PlacementPost %s post missing channel data', placement_post.id) return None return dto.PlacementPostOutput( subscriptions_count=subscriptions_count, views_count=views_count, created_at=placement_post.created_at, time_on_top=time_on_top, post=post_output, ) if TYPE_CHECKING: from .. import Usecase async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> dto.PlacementWithPostsOutput: """Get single placement by ID (user-managed)""" 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) placement = await self.database.get_placement(input.workspace_id, input.placement_id) if not placement or placement.project_id != project.id: raise domain.PlacementNotFound(input.placement_id) # Prefetch channel for output building if placement.channel is None: channel = await self.database.get_channel(channel_id=placement.channel_id) placement.channel = channel placement_posts = await self.database.get_workspace_placement_posts( input.workspace_id, placement_id=placement.id, include_archived=True, ) # Подсчёт подписок по placement_id (один Placement = одна ссылка = один счётчик подписок) subscriptions_count = await self.database.count_subscriptions_by_placement(placement.id) 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 {} placement_post_output = None if placement_posts: if len(placement_posts) > 1: log.warning('Placement %s has %s placement_posts, returning latest', placement.id, len(placement_posts)) for placement_post in placement_posts: views_count = None time_on_top = None if placement_post.post: views_count = views_map.get(placement_post.post.id, (None,))[0] # Calculate time_on_top post = placement_post.post next_post = await self.database.get_next_post_after(post.channel_id, post.message_id) if next_post and next_post.published_at and post.published_at: time_on_top = int((next_post.published_at - post.published_at).total_seconds()) elif post.published_at: time_on_top = int((timezone.now() - post.published_at).total_seconds()) placement_post_output = _build_placement_post_output( placement_post, subscriptions_count, views_count, time_on_top, ) if placement_post_output is not None: break placement_output = _build_placement_output(placement) return dto.PlacementWithPostsOutput( **placement_output.model_dump(), placement_post=placement_post_output, )