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, creative_id=placement.creative_id, 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) if project.channel.telegram_id is None: raise domain.ChannelNotFound(project.channel.id) invite_link_type = project.purchase_invite_type_default details = input.details placements: list[domain.Placement] = [] creatives_by_id: dict[uuid.UUID, domain.Creative] = {} for channel_input in input.channels: channel = await self.database.get_channel(channel_id=channel_input.channel_id) if not channel: raise domain.ChannelNotFound(channel_input.channel_id) # Объединяем общие детали с деталями конкретного канала channel_details = channel_input.details creative_id = ( (channel_details.creative_id if channel_details and channel_details.creative_id else None) or (details.creative_id if details and details.creative_id else None) or input.creative_id ) # Если creative_id передан, ищем и валидируем креатив creative: domain.Creative | None = None if creative_id: creative = creatives_by_id.get(creative_id) if not creative: creative = await self.database.get_creative(workspace_id, creative_id) if not creative or creative.project_id != project.id: raise domain.CreativeNotFound(creative_id) creatives_by_id[creative_id] = creative placement = domain.Placement( project_id=project.id, creative_id=creative.id if creative else None, channel_id=channel.id, invite_link=None, invite_link_type=invite_link_type, status=channel_input.status or domain.PlacementStatus.NO_STATUS, 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 (creatives %s)', len(placements), project.id, list(creatives_by_id.keys()), ) placement_outputs = [] for placement in placements: placement_output = _build_placement_output(placement) placement_outputs.append( dto.PlacementWithPostsOutput( **placement_output.model_dump(), placement_post=None, ) ) return dto.GetPlacementsOutput(placements=placement_outputs)