This commit is contained in:
Artem Tsyrulnikov
2026-01-15 22:02:16 +03:00
parent 6ec26c96e3
commit 049024ebe8
72 changed files with 2417 additions and 2054 deletions

View File

@@ -0,0 +1,148 @@
import logging
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
def _build_cost_info(cost_type: domain.CostType | None, cost_value: float | None) -> dto.CostInfo | None:
if cost_type is None or cost_value is None:
return None
return dto.CostInfo(type=cost_type, value=cost_value)
def _build_placement_details(placement: domain.Placement) -> dto.PlacementDetails | None:
details = dto.PlacementDetails(
placement_at=placement.placement_at,
payment_at=placement.payment_at,
cost=_build_cost_info(placement.cost_type, placement.cost_value),
cost_before_bargain=placement.cost_before_bargain,
placement_type=placement.placement_type,
format=placement.format,
comment=placement.comment,
)
if details.model_dump(exclude_none=True):
return details
return None
def _build_placement_output(placement: domain.Placement) -> dto.PlacementOutput:
channel = placement.channel
if channel is None:
log.error('Placement %s has no channel prefetched', placement.id)
raise ValueError(f'Placement {placement.id} has no channel')
return dto.PlacementOutput(
id=placement.id,
status=placement.status,
comment=placement.comment,
invite_link=placement.invite_link,
invite_link_type=placement.invite_link_type,
channel=dto.ChannelOutput(
id=channel.id,
telegram_id=channel.telegram_id,
title=channel.title,
username=channel.username,
),
details=_build_placement_details(placement),
)
async def create_placements(
self: 'Usecase',
project_id: uuid.UUID,
workspace_id: uuid.UUID,
user_id: uuid.UUID,
input: dto.CreatePlacementsInput,
) -> dto.GetPlacementsOutput:
"""Create multiple placements for different channels (bulk creation, бывший create_purchase)"""
context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE)
project = await self.database.get_project(workspace_id, project_id=project_id)
if not project:
raise domain.ProjectNotFound(project_id)
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_WRITE, project.id)
creative = await self.database.get_creative(workspace_id, input.creative_id)
if not creative or creative.project_id != project.id:
raise domain.CreativeNotFound(input.creative_id)
if project.channel.telegram_id is None:
raise domain.ChannelNotFound(project.channel.id)
invite_link_type = project.purchase_invite_type_default
requires_approval = invite_link_type == domain.InviteLinkType.APPROVAL
details = input.details
placements: list[domain.Placement] = []
for channel_input in input.channels:
channel = await self.database.get_channel(username=channel_input.username)
if not channel:
parser_response = await self.parser.fetch_telegram_channel(channel_input.username)
if not parser_response:
raise domain.TelegramChannelNotFound(channel_input.username)
channel = domain.Channel(
username=parser_response.username,
telegram_id=parser_response.telegram_id,
title=parser_response.title,
)
await self.database.create_channel(channel)
log.info('Created channel @%s with telegram_id=%s', channel.username, channel.telegram_id)
invite_link = await self.telegram_bot.create_chat_invite_link(project.channel.telegram_id, requires_approval)
log.info(
'Created invite link for placement channel_telegram_id=%s requires_approval=%s invite_link=%s',
project.channel.telegram_id,
requires_approval,
invite_link,
)
# Объединяем общие детали с деталями конкретного канала
channel_details = channel_input.details
placement = domain.Placement(
project_id=project.id,
creative_id=creative.id,
channel_id=channel.id,
invite_link=invite_link,
invite_link_type=invite_link_type,
status=channel_input.status or domain.PlacementStatus.PLANNED,
comment=channel_input.comment,
# Приоритет: channel details > общие details
placement_at=(channel_details.placement_at if channel_details else None) or (
details.placement_at if details else None
),
payment_at=details.payment_at if details else None,
cost_type=(channel_details.cost.type if channel_details and channel_details.cost else None) or (
details.cost.type if details and details.cost else None
),
cost_value=(channel_details.cost.value if channel_details and channel_details.cost else None) or (
details.cost.value if details and details.cost else None
),
cost_before_bargain=(channel_details.cost_before_bargain if channel_details else None) or (
details.cost_before_bargain if details else None
),
placement_type=details.placement_type if details else None,
format=(channel_details.format if channel_details else None) or (details.format if details else None),
)
await self.database.create_placement(placement)
placement.channel = channel
placements.append(placement)
log.info(
'Created %s placements for project %s (creative %s)',
len(placements),
project.id,
creative.id,
)
return dto.GetPlacementsOutput(placements=[_build_placement_output(p) for p in placements])