feat: проверка существования канала при привязке

This commit is contained in:
Artem Tsyrulnikov
2025-12-15 22:09:23 +03:00
parent 68edf11576
commit 83a81f46f7
22 changed files with 270 additions and 125 deletions

View File

@@ -279,11 +279,22 @@ class JWTEncoder(typing.Protocol):
def encode_access_token(self, user_id: UUID, telegram_id: int, username: str | None = None) -> str: ...
class FetchChannelResponse(typing.Protocol):
telegram_id: int
username: str
title: str
class ParserClient(typing.Protocol):
async def fetch_telegram_channel(self, username: str) -> FetchChannelResponse | None: ...
@dataclass
class Usecase:
database: Database
telegram_bot: TelegramBotWriter
jwt_encoder: JWTEncoder
parser_client: ParserClient
async def ensure_workspace_access(self, workspace_id: UUID, user_id: UUID) -> domain.Workspace:
workspace = await self.database.get_workspace_for_user(workspace_id, user_id)

View File

@@ -11,19 +11,19 @@ log = logging.getLogger(__name__)
async def create_creative(
self: 'Usecase', input: dto.CreateCreativeInput, user_id: uuid.UUID, workspace_id: uuid.UUID
self: 'Usecase', input: dto.CreateCreativeInput, project_id: uuid.UUID, user_id: uuid.UUID, workspace_id: uuid.UUID
) -> dto.CreativeOutput:
await self.ensure_workspace_permission(
workspace_id,
user_id,
domain.PermissionKey.PROJECTS_WRITE,
for_project_id=input.project_id,
for_project_id=project_id,
)
project = await self.database.get_project(workspace_id, project_id=input.project_id)
project = await self.database.get_project(workspace_id, project_id=project_id)
if not project:
log.warning('User %s attempted to create creative for unavailable project %s', user_id, input.project_id)
raise domain.ProjectNotFound(input.project_id)
log.warning('User %s attempted to create creative for unavailable project %s', user_id, project_id)
raise domain.ProjectNotFound(project_id)
creative = domain.Creative(
name=input.name,

View File

@@ -53,7 +53,7 @@ async def create_placement(
comment=input.comment,
invite_link_type=input.invite_link_type,
invite_link=invite_link,
status=domain.PlacementStatus.PENDING,
status=domain.PlacementStatus.ACTIVE,
)
async with self.database.transaction():
@@ -78,8 +78,5 @@ async def create_placement(
invite_link=placement.invite_link,
status=placement.status,
subscriptions_count=placement.subscriptions_count,
views_count=None,
views_availability=domain.PostViewsAvailability.UNKNOWN,
last_views_fetch_at=None,
created_at=placement.created_at,
)

View File

@@ -10,7 +10,7 @@ log = logging.getLogger(__name__)
async def fetch_placement_post_cycle(self: 'Usecase', interval_seconds: int) -> None:
log.info('Starting fetch_placement_post_cycle')
log.debug('Starting fetch_placement_post_cycle')
# Получаем все placement без привязанного поста
placements_without_post = (

View File

@@ -54,8 +54,5 @@ async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.Pl
invite_link=placement.invite_link,
status=placement.status,
subscriptions_count=placement.subscriptions_count,
views_count=views_count,
views_availability=views_availability,
last_views_fetch_at=last_views_fetch_at,
created_at=placement.created_at,
)

View File

@@ -28,25 +28,12 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
allowed_project_ids=allowed_project_ids,
)
# Batch fetch views data for all posts
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 {}
# Build output DTOs
placement_outputs = []
for placement in placements:
# Compute post-related fields
ad_post_url = None
views_count = None
views_availability = domain.PostViewsAvailability.UNKNOWN
last_views_fetch_at = None
if placement.post:
ad_post_url = generate_post_url(placement.post.channel.username, placement.post.message_id)
views_data = views_map.get(placement.post.id)
if views_data:
views_count, last_views_fetch_at = views_data
views_availability = domain.PostViewsAvailability.AVAILABLE
placement_outputs.append(
dto.PlacementOutput(
@@ -65,9 +52,6 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
invite_link=placement.invite_link,
status=placement.status,
subscriptions_count=placement.subscriptions_count,
views_count=views_count,
views_availability=views_availability,
last_views_fetch_at=last_views_fetch_at,
created_at=placement.created_at,
)
)

View File

@@ -39,21 +39,11 @@ async def update_placement(
await self.database.update_placement(placement)
# Compute post-related fields
ad_post_url = None
views_count = None
views_availability = domain.PostViewsAvailability.UNKNOWN
last_views_fetch_at = None
if placement.post:
ad_post_url = generate_post_url(placement.post.channel.username, placement.post.message_id)
# Get latest views data
views_data = await self.database.get_latest_views_data(placement.post.id)
if views_data:
views_count, last_views_fetch_at = views_data
views_availability = domain.PostViewsAvailability.AVAILABLE
return dto.PlacementOutput(
id=placement.id,
project_id=placement.project_id,
@@ -70,8 +60,5 @@ async def update_placement(
invite_link=placement.invite_link,
status=placement.status,
subscriptions_count=placement.subscriptions_count,
views_count=views_count,
views_availability=views_availability,
last_views_fetch_at=last_views_fetch_at,
created_at=placement.created_at,
)

View File

@@ -27,16 +27,20 @@ async def attach_channel_to_purchase_plan(
plan = await self.ensure_active_purchase_plan(project)
# Поиск канала по username
channel = await self.database.get_channel(username=input.username)
if not channel:
# Создать канал с nullable telegram_id/title (заполнятся парсером позже)
channel = domain.Channel(username=input.username)
parser_response = await self.parser_client.fetch_telegram_channel(input.username)
if not parser_response:
raise domain.TelegramChannelNotFound(input.username)
channel = domain.Channel(
username=input.username,
telegram_id=parser_response.telegram_id,
title=parser_response.title,
)
await self.database.create_channel(channel)
log.info('Created channel with username @%s (pending parser update)', input.username)
else:
log.info('Found existing channel %s for username @%s', channel.id, input.username)
log.info('Created channel @%s with telegram_id=%s', input.username, parser_response.telegram_id)
plan_channel = await self.database.add_channel_to_purchase_plan(
plan.id,