148 lines
5.8 KiB
Python
148 lines
5.8 KiB
Python
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)
|