инвайт линк и подсчет отписок

This commit is contained in:
Artem Tsyrulnikov
2026-01-28 12:58:00 +03:00
parent 632fa4b971
commit e87958fb60
7 changed files with 147 additions and 0 deletions

View File

@@ -733,6 +733,47 @@ class Postgres(DatabaseBase):
counts = {row['placement_id']: row['count'] for row in results}
return {pid: counts.get(pid, 0) for pid in placement_ids}
@staticmethod
async def count_unsubscriptions_by_placement_post_batch(
placement_post_ids: list[uuid.UUID],
) -> dict[uuid.UUID, int]:
"""Count unsubscriptions for placement_posts by finding their parent placements."""
if not placement_post_ids:
return {}
from tortoise.functions import Count
# Get placement_ids from placement_posts
placement_posts = await domain.PlacementPost.filter(id__in=placement_post_ids).all()
placement_id_to_post_ids: dict[uuid.UUID, list[uuid.UUID]] = {}
for pp in placement_posts:
placement_id_to_post_ids.setdefault(pp.placement_id, []).append(pp.id)
placement_ids = list(placement_id_to_post_ids.keys())
if not placement_ids:
return dict.fromkeys(placement_post_ids, 0)
# Count unsubscriptions by placement (filter by UNSUBSCRIBED status)
results = (
await domain.Subscription.filter(
placement_id__in=placement_ids, status=domain.SubscriptionStatus.UNSUBSCRIBED
)
.group_by('placement_id')
.annotate(count=Count('id'))
.values('placement_id', 'count')
)
placement_counts = {row['placement_id']: row['count'] for row in results}
# Map back to placement_post_ids
post_counts: dict[uuid.UUID, int] = {}
for placement_id, post_ids in placement_id_to_post_ids.items():
count = placement_counts.get(placement_id, 0)
for post_id in post_ids:
post_counts[post_id] = count
return {pid: post_counts.get(pid, 0) for pid in placement_post_ids}
@staticmethod
async def has_placement_posts_for_creative(creative_id: uuid.UUID) -> bool:
return await domain.PlacementPost.filter(placement__creative_id=creative_id).exists()

View File

@@ -54,6 +54,7 @@ class Placement(TimestampedModel):
comment = fields.TextField(null=True)
invite_link = fields.CharField(max_length=512, null=True)
invite_link_created_at = fields.DatetimeField(null=True)
invite_link_type = fields.CharEnumField(InviteLinkType)
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(

View File

@@ -47,6 +47,7 @@ class PlacementAnalyticsOutput(pydantic.BaseModel):
placement_date: datetime.datetime
cost: float | None
subscriptions_count: int
unsubscriptions_count: int
views_count: int | None
cpf: float | None
cpm: float | None

View File

@@ -30,6 +30,7 @@ class PlacementOutput(pydantic.BaseModel):
creative_id: uuid.UUID | None = None
comment: str | None = None
invite_link: str | None
invite_link_created_at: datetime.datetime | None = None
invite_link_type: InviteLinkType
channel: ChannelOutput
details: PlacementDetails | None = None

View File

@@ -72,6 +72,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_placement_post_batch(placement_ids)
unsubscriptions_counts = await self.database.count_unsubscriptions_by_placement_post_batch(placement_ids)
results: list[dto.PlacementAnalyticsOutput] = []
for placement_post in placements:
@@ -92,6 +93,7 @@ async def get_placements_analytics(
cost = _get_cost(placement_post)
subs_count = 0 if hide_subscriptions else subscriptions_counts.get(placement_post.id, 0)
unsubs_count = 0 if hide_subscriptions else unsubscriptions_counts.get(placement_post.id, 0)
if placement_post.post and placement_post.post.id in views_map:
views_count = views_map[placement_post.post.id][0]
else:
@@ -109,6 +111,7 @@ async def get_placements_analytics(
placement_date=_get_placement_date(placement_post),
cost=cost,
subscriptions_count=subs_count,
unsubscriptions_count=unsubs_count,
views_count=views_count,
cpf=cpf,
cpm=_calculate_cpm(cost, views_count),

View File

@@ -4,6 +4,7 @@ import uuid
from typing import TYPE_CHECKING
from aiogram.types import InlineKeyboardButton
from tortoise import timezone
from src import domain, dto
@@ -87,6 +88,7 @@ async def build_placement_creative(
requires_approval = placement.invite_link_type == domain.InviteLinkType.APPROVAL
invite_link = await self.telegram_bot.create_chat_invite_link(project.channel.telegram_id, requires_approval)
placement.invite_link = invite_link
placement.invite_link_created_at = timezone.now()
await self.database.update_placement(placement)
invite_link = placement.invite_link