Files
tgex-backend/src/usecase/purchase/get_placement.py
2026-01-28 12:34:00 +03:00

118 lines
4.3 KiB
Python

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,
)