refactor
This commit is contained in:
@@ -27,9 +27,9 @@ from .creative.delete_creative import delete_creative
|
||||
from .creative.get_creative import get_creative
|
||||
from .creative.get_creatives import get_creatives
|
||||
from .creative.update_creative import update_creative
|
||||
from .placement.fetch_publication_post_cycle import fetch_publication_post_cycle
|
||||
from .placement.get_publication import get_publication
|
||||
from .placement.get_publications import get_publications
|
||||
from .placement.fetch_placement_post_cycle import fetch_placement_post_cycle
|
||||
from .placement.get_placement_post import get_placement_post
|
||||
from .placement.get_placement_posts import get_placement_posts
|
||||
from .project.archive_project import archive_project
|
||||
from .project.delete_project import delete_project
|
||||
from .project.disconnect_project_by_tg_id import disconnect_project_by_tg_id
|
||||
@@ -171,10 +171,10 @@ class Usecase:
|
||||
create_creative = create_creative
|
||||
update_creative = update_creative
|
||||
delete_creative = delete_creative
|
||||
# Publication (system-managed) use cases
|
||||
get_publications = get_publications
|
||||
get_publication = get_publication
|
||||
fetch_publication_post_cycle = fetch_publication_post_cycle
|
||||
# PlacementPost (system-managed) use cases
|
||||
get_placement_posts = get_placement_posts
|
||||
get_placement_post = get_placement_post
|
||||
fetch_placement_post_cycle = fetch_placement_post_cycle
|
||||
handle_subscription = handle_subscription
|
||||
handle_unsubscription = handle_unsubscription
|
||||
get_views_history = get_views_history
|
||||
|
||||
@@ -29,7 +29,7 @@ async def get_channel_analytics(
|
||||
else:
|
||||
allowed_project_filter = allowed_project_ids
|
||||
|
||||
placements = await self.database.get_workspace_publications(
|
||||
placements = await self.database.get_workspace_placement_posts(
|
||||
input.workspace_id,
|
||||
input.project_id,
|
||||
include_archived=False,
|
||||
@@ -38,8 +38,8 @@ async def get_channel_analytics(
|
||||
|
||||
# Collect unique channels from placements
|
||||
channel_map: dict[UUID, domain.Channel] = {}
|
||||
for publication in placements:
|
||||
placement = publication.placement
|
||||
for placement_post in placements:
|
||||
placement = placement_post.placement
|
||||
if placement and placement.channel:
|
||||
channel_map[placement.channel_id] = placement.channel
|
||||
channels = list(channel_map.values())
|
||||
@@ -59,22 +59,22 @@ async def get_channel_analytics(
|
||||
|
||||
# Batch fetch subscriptions counts
|
||||
placement_ids = [p.id for p in placements]
|
||||
subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(placement_ids)
|
||||
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_ids)
|
||||
|
||||
for publication in placements:
|
||||
placement = publication.placement
|
||||
for placement_post in placements:
|
||||
placement = placement_post.placement
|
||||
if not placement:
|
||||
continue
|
||||
stats = channel_stats.get(placement.channel_id)
|
||||
if stats is None:
|
||||
continue
|
||||
|
||||
stats.total_cost += publication.cost if publication.cost is not None else 0
|
||||
stats.total_subscriptions += subscriptions_counts.get(publication.id, 0)
|
||||
stats.total_cost += placement_post.cost if placement_post.cost is not None else 0
|
||||
stats.total_subscriptions += subscriptions_counts.get(placement_post.id, 0)
|
||||
|
||||
# Get views from batch data
|
||||
if publication.post and publication.post.id in views_map:
|
||||
views_count = views_map[publication.post.id][0]
|
||||
if placement_post.post and placement_post.post.id in views_map:
|
||||
views_count = views_map[placement_post.post.id][0]
|
||||
stats.total_views += views_count
|
||||
|
||||
stats.placements_count += 1
|
||||
|
||||
@@ -33,7 +33,7 @@ async def get_creatives_analytics(
|
||||
include_archived=False,
|
||||
allowed_project_ids=allowed_project_ids,
|
||||
)
|
||||
placements = await self.database.get_workspace_publications(
|
||||
placements = await self.database.get_workspace_placement_posts(
|
||||
input.workspace_id,
|
||||
input.project_id,
|
||||
include_archived=False,
|
||||
@@ -55,7 +55,7 @@ async def get_creatives_analytics(
|
||||
|
||||
# Batch fetch subscriptions counts
|
||||
placement_ids = [p.id for p in placements]
|
||||
subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(placement_ids)
|
||||
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_ids)
|
||||
|
||||
for p in placements:
|
||||
stats = creative_stats.get(p.creative_id)
|
||||
|
||||
@@ -55,7 +55,7 @@ async def get_overview_analytics(
|
||||
period_length = raw_duration if raw_duration.total_seconds() > 0 else datetime.timedelta(days=1)
|
||||
previous_period_start = input.date_from - period_length
|
||||
|
||||
placements = await self.database.get_workspace_publications(
|
||||
placements = await self.database.get_workspace_placement_posts(
|
||||
input.workspace_id,
|
||||
project_id=input.project_id,
|
||||
include_archived=False,
|
||||
@@ -64,18 +64,18 @@ async def get_overview_analytics(
|
||||
date_to=input.date_to,
|
||||
)
|
||||
|
||||
current_placements: list[domain.Publication] = []
|
||||
previous_placements: list[domain.Publication] = []
|
||||
current_placements: list[domain.PlacementPost] = []
|
||||
previous_placements: list[domain.PlacementPost] = []
|
||||
|
||||
for publication in placements:
|
||||
placement_date = publication.wanted_placement_date
|
||||
for placement_post in placements:
|
||||
placement_date = placement_post.wanted_placement_date
|
||||
if placement_date >= input.date_from and placement_date <= input.date_to:
|
||||
current_placements.append(publication)
|
||||
current_placements.append(placement_post)
|
||||
elif placement_date >= previous_period_start and placement_date < input.date_from:
|
||||
previous_placements.append(publication)
|
||||
previous_placements.append(placement_post)
|
||||
|
||||
placement_ids = [p.id for p in placements]
|
||||
subscriptions = await self.database.get_subscriptions_for_publications(
|
||||
subscriptions = await self.database.get_subscriptions_for_placement_posts(
|
||||
placement_ids, date_from=previous_period_start, date_to=input.date_to
|
||||
)
|
||||
|
||||
@@ -89,30 +89,30 @@ async def get_overview_analytics(
|
||||
continue
|
||||
|
||||
if created_at >= input.date_from and created_at <= input.date_to:
|
||||
subs_per_placement_current[sub.publication_id] += 1
|
||||
subs_per_placement_current[sub.placement_post_id] += 1
|
||||
subs_per_day_current[created_at.date()] += 1
|
||||
elif created_at >= previous_period_start and created_at < input.date_from:
|
||||
subs_per_placement_previous[sub.publication_id] += 1
|
||||
subs_per_placement_previous[sub.placement_post_id] += 1
|
||||
|
||||
post_ids = [p.post.id for p in placements if p.post]
|
||||
views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {}
|
||||
|
||||
def _aggregate_totals(
|
||||
placement_list: list[domain.Publication], subs_per_placement: dict[UUID, int]
|
||||
placement_list: list[domain.PlacementPost], subs_per_placement: dict[UUID, int]
|
||||
) -> tuple[float, int, int]:
|
||||
total_cost = 0.0
|
||||
total_subscriptions = 0
|
||||
total_views = 0
|
||||
|
||||
for publication in placement_list:
|
||||
if publication.cost is not None:
|
||||
total_cost += publication.cost
|
||||
for placement_post in placement_list:
|
||||
if placement_post.cost is not None:
|
||||
total_cost += placement_post.cost
|
||||
|
||||
subs = subs_per_placement.get(publication.id, 0)
|
||||
subs = subs_per_placement.get(placement_post.id, 0)
|
||||
total_subscriptions += subs
|
||||
|
||||
if publication.post and publication.post.id in views_map:
|
||||
total_views += views_map[publication.post.id][0]
|
||||
if placement_post.post and placement_post.post.id in views_map:
|
||||
total_views += views_map[placement_post.post.id][0]
|
||||
|
||||
return total_cost, total_subscriptions, total_views
|
||||
|
||||
@@ -130,16 +130,16 @@ async def get_overview_analytics(
|
||||
project_spending_map: dict[UUID, float] = defaultdict(float)
|
||||
project_meta: dict[UUID, domain.Project] = {}
|
||||
|
||||
for publication in current_placements:
|
||||
placement_day = publication.wanted_placement_date.date()
|
||||
project_meta[publication.project_id] = publication.project
|
||||
for placement_post in current_placements:
|
||||
placement_day = placement_post.wanted_placement_date.date()
|
||||
project_meta[placement_post.project_id] = placement_post.project
|
||||
|
||||
cost_value = publication.cost or 0.0
|
||||
cost_value = placement_post.cost or 0.0
|
||||
cost_per_day[placement_day] += cost_value
|
||||
project_spending_map[publication.project_id] += cost_value
|
||||
subs = subs_per_placement_current.get(publication.id, 0)
|
||||
project_spending_map[placement_post.project_id] += cost_value
|
||||
subs = subs_per_placement_current.get(placement_post.id, 0)
|
||||
|
||||
placement = publication.placement
|
||||
placement = placement_post.placement
|
||||
if placement:
|
||||
channel_id = placement.channel_id
|
||||
if channel_id not in channel_stats:
|
||||
|
||||
@@ -37,7 +37,7 @@ async def get_placements_analytics(
|
||||
context.ensure_project_permission(domain.PermissionKey.ANALYTICS_READ, project.id)
|
||||
allowed_project_ids = None
|
||||
|
||||
placements = await self.database.get_workspace_publications(
|
||||
placements = await self.database.get_workspace_placement_posts(
|
||||
input.workspace_id,
|
||||
input.project_id,
|
||||
include_archived=False,
|
||||
@@ -50,7 +50,7 @@ async def get_placements_analytics(
|
||||
|
||||
# Batch fetch subscriptions counts
|
||||
placement_ids = [p.id for p in placements]
|
||||
subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(placement_ids)
|
||||
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_ids)
|
||||
|
||||
return [
|
||||
dto.PlacementAnalyticsOutput(
|
||||
|
||||
@@ -47,7 +47,7 @@ def _format_period_label(dt: datetime.datetime, grouping: dto.DateGrouping) -> s
|
||||
if week_start.month == week_end.month:
|
||||
return f'{week_start.day}-{week_end.day} {month_names[week_start.month - 1]}'
|
||||
else:
|
||||
return f'{week_start.day} {month_names[week_start.month - 1]}-{week_end.day} {month_names[week_end.month - 1]}'
|
||||
return f'{week_start.day} {month_names[week_start.month - 1]}-{week_end.day} {month_names[week_end.month - 1]}' # noqa: E501
|
||||
case dto.DateGrouping.MONTH:
|
||||
# "дек 2024"
|
||||
month_names = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек']
|
||||
@@ -60,20 +60,20 @@ def _format_period_label(dt: datetime.datetime, grouping: dto.DateGrouping) -> s
|
||||
raise ValueError('Invalid date grouping')
|
||||
|
||||
|
||||
def _get_grouping_date(publication: domain.Publication, date_grouping: dto.DateGroupingType) -> datetime.datetime:
|
||||
def _get_grouping_date(placement_post: domain.PlacementPost, date_grouping: dto.DateGroupingType) -> datetime.datetime:
|
||||
match date_grouping:
|
||||
case dto.DateGroupingType.PLACEMENT_DATE:
|
||||
return publication.wanted_placement_date
|
||||
return placement_post.wanted_placement_date
|
||||
case dto.DateGroupingType.PURCHASE_DATE:
|
||||
if publication.placement:
|
||||
return publication.placement.created_at
|
||||
return publication.wanted_placement_date # Fallback
|
||||
if placement_post.placement:
|
||||
return placement_post.placement.created_at
|
||||
return placement_post.wanted_placement_date # Fallback
|
||||
case dto.DateGroupingType.LINK_DATE:
|
||||
if publication.placement:
|
||||
return publication.placement.created_at
|
||||
return publication.wanted_placement_date # Fallback
|
||||
if placement_post.placement:
|
||||
return placement_post.placement.created_at
|
||||
return placement_post.wanted_placement_date # Fallback
|
||||
case _:
|
||||
return publication.wanted_placement_date
|
||||
return placement_post.wanted_placement_date
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -183,7 +183,7 @@ async def get_projects_analytics(
|
||||
else:
|
||||
allowed_project_ids = set(input.project_ids)
|
||||
|
||||
placements = await self.database.get_workspace_publications(
|
||||
placements = await self.database.get_workspace_placement_posts(
|
||||
input.workspace_id,
|
||||
project_id=None,
|
||||
include_archived=False,
|
||||
@@ -194,28 +194,28 @@ async def get_projects_analytics(
|
||||
|
||||
# Фильтрация по датам на основе date_grouping
|
||||
filtered_placements = []
|
||||
for publication in placements:
|
||||
grouping_date = _get_grouping_date(publication, input.date_grouping)
|
||||
for placement_post in placements:
|
||||
grouping_date = _get_grouping_date(placement_post, input.date_grouping)
|
||||
if input.date_from and grouping_date < input.date_from:
|
||||
continue
|
||||
if input.date_to and grouping_date > input.date_to:
|
||||
continue
|
||||
filtered_placements.append(publication)
|
||||
filtered_placements.append(placement_post)
|
||||
|
||||
# Batch fetch views data
|
||||
post_ids = [p.post.id for p in filtered_placements if p.post]
|
||||
views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {}
|
||||
|
||||
# Batch fetch subscriptions counts
|
||||
publication_ids = [p.id for p in filtered_placements]
|
||||
subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(publication_ids)
|
||||
placement_post_ids = [p.id for p in filtered_placements]
|
||||
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids)
|
||||
|
||||
# Группировка по периодам
|
||||
period_data: dict[str, PeriodMetrics] = defaultdict(PeriodMetrics)
|
||||
total_metrics = PeriodMetrics()
|
||||
|
||||
for publication in filtered_placements:
|
||||
grouping_date = _get_grouping_date(publication, input.date_grouping)
|
||||
for placement_post in filtered_placements:
|
||||
grouping_date = _get_grouping_date(placement_post, input.date_grouping)
|
||||
period = _format_period(grouping_date, input.grouping)
|
||||
pd = period_data[period]
|
||||
|
||||
@@ -223,25 +223,25 @@ async def get_projects_analytics(
|
||||
pd.purchases_count += 1
|
||||
total_metrics.purchases_count += 1
|
||||
|
||||
cost = publication.cost or 0.0
|
||||
cost = placement_post.cost or 0.0
|
||||
pd.total_cost += cost
|
||||
total_metrics.total_cost += cost
|
||||
|
||||
subs_count = subscriptions_counts.get(publication.id, 0)
|
||||
subs_count = subscriptions_counts.get(placement_post.id, 0)
|
||||
pd.total_subscriptions += subs_count
|
||||
pd.clicks_count += subs_count
|
||||
total_metrics.total_subscriptions += subs_count
|
||||
total_metrics.clicks_count += subs_count
|
||||
|
||||
if publication.post and publication.post.id in views_map:
|
||||
views_count = views_map[publication.post.id][0]
|
||||
if placement_post.post and placement_post.post.id in views_map:
|
||||
views_count = views_map[placement_post.post.id][0]
|
||||
pd.total_views += views_count
|
||||
pd.reach_volume += views_count
|
||||
total_metrics.total_views += views_count
|
||||
total_metrics.reach_volume += views_count
|
||||
|
||||
# Расчет скидок
|
||||
placement = publication.placement
|
||||
placement = placement_post.placement
|
||||
if placement and placement.cost_before_bargain and placement.cost_before_bargain > cost:
|
||||
discount = placement.cost_before_bargain - cost
|
||||
discount_percent = (
|
||||
|
||||
@@ -46,7 +46,7 @@ async def get_spending_analytics(
|
||||
context.ensure_project_permission(domain.PermissionKey.ANALYTICS_READ, project.id)
|
||||
allowed_project_ids = None
|
||||
|
||||
placements = await self.database.get_workspace_publications(
|
||||
placements = await self.database.get_workspace_placement_posts(
|
||||
input.workspace_id,
|
||||
input.project_id,
|
||||
include_archived=False,
|
||||
@@ -76,7 +76,7 @@ async def get_spending_analytics(
|
||||
|
||||
# Batch fetch subscriptions counts
|
||||
placement_ids = [p.id for p in filtered]
|
||||
subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(placement_ids)
|
||||
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_ids)
|
||||
|
||||
# Группировка по периодам
|
||||
period_data: dict[str, PeriodData] = defaultdict(PeriodData)
|
||||
|
||||
@@ -11,9 +11,7 @@ if TYPE_CHECKING:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def attach_channel_to_workspace(
|
||||
self: 'Usecase', input: dto.AttachChannelToWorkspaceInput
|
||||
) -> dto.ProjectOutput:
|
||||
async def attach_channel_to_workspace(self: 'Usecase', input: dto.AttachChannelToWorkspaceInput) -> dto.ProjectOutput:
|
||||
"""Привязать канал к workspace (вызывается из Golang бота после выбора workspace пользователем)"""
|
||||
|
||||
channel = await self.database.get_channel(channel_id=input.channel_id)
|
||||
|
||||
@@ -19,4 +19,4 @@ async def get_channel(self: 'Usecase', input: dto.GetChannelInput) -> dto.Channe
|
||||
telegram_id=channel.telegram_id,
|
||||
title=channel.title,
|
||||
username=channel.username,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -22,9 +22,9 @@ async def delete_creative(self: 'Usecase', input: dto.DeleteCreativeInput) -> No
|
||||
|
||||
context.ensure_project_permission(domain.PermissionKey.PROJECTS_WRITE, creative.project_id)
|
||||
|
||||
has_publications = await self.database.has_publications_for_creative(creative.id)
|
||||
if has_publications:
|
||||
log.warning('Creative %s is used in publications and cannot be deleted', input.creative_id)
|
||||
has_placement_posts = await self.database.has_placement_posts_for_creative(creative.id)
|
||||
if has_placement_posts:
|
||||
log.warning('Creative %s is used in placement_posts and cannot be deleted', input.creative_id)
|
||||
raise domain.CreativeInUse(input.creative_id)
|
||||
|
||||
media_key = creative.media_s3_key
|
||||
|
||||
@@ -20,7 +20,7 @@ async def get_creative(self: 'Usecase', input: dto.GetCreativeInput) -> dto.Crea
|
||||
|
||||
context.ensure_project_permission(domain.PermissionKey.PROJECTS_READ, creative.project_id)
|
||||
|
||||
placements_count = await self.database.count_publications_by_creative(creative.id)
|
||||
placements_count = await self.database.count_placement_posts_by_creative(creative.id)
|
||||
|
||||
return dto.CreativeOutput(
|
||||
id=creative.id,
|
||||
|
||||
@@ -25,7 +25,7 @@ async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> list[d
|
||||
)
|
||||
|
||||
creative_ids = [c.id for c in creatives]
|
||||
placements_counts = await self.database.count_publications_by_creative_batch(creative_ids)
|
||||
placements_counts = await self.database.count_placement_posts_by_creative_batch(creative_ids)
|
||||
|
||||
return [
|
||||
dto.CreativeOutput(
|
||||
|
||||
@@ -69,7 +69,7 @@ async def update_creative(
|
||||
|
||||
await self.database.update_creative(creative)
|
||||
|
||||
placements_count = await self.database.count_publications_by_creative(creative.id)
|
||||
placements_count = await self.database.count_placement_posts_by_creative(creative.id)
|
||||
|
||||
return dto.CreativeOutput(
|
||||
id=creative.id,
|
||||
|
||||
@@ -9,15 +9,13 @@ if TYPE_CHECKING:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def fetch_publication_post_cycle(self: 'Usecase', interval_seconds: int) -> None:
|
||||
"""Находит посты в каналах по invite_link из Placement и создает Publication"""
|
||||
log.debug('Starting fetch_publication_post_cycle')
|
||||
async def fetch_placement_post_cycle(self: 'Usecase', interval_seconds: int) -> None:
|
||||
"""Находит посты в каналах по invite_link из Placement и создает PlacementPost"""
|
||||
log.debug('Starting fetch_placement_post_cycle')
|
||||
|
||||
# Получаем все approved Placements с invite_link
|
||||
placements = (
|
||||
await domain.Placement.filter(
|
||||
status__in=[domain.PlacementStatus.APPROVED, domain.PlacementStatus.IN_PROGRESS]
|
||||
)
|
||||
await domain.Placement.filter(status__in=[domain.PlacementStatus.APPROVED, domain.PlacementStatus.IN_PROGRESS])
|
||||
.prefetch_related('channel', 'project')
|
||||
.all()
|
||||
)
|
||||
@@ -42,23 +40,23 @@ async def fetch_publication_post_cycle(self: 'Usecase', interval_seconds: int) -
|
||||
|
||||
for post in posts:
|
||||
# Проверяем, не создана ли уже публикация для этого поста
|
||||
existing = await domain.Publication.filter(post_id=post.id).first()
|
||||
existing = await domain.PlacementPost.filter(post_id=post.id).first()
|
||||
if existing:
|
||||
continue
|
||||
|
||||
publication = domain.Publication(
|
||||
placement_post = domain.PlacementPost(
|
||||
placement_id=placement.id,
|
||||
post_id=post.id,
|
||||
status=domain.PublicationStatus.ACTIVE,
|
||||
status=domain.PlacementPostStatus.ACTIVE,
|
||||
)
|
||||
|
||||
await self.database.create_publication(publication)
|
||||
await self.database.create_placement_post(placement_post)
|
||||
created_count += 1
|
||||
log.info(
|
||||
'Created publication %s for placement %s from post %s',
|
||||
publication.id,
|
||||
'Created placement_post %s for placement %s from post %s',
|
||||
placement_post.id,
|
||||
placement.id,
|
||||
post.id,
|
||||
)
|
||||
|
||||
log.debug('Fetch publication post cycle completed. Created %s publications', created_count)
|
||||
log.debug('Fetch placement_post post cycle completed. Created %s placement_posts', created_count)
|
||||
@@ -9,47 +9,47 @@ if TYPE_CHECKING:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_publication(self: 'Usecase', input: dto.GetPublicationInput) -> dto.PublicationOutput:
|
||||
async def get_placement_post(self: 'Usecase', input: dto.GetPlacementPostInput) -> dto.PlacementPostOutput:
|
||||
"""Получить одну публикацию по ID (system-managed post)"""
|
||||
context = await self.ensure_workspace_permission(
|
||||
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
|
||||
)
|
||||
|
||||
publication = await self.database.get_publication(input.workspace_id, input.publication_id)
|
||||
if not publication:
|
||||
log.warning('Publication %s not found for user %s', input.publication_id, input.user_id)
|
||||
raise domain.PublicationNotFound(input.publication_id)
|
||||
placement_post = await self.database.get_placement_post(input.workspace_id, input.placement_post_id)
|
||||
if not placement_post:
|
||||
log.warning('PlacementPost %s not found for user %s', input.placement_post_id, input.user_id)
|
||||
raise domain.PlacementPostNotFound(input.placement_post_id)
|
||||
|
||||
placement = publication.placement
|
||||
placement = placement_post.placement
|
||||
if not placement:
|
||||
log.error('Publication %s missing placement data', publication.id)
|
||||
raise domain.PlacementNotFound(publication.placement_id)
|
||||
log.error('PlacementPost %s missing placement data', placement_post.id)
|
||||
raise domain.PlacementNotFound(placement_post.placement_id)
|
||||
|
||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id)
|
||||
|
||||
if not placement.channel or not placement.project or not placement.creative:
|
||||
log.error('Publication %s placement missing related data', publication.id)
|
||||
log.error('PlacementPost %s placement missing related data', placement_post.id)
|
||||
raise domain.PlacementNotFound(placement.id)
|
||||
|
||||
ad_post_url = publication.post.url if publication.post else None
|
||||
subscriptions_count = await self.database.count_subscriptions_by_publication(publication.id)
|
||||
ad_post_url = placement_post.post.url if placement_post.post else None
|
||||
subscriptions_count = await self.database.count_subscriptions_by_placement_post(placement_post.id)
|
||||
|
||||
return dto.PublicationOutput(
|
||||
id=publication.id,
|
||||
return dto.PlacementPostOutput(
|
||||
id=placement_post.id,
|
||||
project_id=placement.project_id,
|
||||
project_channel_title=placement.project.channel.title if placement.project.channel else None,
|
||||
placement_channel_id=placement.channel_id,
|
||||
placement_channel_title=placement.channel.title,
|
||||
creative_id=placement.creative_id,
|
||||
creative_name=placement.creative.name,
|
||||
placement_date=placement.placement_at or publication.created_at,
|
||||
placement_date=placement.placement_at or placement_post.created_at,
|
||||
cost=placement.cost_value,
|
||||
comment=placement.comment,
|
||||
ad_post_url=ad_post_url,
|
||||
invite_link_type=placement.invite_link_type,
|
||||
invite_link=placement.invite_link,
|
||||
status=publication.status,
|
||||
status=placement_post.status,
|
||||
subscriptions_count=subscriptions_count,
|
||||
placement_id=placement.id,
|
||||
created_at=publication.created_at,
|
||||
created_at=placement_post.created_at,
|
||||
)
|
||||
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_publications(self: 'Usecase', input: dto.GetPublicationsInput) -> dto.GetPublicationsOutput:
|
||||
async def get_placement_posts(self: 'Usecase', input: dto.GetPlacementPostsInput) -> dto.GetPlacementPostsOutput:
|
||||
"""Получить список публикаций (system-managed posts)"""
|
||||
context = await self.ensure_workspace_permission(
|
||||
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
|
||||
@@ -21,7 +21,7 @@ async def get_publications(self: 'Usecase', input: dto.GetPublicationsInput) ->
|
||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, input.project_id)
|
||||
allowed_project_ids = None
|
||||
|
||||
publications = await self.database.get_workspace_publications(
|
||||
placement_posts = await self.database.get_workspace_placement_posts(
|
||||
input.workspace_id,
|
||||
project_id=input.project_id,
|
||||
placement_channel_id=input.placement_channel_id,
|
||||
@@ -30,42 +30,42 @@ async def get_publications(self: 'Usecase', input: dto.GetPublicationsInput) ->
|
||||
allowed_project_ids=allowed_project_ids,
|
||||
)
|
||||
|
||||
publication_ids = [p.id for p in publications]
|
||||
subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(publication_ids)
|
||||
placement_post_ids = [p.id for p in placement_posts]
|
||||
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids)
|
||||
|
||||
publication_outputs = []
|
||||
for publication in publications:
|
||||
placement = publication.placement
|
||||
placement_post_outputs = []
|
||||
for placement_post in placement_posts:
|
||||
placement = placement_post.placement
|
||||
if not placement:
|
||||
log.warning('Publication %s missing placement data', publication.id)
|
||||
log.warning('PlacementPost %s missing placement data', placement_post.id)
|
||||
continue
|
||||
|
||||
if not placement.channel or not placement.project or not placement.creative:
|
||||
log.warning('Publication %s placement missing related data', publication.id)
|
||||
log.warning('PlacementPost %s placement missing related data', placement_post.id)
|
||||
continue
|
||||
|
||||
ad_post_url = publication.post.url if publication.post else None
|
||||
ad_post_url = placement_post.post.url if placement_post.post else None
|
||||
|
||||
publication_outputs.append(
|
||||
dto.PublicationOutput(
|
||||
id=publication.id,
|
||||
placement_post_outputs.append(
|
||||
dto.PlacementPostOutput(
|
||||
id=placement_post.id,
|
||||
project_id=placement.project_id,
|
||||
project_channel_title=placement.project.channel.title if placement.project.channel else None,
|
||||
placement_channel_id=placement.channel_id,
|
||||
placement_channel_title=placement.channel.title,
|
||||
creative_id=placement.creative_id,
|
||||
creative_name=placement.creative.name,
|
||||
placement_date=placement.placement_at or publication.created_at,
|
||||
placement_date=placement.placement_at or placement_post.created_at,
|
||||
cost=placement.cost_value,
|
||||
comment=placement.comment,
|
||||
ad_post_url=ad_post_url,
|
||||
invite_link_type=placement.invite_link_type,
|
||||
invite_link=placement.invite_link,
|
||||
status=publication.status,
|
||||
subscriptions_count=subscriptions_counts.get(publication.id, 0),
|
||||
status=placement_post.status,
|
||||
subscriptions_count=subscriptions_counts.get(placement_post.id, 0),
|
||||
placement_id=placement.id,
|
||||
created_at=publication.created_at,
|
||||
created_at=placement_post.created_at,
|
||||
)
|
||||
)
|
||||
|
||||
return dto.GetPublicationsOutput(publications=publication_outputs)
|
||||
return dto.GetPlacementPostsOutput(placement_posts=placement_post_outputs)
|
||||
@@ -28,4 +28,3 @@ async def archive_project(self: 'Usecase', input: dto.ArchiveProjectInput) -> dt
|
||||
status=project.status,
|
||||
purchase_invite_type_default=project.purchase_invite_type_default,
|
||||
)
|
||||
|
||||
|
||||
@@ -14,4 +14,3 @@ async def delete_project(self: 'Usecase', workspace_id: uuid.UUID, project_id: u
|
||||
|
||||
async with self.database.transaction():
|
||||
await self.database.delete_project(workspace_id, project_id)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ async def update_project_invite_link_type(
|
||||
self: 'Usecase',
|
||||
workspace_id: uuid.UUID,
|
||||
project_id: uuid.UUID,
|
||||
purchase_invite_type_default: domain.purchase.InviteLinkType,
|
||||
purchase_invite_type_default: domain.InviteLinkType,
|
||||
user_id: uuid.UUID,
|
||||
) -> dto.ProjectOutput:
|
||||
await self.ensure_workspace_permission(
|
||||
|
||||
@@ -117,19 +117,15 @@ async def create_placements(
|
||||
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
|
||||
),
|
||||
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
|
||||
),
|
||||
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),
|
||||
)
|
||||
|
||||
@@ -17,13 +17,13 @@ async def handle_subscription(
|
||||
first_name: str | None = None,
|
||||
last_name: str | None = None,
|
||||
) -> None:
|
||||
publication = await self.database.get_publication_by_invite_link(invite_link)
|
||||
if not publication:
|
||||
log.warning('Publication not found for invite_link: %s', invite_link)
|
||||
placement_post = await self.database.get_placement_post_by_invite_link(invite_link)
|
||||
if not placement_post:
|
||||
log.warning('PlacementPost not found for invite_link: %s', invite_link)
|
||||
return
|
||||
|
||||
if not publication.placement:
|
||||
log.error('Publication %s has no placement', publication.id)
|
||||
if not placement_post.placement:
|
||||
log.error('PlacementPost %s has no placement', placement_post.id)
|
||||
return
|
||||
|
||||
subscriber = await self.database.get_telegram_user(telegram_id=user_telegram_id)
|
||||
@@ -42,7 +42,7 @@ async def handle_subscription(
|
||||
await self.database.update_telegram_user(subscriber)
|
||||
|
||||
active_subscription = await self.database.get_active_subscription_by_subscriber_and_project(
|
||||
subscriber.id, publication.placement.project_id
|
||||
subscriber.id, placement_post.placement.project_id
|
||||
)
|
||||
|
||||
if active_subscription:
|
||||
@@ -50,18 +50,20 @@ async def handle_subscription(
|
||||
# Это не должно случиться (Telegram не даст подписаться дважды),
|
||||
# но если случилось - логируем и игнорируем
|
||||
log.warning(
|
||||
'User %s (telegram_id: %s) already has active subscription to channel %s via publication %s, '
|
||||
'ignoring new subscription attempt via publication %s',
|
||||
'User %s (telegram_id: %s) already has active subscription to channel %s via placement_post %s, '
|
||||
'ignoring new subscription attempt via placement_post %s',
|
||||
subscriber.id,
|
||||
user_telegram_id,
|
||||
publication.placement.project_id,
|
||||
active_subscription.publication_id,
|
||||
publication.id,
|
||||
placement_post.placement.project_id,
|
||||
active_subscription.placement_post_id,
|
||||
placement_post.id,
|
||||
)
|
||||
return
|
||||
|
||||
# Проверяем, была ли раньше подписка через ЭТУ публикацию (для реактивации)
|
||||
existing_sub = await self.database.get_subscription_by_subscriber_and_publication(subscriber.id, publication.id)
|
||||
existing_sub = await self.database.get_subscription_by_subscriber_and_placement_post(
|
||||
subscriber.id, placement_post.id
|
||||
)
|
||||
|
||||
if existing_sub and existing_sub.status == domain.SubscriptionStatus.UNSUBSCRIBED:
|
||||
existing_sub.status = domain.SubscriptionStatus.ACTIVE
|
||||
@@ -69,24 +71,24 @@ async def handle_subscription(
|
||||
await self.database.update_subscription(existing_sub)
|
||||
|
||||
log.info(
|
||||
'Subscription reactivated: subscriber %s (telegram_id: %s) resubscribed via same publication %s',
|
||||
'Subscription reactivated: subscriber %s (telegram_id: %s) resubscribed via same placement_post %s',
|
||||
subscriber.id,
|
||||
user_telegram_id,
|
||||
publication.id,
|
||||
placement_post.id,
|
||||
)
|
||||
return
|
||||
|
||||
subscription = domain.Subscription(
|
||||
publication_id=publication.id,
|
||||
placement_post_id=placement_post.id,
|
||||
telegram_user_id=subscriber.id,
|
||||
invite_link=invite_link,
|
||||
)
|
||||
await self.database.create_subscription(subscription)
|
||||
|
||||
log.info(
|
||||
'Subscription created: subscriber %s (telegram_id: %s) subscribed via publication %s (invite_link: %s)',
|
||||
'Subscription created: subscriber %s (telegram_id: %s) subscribed via placement_post %s (invite_link: %s)',
|
||||
subscriber.id,
|
||||
user_telegram_id,
|
||||
publication.id,
|
||||
placement_post.id,
|
||||
invite_link,
|
||||
)
|
||||
|
||||
@@ -39,8 +39,8 @@ async def handle_unsubscription(self: 'Usecase', user_telegram_id: int, channel_
|
||||
await self.database.update_subscription(subscription)
|
||||
|
||||
log.info(
|
||||
'Subscription marked as unsubscribed: subscriber %s (telegram_id: %s) unsubscribed from publication %s',
|
||||
'Subscription marked as unsubscribed: subscriber %s (telegram_id: %s) unsubscribed from placement_post %s',
|
||||
subscriber.id,
|
||||
user_telegram_id,
|
||||
subscription.publication_id,
|
||||
subscription.placement_post_id,
|
||||
)
|
||||
|
||||
@@ -14,22 +14,22 @@ async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) ->
|
||||
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
|
||||
)
|
||||
|
||||
publication = await self.database.get_publication(input.workspace_id, input.publication_id)
|
||||
if not publication:
|
||||
log.warning('Publication %s not found for user %s', input.publication_id, input.user_id)
|
||||
raise domain.PublicationNotFound(input.publication_id)
|
||||
placement_post = await self.database.get_placement_post(input.workspace_id, input.placement_post_id)
|
||||
if not placement_post:
|
||||
log.warning('PlacementPost %s not found for user %s', input.placement_post_id, input.user_id)
|
||||
raise domain.PlacementPostNotFound(input.placement_post_id)
|
||||
|
||||
if not publication.placement:
|
||||
log.error('Publication %s has no placement', publication.id)
|
||||
raise domain.PlacementNotFound(publication.placement_id)
|
||||
if not placement_post.placement:
|
||||
log.error('PlacementPost %s has no placement', placement_post.id)
|
||||
raise domain.PlacementNotFound(placement_post.placement_id)
|
||||
|
||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, publication.placement.project_id)
|
||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement_post.placement.project_id)
|
||||
|
||||
if not publication.post:
|
||||
if not placement_post.post:
|
||||
return []
|
||||
|
||||
histories = await self.database.get_views_history(
|
||||
publication.post.id,
|
||||
placement_post.post.id,
|
||||
from_date=input.from_date,
|
||||
to_date=input.to_date,
|
||||
)
|
||||
|
||||
@@ -33,4 +33,4 @@ async def accept_workspace_invite(
|
||||
else:
|
||||
await self.database.add_user_to_workspace(invite.workspace_id, user_id)
|
||||
|
||||
return dto.WorkspaceInviteOutput.from_domain(invite)
|
||||
return dto.WorkspaceInviteOutput.from_domain(invite)
|
||||
|
||||
Reference in New Issue
Block a user