feat: переименование доменной области

This commit is contained in:
Artem Tsyrulnikov
2025-12-14 12:21:35 +03:00
parent 3cb3da4dbe
commit f3e09a08eb
75 changed files with 1624 additions and 1417 deletions

View File

@@ -8,14 +8,15 @@ if typing.TYPE_CHECKING:
from src import domain
from .analytics.get_channel_analytics import get_channel_analytics
from .analytics.get_creatives_analytics import get_creatives_analytics
from .analytics.get_external_channels_analytics import get_external_channels_analytics
from .analytics.get_placements_analytics import get_placements_analytics
from .analytics.get_spending_analytics import get_spending_analytics
from .auth.get_me import get_me
from .auth.telegram_login import telegram_login
from .auth.telegram_start import telegram_start
from .auth.validate_login_token import validate_login_token
from .channel.get_channels import get_channels
from .creative.create_creative import create_creative
from .creative.delete_creative import delete_creative
from .creative.get_creative import get_creative
@@ -25,23 +26,22 @@ from .creative.tg_create_creative_2 import tg_create_creative_2
from .creative.tg_create_creative_3 import tg_create_creative_3
from .creative.tg_create_creative_4 import tg_create_creative_4
from .creative.update_creative import update_creative
from .external_channel.create_external_channel import create_external_channel
from .external_channel.delete_external_channel import delete_external_channel
from .external_channel.get_external_channels import get_external_channels
from .external_channel.update_external_channel import update_external_channel
from .external_channel.update_external_channel_links import update_external_channel_links
from .placement.create_placement import create_placement
from .placement.delete_placement import delete_placement
from .placement.get_placement import get_placement
from .placement.get_placements import get_placements
from .placement.update_placement import update_placement
from .project.disconnect_project_by_tg_id import disconnect_project_by_tg_id
from .project.get_workspace_projects import get_workspace_projects
from .project.tg_add_project_1 import tg_add_project_1
from .project.tg_add_project_2 import tg_add_project_2
from .project.update_project_permissions import update_project_permissions
from .purchase_plan.attach_channel_to_purchase_plan import attach_channel_to_purchase_plan
from .purchase_plan.get_purchase_plan_channels import get_purchase_plan_channels
from .purchase_plan.remove_purchase_plan_channel import remove_purchase_plan_channel
from .purchase_plan.update_purchase_plan_channel import update_purchase_plan_channel
from .subscription.handle_subscription import handle_subscription
from .subscription.handle_unsubscription import handle_unsubscription
from .target_channel.disconnect_target_chan_by_tg_id import disconnect_target_chan_by_tg_id
from .target_channel.get_user_target_chans import get_user_target_chans
from .target_channel.tg_add_target_chan_1 import tg_add_target_chan_1
from .target_channel.tg_add_target_chan_2 import tg_add_target_chan_2
from .target_channel.update_target_chan_permissions import update_target_chan_permissions
from .views.get_views_history import get_views_history
from .workspace.create_workspace import create_workspace
from .workspace.delete_workspace import delete_workspace
@@ -82,46 +82,74 @@ class Database(typing.Protocol):
async def mark_token_as_used(self, token: str) -> None: ...
async def get_target_channel(
self, workspace_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
) -> domain.TargetChannel | None: ...
async def update_user(self, user: domain.User) -> None: ...
async def get_target_channel_for_user_by_telegram(
self, user_id: UUID, telegram_id: int
) -> domain.TargetChannel | None: ...
async def get_channel(
self,
workspace_id: UUID,
channel_id: UUID | None = None,
telegram_id: int | None = None,
username: str | None = None,
) -> domain.Channel | None: ...
async def upsert_target_channel(self, channel: domain.TargetChannel) -> None: ...
async def get_workspace_channels(self, workspace_id: UUID) -> list[domain.Channel]: ...
async def get_workspace_target_channels(self, workspace_id: UUID) -> list[domain.TargetChannel]: ...
async def create_channel(self, channel: domain.Channel) -> None: ...
async def update_target_channel(self, channel: domain.TargetChannel) -> None: ...
async def update_channel(self, channel: domain.Channel) -> None: ...
async def get_external_channel(
self, workspace_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
) -> domain.ExternalChannel | None: ...
async def delete_channel(self, channel_id: UUID) -> None: ...
async def create_external_channel(self, channel: domain.ExternalChannel) -> None: ...
async def search_channels(self, username_query: str | None = None) -> list[domain.Channel]: ...
async def get_external_channels_for_target(self, target_channel_id: UUID) -> list[domain.ExternalChannel]: ...
async def get_project(
self, workspace_id: UUID, project_id: UUID | None = None, channel_id: UUID | None = None
) -> domain.Project | None: ...
async def get_workspace_external_channels(self, workspace_id: UUID) -> list[domain.ExternalChannel]: ...
async def get_project_for_user_by_telegram(
self, user_id: UUID, channel_telegram_id: int
) -> domain.Project | None: ...
async def add_external_channel_to_targets(
self, external_channel: domain.ExternalChannel, target_channel_ids: list[UUID]
) -> None: ...
async def get_project_by_channel_telegram(self, channel_telegram_id: int) -> domain.Project | None: ...
async def remove_external_channel_from_target(self, external_channel_id: UUID, target_channel_id: UUID) -> None: ...
async def create_project(self, project: domain.Project) -> None: ...
async def delete_external_channel(self, channel_id: UUID) -> None: ...
async def update_project(self, project: domain.Project) -> None: ...
async def update_external_channel(self, channel: domain.ExternalChannel) -> None: ...
async def get_workspace_projects(self, workspace_id: UUID) -> list[domain.Project]: ...
async def get_active_purchase_plan(self, project_id: UUID) -> domain.PurchasePlan | None: ...
async def get_purchase_plan(self, workspace_id: UUID, plan_id: UUID) -> domain.PurchasePlan | None: ...
async def create_purchase_plan(self, plan: domain.PurchasePlan) -> None: ...
async def get_purchase_plan_channels(self, plan_id: UUID) -> list[domain.PurchasePlanChannel]: ...
async def get_purchase_plan_channel(
self, plan_channel_id: UUID, plan_id: UUID
) -> domain.PurchasePlanChannel | None: ...
async def add_channel_to_purchase_plan(
self,
plan_id: UUID,
channel_id: UUID,
*,
status: domain.PurchasePlanChannelStatus | None = None,
planned_cost: float | None = None,
comment: str | None = None,
) -> domain.PurchasePlanChannel: ...
async def update_purchase_plan_channel(self, plan_channel: domain.PurchasePlanChannel) -> None: ...
async def remove_channel_from_purchase_plan(self, plan_id: UUID, channel_id: UUID) -> None: ...
async def get_creative(self, workspace_id: UUID, creative_id: UUID) -> domain.Creative | None: ...
async def update_creative(self, creative: domain.Creative) -> None: ...
async def get_workspace_creatives(
self, workspace_id: UUID, target_channel_id: UUID | None = None, include_archived: bool = False
self, workspace_id: UUID, project_id: UUID | None = None, include_archived: bool = False
) -> list[domain.Creative]: ...
async def delete_creative(self, creative_id: UUID) -> None: ...
@@ -135,8 +163,8 @@ class Database(typing.Protocol):
async def get_workspace_placements(
self,
workspace_id: UUID,
target_channel_id: UUID | None = None,
external_channel_id: UUID | None = None,
project_id: UUID | None = None,
placement_channel_id: UUID | None = None,
creative_id: UUID | None = None,
include_archived: bool = False,
) -> list[domain.Placement]: ...
@@ -145,10 +173,6 @@ class Database(typing.Protocol):
async def get_placement_by_invite_link(self, invite_link: str) -> domain.Placement | None: ...
async def get_subscriber(self, telegram_id: int) -> domain.Subscriber | None: ...
async def upsert_subscriber(self, subscriber: domain.Subscriber) -> None: ...
async def create_subscription(self, subscription: domain.Subscription) -> None: ...
async def get_subscription_by_subscriber_and_placement(
@@ -157,12 +181,12 @@ class Database(typing.Protocol):
async def update_subscription(self, subscription: domain.Subscription) -> None: ...
async def get_active_subscriptions_by_subscriber_and_channel(
self, subscriber_id: UUID, channel_telegram_id: int
async def get_active_subscriptions_by_subscriber_and_project(
self, subscriber_id: UUID, project_id: UUID
) -> list[domain.Subscription]: ...
async def get_active_subscription_by_subscriber_and_channel(
self, subscriber_id: UUID, channel_telegram_id: int
async def get_active_subscription_by_subscriber_and_project(
self, subscriber_id: UUID, project_id: UUID
) -> domain.Subscription | None: ...
async def create_placement_views_history(self, history: domain.PlacementViewsHistory) -> None: ...
@@ -229,20 +253,29 @@ class Usecase:
return workspace
async def ensure_active_purchase_plan(self, project: domain.Project) -> domain.PurchasePlan:
plan = await self.database.get_active_purchase_plan(project.id)
if plan:
return plan
plan = domain.PurchasePlan(workspace_id=project.workspace_id, project_id=project.id)
await self.database.create_purchase_plan(plan)
return plan
validate_login_token = validate_login_token
telegram_login = telegram_login
telegram_start = telegram_start
get_me = get_me
tg_add_target_chan_1 = tg_add_target_chan_1
tg_add_target_chan_2 = tg_add_target_chan_2
get_user_target_chans = get_user_target_chans
disconnect_target_chan_by_tg_id = disconnect_target_chan_by_tg_id
update_target_chan_permissions = update_target_chan_permissions
get_external_channels = get_external_channels
create_external_channel = create_external_channel
delete_external_channel = delete_external_channel
update_external_channel = update_external_channel
update_external_channel_links = update_external_channel_links
tg_add_project_1 = tg_add_project_1
tg_add_project_2 = tg_add_project_2
get_workspace_projects = get_workspace_projects
disconnect_project_by_tg_id = disconnect_project_by_tg_id
update_project_permissions = update_project_permissions
get_channels = get_channels
attach_channel_to_purchase_plan = attach_channel_to_purchase_plan
get_purchase_plan_channels = get_purchase_plan_channels
update_purchase_plan_channel = update_purchase_plan_channel
remove_purchase_plan_channel = remove_purchase_plan_channel
get_creatives = get_creatives
get_creative = get_creative
create_creative = create_creative
@@ -262,7 +295,7 @@ class Usecase:
get_views_history = get_views_history
get_placements_analytics = get_placements_analytics
get_creatives_analytics = get_creatives_analytics
get_external_channels_analytics = get_external_channels_analytics
get_channel_analytics = get_channel_analytics
get_spending_analytics = get_spending_analytics
get_workspaces = get_workspaces
create_workspace = create_workspace

View File

@@ -1,11 +0,0 @@
from .get_creatives_analytics import get_creatives_analytics
from .get_external_channels_analytics import get_external_channels_analytics
from .get_placements_analytics import get_placements_analytics
from .get_spending_analytics import get_spending_analytics
__all__ = (
'get_placements_analytics',
'get_creatives_analytics',
'get_external_channels_analytics',
'get_spending_analytics',
)

View File

@@ -11,21 +11,21 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__)
async def get_external_channels_analytics(
self: 'Usecase', input: dto.GetExternalChannelsAnalyticsInput
) -> dto.GetExternalChannelsAnalyticsOutput:
async def get_channel_analytics(self: 'Usecase', input: dto.GetChannelAnalyticsInput) -> dto.GetChannelAnalyticsOutput:
await self.ensure_workspace_access(input.workspace_id, input.user_id)
if input.target_channel_id:
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
if not target_channel:
raise domain.TargetChannelNotFound(input.target_channel_id)
external_channels = await self.database.get_external_channels_for_target(input.target_channel_id)
if input.project_id:
project = await self.database.get_project(input.workspace_id, input.project_id)
if not project:
raise domain.ProjectNotFound(input.project_id)
plan = await self.ensure_active_purchase_plan(project)
plan_channels = await self.database.get_purchase_plan_channels(plan.id)
channels = [pc.channel for pc in plan_channels]
else:
external_channels = await self.database.get_workspace_external_channels(input.workspace_id)
channels = await self.database.get_workspace_channels(input.workspace_id)
placements = await self.database.get_workspace_placements(
input.workspace_id, input.target_channel_id, include_archived=False
input.workspace_id, input.project_id, include_archived=False
)
@dataclass
@@ -35,10 +35,10 @@ async def get_external_channels_analytics(
total_views: int = 0
placements_count: int = 0
channel_stats: dict[UUID, ChannelStats] = {ch.id: ChannelStats() for ch in external_channels}
channel_stats: dict[UUID, ChannelStats] = {ch.id: ChannelStats() for ch in channels}
for p in placements:
stats = channel_stats.get(p.external_channel_id)
stats = channel_stats.get(p.placement_channel_id)
if stats is None:
continue
@@ -48,7 +48,7 @@ async def get_external_channels_analytics(
stats.placements_count += 1
result = []
for ch in external_channels:
for ch in channels:
stats = channel_stats[ch.id]
avg_cpf = (
@@ -61,7 +61,7 @@ async def get_external_channels_analytics(
)
result.append(
dto.ExternalChannelAnalyticsOutput(
dto.ChannelAnalyticsOutput(
id=ch.id,
title=ch.title,
username=ch.username,
@@ -74,4 +74,4 @@ async def get_external_channels_analytics(
)
)
return dto.GetExternalChannelsAnalyticsOutput(external_channels=result)
return dto.GetChannelAnalyticsOutput(channels=result)

View File

@@ -16,16 +16,16 @@ async def get_creatives_analytics(
) -> dto.GetCreativesAnalyticsOutput:
await self.ensure_workspace_access(input.workspace_id, input.user_id)
if input.target_channel_id:
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
if not target_channel:
raise domain.TargetChannelNotFound(input.target_channel_id)
if input.project_id:
project = await self.database.get_project(input.workspace_id, input.project_id)
if not project:
raise domain.ProjectNotFound(input.project_id)
creatives = await self.database.get_workspace_creatives(
input.workspace_id, input.target_channel_id, include_archived=False
input.workspace_id, input.project_id, include_archived=False
)
placements = await self.database.get_workspace_placements(
input.workspace_id, input.target_channel_id, include_archived=False
input.workspace_id, input.project_id, include_archived=False
)
@dataclass

View File

@@ -26,22 +26,22 @@ async def get_placements_analytics(
) -> dto.GetPlacementsAnalyticsOutput:
await self.ensure_workspace_access(input.workspace_id, input.user_id)
if input.target_channel_id:
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
if not target_channel:
raise domain.TargetChannelNotFound(input.target_channel_id)
if input.project_id:
project = await self.database.get_project(input.workspace_id, input.project_id)
if not project:
raise domain.ProjectNotFound(input.project_id)
placements = await self.database.get_workspace_placements(
input.workspace_id, input.target_channel_id, include_archived=False
input.workspace_id, input.project_id, include_archived=False
)
result = [
dto.PlacementAnalyticsOutput(
id=p.id,
target_channel_id=p.target_channel_id,
target_channel_title=p.target_channel.title,
external_channel_id=p.external_channel_id,
external_channel_title=p.external_channel.title,
project_id=p.project_id,
project_channel_title=p.project.channel.title,
placement_channel_id=p.placement_channel_id,
placement_channel_title=p.placement_channel.title,
creative_id=p.creative_id,
creative_name=p.creative.name,
placement_date=p.placement_date,

View File

@@ -35,13 +35,13 @@ async def get_spending_analytics(
) -> dto.GetSpendingAnalyticsOutput:
await self.ensure_workspace_access(input.workspace_id, input.user_id)
if input.target_channel_id:
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
if not target_channel:
raise domain.TargetChannelNotFound(input.target_channel_id)
if input.project_id:
project = await self.database.get_project(input.workspace_id, input.project_id)
if not project:
raise domain.ProjectNotFound(input.project_id)
placements = await self.database.get_workspace_placements(
input.workspace_id, input.target_channel_id, include_archived=False
input.workspace_id, input.project_id, include_archived=False
)
filtered = [

View File

@@ -0,0 +1,28 @@
import logging
from typing import TYPE_CHECKING
from src import dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def get_channels(self: 'Usecase', input: dto.GetChannelsInput) -> dto.GetChannelsOutput:
channels = await self.database.search_channels(username_query=input.username)
log.debug('Found %s channels for username query: %s', len(channels), input.username)
return dto.GetChannelsOutput(
channels=[
dto.ChannelOutput(
id=channel.id,
telegram_id=channel.telegram_id,
title=channel.title,
username=channel.username,
verification_status=channel.verification_status,
)
for channel in channels
]
)

View File

@@ -15,16 +15,16 @@ async def create_creative(
) -> dto.CreativeOutput:
await self.ensure_workspace_access(workspace_id, user_id)
target_channel = await self.database.get_target_channel(workspace_id, channel_id=input.target_channel_id)
if not target_channel:
log.warning('User %s attempted to create creative for unavailable target %s', user_id, input.target_channel_id)
raise domain.TargetChannelNotFound(input.target_channel_id)
project = await self.database.get_project(workspace_id, project_id=input.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)
creative = domain.Creative(
name=input.name,
text=input.text,
status=domain.CreativeStatus.ACTIVE,
target_channel_id=target_channel.id,
project_id=project.id,
workspace_id=workspace_id,
)
await self.database.create_creative(creative)
@@ -33,8 +33,8 @@ async def create_creative(
id=creative.id,
name=creative.name,
text=creative.text,
target_channel_id=target_channel.id,
target_channel_title=target_channel.title,
project_id=project.id,
project_channel_title=project.channel.title,
created_at=creative.created_at,
status=creative.status,
placements_count=creative.placements_count,

View File

@@ -20,8 +20,8 @@ async def get_creative(self: 'Usecase', input: dto.GetCreativeInput) -> dto.Crea
id=creative.id,
name=creative.name,
text=creative.text,
target_channel_id=creative.target_channel_id,
target_channel_title=creative.target_channel.title,
project_id=creative.project_id,
project_channel_title=creative.project.channel.title,
created_at=creative.created_at,
status=creative.status,
placements_count=creative.placements_count,

View File

@@ -10,7 +10,7 @@ async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> dto.Ge
await self.ensure_workspace_access(input.workspace_id, input.user_id)
creatives = await self.database.get_workspace_creatives(
input.workspace_id, input.target_channel_id, input.include_archived
input.workspace_id, input.project_id, input.include_archived
)
return dto.GetCreativesOutput(
@@ -19,8 +19,8 @@ async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> dto.Ge
id=creative.id,
name=creative.name,
text=creative.text,
target_channel_id=creative.target_channel_id,
target_channel_title=creative.target_channel.title,
project_id=creative.project_id,
project_channel_title=creative.project.channel.title,
created_at=creative.created_at,
status=creative.status,
placements_count=creative.placements_count,

View File

@@ -36,18 +36,23 @@ async def tg_create_creative_1(self: 'Usecase', telegram_id: int, chat_id: int)
await self.database.clear_telegram_state(telegram_id)
return
channels = await self.database.get_workspace_target_channels(workspace.id)
if not channels:
projects = await self.database.get_workspace_projects(workspace.id)
if not projects:
await self.telegram_bot.send_message(
'В выбранном рабочем пространстве нет подключенных каналов. Добавьте канал и попробуйте снова.',
'В выбранном рабочем пространстве нет подключенных проектов. Добавьте канал и попробуйте снова.',
chat_id,
)
await self.database.clear_telegram_state(telegram_id)
return
buttons = [
[InlineKeyboardButton(text=channel.title, callback_data=f'tg_create_creative_3:{channel.id}')]
for channel in channels
[
InlineKeyboardButton(
text=project.channel.title or project.channel.username or 'Unnamed Channel',
callback_data=f'tg_create_creative_3:{project.id}',
)
]
for project in projects
]
await self.telegram_bot.send_message_with_inline_keyboard(

View File

@@ -47,18 +47,23 @@ async def tg_create_creative_2(
await self.telegram_bot.edit_message_reply_markup(chat_id, message_id)
channels = await self.database.get_workspace_target_channels(workspace.id)
if not channels:
projects = await self.database.get_workspace_projects(workspace.id)
if not projects:
await self.telegram_bot.send_message(
'В выбранном рабочем пространстве нет подключенных каналов. Добавьте канал и попробуйте снова.',
'В выбранном рабочем пространстве нет подключенных проектов. Добавьте канал и попробуйте снова.',
chat_id,
)
await self.database.clear_telegram_state(telegram_id)
return
buttons = [
[InlineKeyboardButton(text=channel.title, callback_data=f'tg_create_creative_3:{channel.id}')]
for channel in channels
[
InlineKeyboardButton(
text=project.channel.title or project.channel.username or 'Unnamed Channel',
callback_data=f'tg_create_creative_3:{project.id}',
)
]
for project in projects
]
await self.telegram_bot.send_message_with_inline_keyboard(

View File

@@ -22,11 +22,11 @@ async def tg_create_creative_3(
log.warning('Invalid callback data format: %s', callback_data)
return
channel_id_str = callback_data.split(':', 1)[1]
project_id_str = callback_data.split(':', 1)[1]
try:
channel_id = uuid.UUID(channel_id_str)
project_id = uuid.UUID(project_id_str)
except ValueError:
log.error('Invalid channel UUID in callback data: %s', channel_id_str)
log.error('Invalid project UUID in callback data: %s', project_id_str)
return
user = await self.database.get_user(telegram_id=telegram_id)
@@ -53,13 +53,13 @@ async def tg_create_creative_3(
workspace_id = workspace.id
if not workspace_id:
log.error('Workspace ID missing for user %s when selecting channel %s', telegram_id, channel_id)
log.error('Workspace ID missing for user %s when selecting project %s', telegram_id, project_id)
await self.telegram_bot.send_message('❌ Ошибка: рабочее пространство не найдено. Начните заново.', chat_id)
await self.database.clear_telegram_state(telegram_id)
return
channel = await self.database.get_target_channel(workspace_id, channel_id=channel_id)
if not channel:
project = await self.database.get_project(workspace_id, project_id=project_id)
if not project:
await self.telegram_bot.send_message('❌ Канал не найден или не принадлежит вам.', chat_id)
await self.database.clear_telegram_state(telegram_id)
return
@@ -68,12 +68,14 @@ async def tg_create_creative_3(
telegram_id,
domain.TelegramStateEnum.CREATIVE_WAITING_NAME,
{
'target_channel_id': str(channel_id),
'channel_title': channel.title,
'project_id': str(project_id),
'channel_title': project.channel.title,
'workspace_id': str(workspace_id),
},
)
await self.telegram_bot.edit_message_reply_markup(chat_id, message_id)
await self.telegram_bot.send_message(f'✅ Канал выбран: {channel.title}\n\n📝 Введите название креатива:', chat_id)
await self.telegram_bot.send_message(
f'✅ Канал выбран: {project.channel.title}\n\n📝 Введите название креатива:', chat_id
)

View File

@@ -38,20 +38,20 @@ async def tg_create_creative_4(
)
elif state.state == domain.TelegramStateEnum.CREATIVE_WAITING_TEXT:
target_channel_id_str = state.context.get('target_channel_id')
project_id_str = state.context.get('project_id')
creative_name = state.context.get('creative_name')
workspace_id_str = state.context.get('workspace_id')
if not target_channel_id_str or not creative_name:
if not project_id_str or not creative_name:
log.error('Missing context data for creative creation: %s', state.context)
await self.telegram_bot.send_message('❌ Ошибка: данные не найдены. Начните заново.', chat_id)
await self.database.clear_telegram_state(telegram_id)
return
try:
target_channel_id = uuid.UUID(target_channel_id_str)
project_id = uuid.UUID(project_id_str)
except ValueError:
log.error('Invalid target_channel_id in context: %s', target_channel_id_str)
log.error('Invalid project_id in context: %s', project_id_str)
await self.telegram_bot.send_message('❌ Ошибка: неверный формат данных.', chat_id)
await self.database.clear_telegram_state(telegram_id)
return
@@ -78,8 +78,8 @@ async def tg_create_creative_4(
await self.database.clear_telegram_state(telegram_id)
return
channel = await self.database.get_target_channel(workspace_id, channel_id=target_channel_id)
if not channel:
project = await self.database.get_project(workspace_id, project_id=project_id)
if not project:
await self.telegram_bot.send_message('❌ Канал не найден.', chat_id)
await self.database.clear_telegram_state(telegram_id)
return
@@ -92,7 +92,7 @@ async def tg_create_creative_4(
text=creative_text,
status=domain.CreativeStatus.ACTIVE,
workspace_id=workspace_id,
target_channel_id=target_channel_id,
project_id=project_id,
)
async with self.database.transaction():
@@ -104,7 +104,7 @@ async def tg_create_creative_4(
await self.telegram_bot.send_message(
f'✅ Креатив успешно создан!\n\n'
f'📝 Название: {creative.name}\n'
f'📢 Канал: {channel.title}\n\n'
f'📢 Канал: {project.channel.title}\n\n'
f'💬 Текст:\n{text_preview}',
chat_id,
)

View File

@@ -37,8 +37,8 @@ async def update_creative(
id=creative.id,
name=creative.name,
text=creative.text,
target_channel_id=creative.target_channel_id,
target_channel_title=creative.target_channel.title,
project_id=creative.project_id,
project_channel_title=creative.project.channel.title,
created_at=creative.created_at,
status=creative.status,
placements_count=creative.placements_count,

View File

@@ -1,53 +0,0 @@
import logging
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def create_external_channel(
self: 'Usecase', input: dto.CreateExternalChannelInput, user_id: uuid.UUID, workspace_id: uuid.UUID
) -> dto.ExternalChannelOutput:
await self.ensure_workspace_access(workspace_id, user_id)
existing_channel = await self.database.get_external_channel(workspace_id, telegram_id=input.telegram_id)
if existing_channel:
raise domain.ExternalChannelAlreadyExists(input.telegram_id)
for target_id in input.target_channel_ids:
target_channel = await self.database.get_target_channel(workspace_id, channel_id=target_id)
if not target_channel:
raise domain.TargetChannelNotFound(target_id)
async with self.database.transaction():
channel = domain.ExternalChannel(
telegram_id=input.telegram_id,
title=input.title,
username=input.username,
description=input.description,
subscribers_count=input.subscribers_count,
workspace_id=workspace_id,
)
await self.database.create_external_channel(channel)
if input.target_channel_ids:
await self.database.add_external_channel_to_targets(channel, input.target_channel_ids)
log.info(
'External channel %s created and linked to %s target channels', input.telegram_id, len(input.target_channel_ids)
)
return dto.ExternalChannelOutput(
id=channel.id,
telegram_id=channel.telegram_id,
title=channel.title,
username=channel.username,
description=channel.description,
subscribers_count=channel.subscribers_count,
)

View File

@@ -1,20 +0,0 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def delete_external_channel(self: 'Usecase', input: dto.DeleteExternalChannelInput) -> None:
await self.ensure_workspace_access(input.workspace_id, input.user_id)
channel = await self.database.get_external_channel(input.workspace_id, channel_id=input.channel_id)
if not channel:
raise domain.ExternalChannelNotFound(input.channel_id)
await self.database.delete_external_channel(input.channel_id)
log.info('User %s deleted external channel %s', input.user_id, input.channel_id)

View File

@@ -1,34 +0,0 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def get_external_channels(self: 'Usecase', input: dto.GetExternalChannelsInput) -> dto.GetExternalChannelsOutput:
await self.ensure_workspace_access(input.workspace_id, input.user_id)
target_channel = await self.database.get_target_channel(input.workspace_id, channel_id=input.target_channel_id)
if not target_channel:
log.warning('Target channel %s not found or not accessible for user %s', input.target_channel_id, input.user_id)
raise domain.TargetChannelNotFound(input.target_channel_id)
channels = await self.database.get_external_channels_for_target(input.target_channel_id)
return dto.GetExternalChannelsOutput(
external_channels=[
dto.ExternalChannelOutput(
id=channel.id,
telegram_id=channel.telegram_id,
title=channel.title,
username=channel.username,
description=channel.description,
subscribers_count=channel.subscribers_count,
)
for channel in channels
]
)

View File

@@ -1,47 +0,0 @@
import logging
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def update_external_channel(
self: 'Usecase',
channel_id: uuid.UUID,
input: dto.UpdateExternalChannelInput,
user_id: uuid.UUID,
workspace_id: uuid.UUID,
) -> dto.ExternalChannelOutput:
await self.ensure_workspace_access(workspace_id, user_id)
external_channel = await self.database.get_external_channel(workspace_id, channel_id=channel_id)
if not external_channel:
log.warning('External channel %s not found', channel_id)
raise domain.ExternalChannelNotFound(channel_id)
if input.title is not None:
external_channel.title = input.title
if input.username is not None:
external_channel.username = input.username
if input.description is not None:
external_channel.description = input.description
if input.subscribers_count is not None:
external_channel.subscribers_count = input.subscribers_count
await self.database.update_external_channel(external_channel)
log.info('External channel %s updated', channel_id)
return dto.ExternalChannelOutput(
id=external_channel.id,
telegram_id=external_channel.telegram_id,
title=external_channel.title,
username=external_channel.username,
description=external_channel.description,
subscribers_count=external_channel.subscribers_count,
)

View File

@@ -1,54 +0,0 @@
import logging
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def update_external_channel_links(
self: 'Usecase',
channel_id: uuid.UUID,
input: dto.UpdateExternalChannelLinksInput,
user_id: uuid.UUID,
workspace_id: uuid.UUID,
) -> dto.ExternalChannelOutput:
await self.ensure_workspace_access(workspace_id, user_id)
external_channel = await self.database.get_external_channel(workspace_id, channel_id=channel_id)
if not external_channel:
log.warning('External channel %s not found', channel_id)
raise domain.ExternalChannelNotFound(channel_id)
for target_id in input.add_target_channel_ids:
target_channel = await self.database.get_target_channel(workspace_id, channel_id=target_id)
if not target_channel:
log.warning('User %s attempted to link external channel to target %s they do not own', user_id, target_id)
raise domain.TargetChannelNotFound(target_id)
async with self.database.transaction():
if input.add_target_channel_ids:
await self.database.add_external_channel_to_targets(external_channel, input.add_target_channel_ids)
for target_id in input.remove_target_channel_ids:
await self.database.remove_external_channel_from_target(channel_id, target_id)
log.info(
'External channel %s links updated: %s added, %s removed',
channel_id,
len(input.add_target_channel_ids),
len(input.remove_target_channel_ids),
)
return dto.ExternalChannelOutput(
id=external_channel.id,
telegram_id=external_channel.telegram_id,
title=external_channel.title,
username=external_channel.username,
description=external_channel.description,
subscribers_count=external_channel.subscribers_count,
)

View File

@@ -15,24 +15,30 @@ async def create_placement(
) -> dto.PlacementOutput:
await self.ensure_workspace_access(workspace_id, user_id)
target_channel = await self.database.get_target_channel(workspace_id, channel_id=input.target_channel_id)
if not target_channel:
raise domain.TargetChannelNotFound(input.target_channel_id)
project = await self.database.get_project(workspace_id, project_id=input.project_id)
if not project:
raise domain.ProjectNotFound(input.project_id)
external_channel = await self.database.get_external_channel(workspace_id, channel_id=input.external_channel_id)
if not external_channel:
raise domain.ExternalChannelNotFound(input.external_channel_id)
placement_channel = await self.database.get_channel(workspace_id, channel_id=input.placement_channel_id)
if not placement_channel:
raise domain.ChannelNotFound(input.placement_channel_id)
creative = await self.database.get_creative(workspace_id, input.creative_id)
if not creative:
raise domain.CreativeNotFound(input.creative_id)
if creative.project_id != project.id:
raise domain.CreativeNotFound(input.creative_id)
requires_approval = input.invite_link_type == domain.InviteLinkType.APPROVAL
invite_link = await self.telegram_bot.create_chat_invite_link(target_channel.telegram_id, requires_approval)
if project.channel.telegram_id is None:
raise domain.ChannelNotFound(project.channel.id)
invite_link = await self.telegram_bot.create_chat_invite_link(project.channel.telegram_id, requires_approval)
placement = domain.Placement(
target_channel_id=input.target_channel_id,
external_channel_id=input.external_channel_id,
project_id=input.project_id,
placement_channel_id=input.placement_channel_id,
creative_id=input.creative_id,
workspace_id=workspace_id,
placement_date=input.placement_date,
@@ -51,10 +57,10 @@ async def create_placement(
return dto.PlacementOutput(
id=placement.id,
target_channel_id=placement.target_channel_id,
target_channel_title=target_channel.title,
external_channel_id=placement.external_channel_id,
external_channel_title=external_channel.title,
project_id=placement.project_id,
project_channel_title=project.channel.title,
placement_channel_id=placement.placement_channel_id,
placement_channel_title=placement_channel.title,
creative_id=placement.creative_id,
creative_name=creative.name,
placement_date=placement.placement_date,

View File

@@ -19,10 +19,10 @@ async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.Pl
return dto.PlacementOutput(
id=placement.id,
target_channel_id=placement.target_channel_id,
target_channel_title=placement.target_channel.title,
external_channel_id=placement.external_channel_id,
external_channel_title=placement.external_channel.title,
project_id=placement.project_id,
project_channel_title=placement.project.channel.title,
placement_channel_id=placement.placement_channel_id,
placement_channel_title=placement.placement_channel.title,
creative_id=placement.creative_id,
creative_name=placement.creative.name,
placement_date=placement.placement_date,

View File

@@ -11,8 +11,8 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
placements = await self.database.get_workspace_placements(
input.workspace_id,
target_channel_id=input.target_channel_id,
external_channel_id=input.external_channel_id,
project_id=input.project_id,
placement_channel_id=input.placement_channel_id,
creative_id=input.creative_id,
include_archived=input.include_archived,
)
@@ -21,10 +21,10 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
placements=[
dto.PlacementOutput(
id=placement.id,
target_channel_id=placement.target_channel_id,
target_channel_title=placement.target_channel.title,
external_channel_id=placement.external_channel_id,
external_channel_title=placement.external_channel.title,
project_id=placement.project_id,
project_channel_title=placement.project.channel.title,
placement_channel_id=placement.placement_channel_id,
placement_channel_title=placement.placement_channel.title,
creative_id=placement.creative_id,
creative_name=placement.creative.name,
placement_date=placement.placement_date,

View File

@@ -37,10 +37,10 @@ async def update_placement(
return dto.PlacementOutput(
id=placement.id,
target_channel_id=placement.target_channel_id,
target_channel_title=placement.target_channel.title,
external_channel_id=placement.external_channel_id,
external_channel_title=placement.external_channel.title,
project_id=placement.project_id,
project_channel_title=placement.project.channel.title,
placement_channel_id=placement.placement_channel_id,
placement_channel_title=placement.placement_channel.title,
creative_id=placement.creative_id,
creative_name=placement.creative.name,
placement_date=placement.placement_date,

View File

@@ -0,0 +1,26 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def disconnect_project_by_tg_id(self: 'Usecase', input: dto.DisconnectProjectByTgIdInput) -> None:
user = await self.database.get_user(telegram_id=input.user_telegram_id)
if not user:
log.warning(f'User with telegram_id {input.user_telegram_id} not found when disconnecting channel')
return
project = await self.database.get_project_for_user_by_telegram(user.id, input.telegram_id)
if not project:
log.warning(f'Project channel {input.telegram_id} not found')
raise domain.ProjectNotFound()
project.status = domain.ProjectStatus.ARCHIVED
await self.database.update_project(project)
log.info('Project %s archived for channel %s', project.id, input.telegram_id)

View File

@@ -0,0 +1,27 @@
from typing import TYPE_CHECKING
from src import dto
if TYPE_CHECKING:
from .. import Usecase
async def get_workspace_projects(
self: 'Usecase', input: dto.GetWorkspaceProjectsInput
) -> dto.GetWorkspaceProjectsOutput:
await self.ensure_workspace_access(input.workspace_id, input.user_id)
projects = await self.database.get_workspace_projects(input.workspace_id)
return dto.GetWorkspaceProjectsOutput(
projects=[
dto.ProjectOutput(
id=project.id,
telegram_id=project.channel.telegram_id,
title=project.channel.title,
username=project.channel.username,
status=project.status,
)
for project in projects
]
)

View File

@@ -11,9 +11,7 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__)
async def tg_add_target_chan_1(
self: 'Usecase', input: dto.ConnectTargetChanInput
) -> dto.ConnectTargetChanOutput | None:
async def tg_add_project_1(self: 'Usecase', input: dto.ConnectProjectInput) -> dto.ProjectOutput | None:
permissions = input.bot_permissions
if not permissions.is_admin:
@@ -71,17 +69,37 @@ async def tg_add_target_chan_1(
if len(workspaces) == 1:
workspace = workspaces[0]
channel = domain.TargetChannel(
telegram_id=input.telegram_id,
title=input.title,
username=input.username,
workspace_id=workspace.id,
status=domain.TargetChannelStatus.ACTIVE,
)
await self.database.upsert_target_channel(channel)
channel = await self.database.get_channel(workspace.id, telegram_id=input.telegram_id)
if channel:
channel.title = input.title
channel.username = input.username
await self.database.update_channel(channel)
else:
channel = domain.Channel(
telegram_id=input.telegram_id,
title=input.title,
username=input.username,
workspace_id=workspace.id,
)
await self.database.create_channel(channel)
project = await self.database.get_project(workspace.id, channel_id=channel.id)
if project:
project.status = domain.ProjectStatus.ACTIVE
await self.database.update_project(project)
else:
project = domain.Project(
workspace_id=workspace.id,
channel_id=channel.id,
status=domain.ProjectStatus.ACTIVE,
)
await self.database.create_project(project)
await self.ensure_active_purchase_plan(project)
log.info(
'Channel %s connected/updated successfully in workspace %s by user %s',
'Project for channel %s connected/updated successfully in workspace %s by user %s',
input.telegram_id,
workspace.id,
input.user_telegram_id,
@@ -91,17 +109,16 @@ async def tg_add_target_chan_1(
f'✅ Канал "{input.title}" добавлен в рабочее пространство "{workspace.name}".', input.user_telegram_id
)
return dto.ConnectTargetChanOutput(
id=channel.id,
return dto.ProjectOutput(
id=project.id,
telegram_id=channel.telegram_id,
title=channel.title,
username=channel.username,
status=channel.status,
is_active=channel.status == domain.TargetChannelStatus.ACTIVE,
status=project.status,
)
current_state = await self.database.get_telegram_state(user.telegram_id)
if current_state and current_state.state == domain.TelegramStateEnum.TARGET_CHANNEL_WAITING_WORKSPACE:
if current_state and current_state.state == domain.TelegramStateEnum.PROJECT_WAITING_WORKSPACE:
await self.telegram_bot.send_message(
'⚠️ Сначала завершите выбор рабочего пространства для предыдущего канала.', user.telegram_id
)
@@ -122,20 +139,17 @@ async def tg_add_target_chan_1(
await self.database.set_telegram_state(
user.telegram_id,
domain.TelegramStateEnum.TARGET_CHANNEL_WAITING_WORKSPACE,
domain.TelegramStateEnum.PROJECT_WAITING_WORKSPACE,
context,
)
buttons = [
[InlineKeyboardButton(text=workspace.name, callback_data=f'tg_add_target_chan_2:{workspace.id}')]
[InlineKeyboardButton(text=workspace.name, callback_data=f'tg_add_project_2:{workspace.id}')]
for workspace in workspaces
]
await self.telegram_bot.send_message_with_inline_keyboard(
(
'✨ Подключение канала\n\n'
f'Выберите рабочее пространство, в которое добавить канал "{input.title}":'
),
(f'✨ Подключение канала\n\nВыберите рабочее пространство, в которое добавить канал "{input.title}":'),
user.telegram_id,
buttons,
)

View File

@@ -10,12 +10,12 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__)
async def tg_add_target_chan_2(
async def tg_add_project_2(
self: 'Usecase', telegram_id: int, chat_id: int, callback_data: str, message_id: int
) -> None:
state = await self.database.get_telegram_state(telegram_id)
if not state or state.state != domain.TelegramStateEnum.TARGET_CHANNEL_WAITING_WORKSPACE:
log.debug('User %s has no active target channel workspace selection flow', telegram_id)
if not state or state.state != domain.TelegramStateEnum.PROJECT_WAITING_WORKSPACE:
log.debug('User %s has no active project workspace selection flow', telegram_id)
return
context = state.context or {}
@@ -28,8 +28,8 @@ async def tg_add_target_chan_2(
except ValueError:
previous_state = None
if not callback_data.startswith('tg_add_target_chan_2:'):
log.warning('Invalid target channel workspace callback data: %s', callback_data)
if not callback_data.startswith('tg_add_project_2:'):
log.warning('Invalid project workspace callback data: %s', callback_data)
return
workspace_id_str = callback_data.split(':', 1)[1]
@@ -44,9 +44,9 @@ async def tg_add_target_chan_2(
await self.database.clear_telegram_state(telegram_id)
return
channel_data = state.context.get('channel_data') if state.context else None
channel_data = context.get('channel_data')
if not channel_data:
log.error('Missing channel data for user %s in target channel flow', telegram_id)
log.error('Missing channel data for user %s in project flow', telegram_id)
await self.telegram_bot.send_message('Не удалось подключить канал. Попробуйте снова.', chat_id)
if previous_state:
await self.database.set_telegram_state(telegram_id, previous_state, previous_context)
@@ -65,15 +65,31 @@ async def tg_add_target_chan_2(
await self.telegram_bot.send_message('У вас нет доступа к выбранному рабочему пространству.', chat_id)
return
channel = domain.TargetChannel(
telegram_id=int(channel_data['telegram_id']),
title=str(channel_data['title']),
username=channel_data.get('username'),
workspace_id=workspace.id,
status=domain.TargetChannelStatus.ACTIVE,
)
telegram_channel_id = int(channel_data['telegram_id'])
channel = await self.database.get_channel(workspace.id, telegram_id=telegram_channel_id)
if channel:
channel.title = str(channel_data['title'])
channel.username = channel_data.get('username')
await self.database.update_channel(channel)
else:
channel = domain.Channel(
telegram_id=telegram_channel_id,
title=str(channel_data['title']),
username=channel_data.get('username'),
workspace_id=workspace.id,
)
await self.database.create_channel(channel)
project = await self.database.get_project(workspace.id, channel_id=channel.id)
if project:
project.status = domain.ProjectStatus.ACTIVE
await self.database.update_project(project)
else:
project = domain.Project(workspace_id=workspace.id, channel_id=channel.id, status=domain.ProjectStatus.ACTIVE)
await self.database.create_project(project)
await self.ensure_active_purchase_plan(project)
await self.database.upsert_target_channel(channel)
log.info(
'Channel %s connected/updated successfully in workspace %s by user %s',
channel.telegram_id,

View File

@@ -9,7 +9,7 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__)
async def update_target_chan_permissions(self: 'Usecase', input: dto.UpdateTargetChanPermissionsInput) -> None:
async def update_project_permissions(self: 'Usecase', input: dto.UpdateProjectPermissionsInput) -> None:
missing_permissions = []
if not input.permissions.can_invite_users:
missing_permissions.append('Создание инвайт-ссылок')
@@ -22,31 +22,37 @@ async def update_target_chan_permissions(self: 'Usecase', input: dto.UpdateTarge
log.warning(f'User with telegram_id {input.user_telegram_id} not found when updating channel permissions')
return
channel = await self.database.get_target_channel_for_user_by_telegram(user.id, input.telegram_id)
if not channel:
log.warning(f'Target channel {input.telegram_id} not found when permissions changed')
raise domain.TargetChannelNotFound()
project = await self.database.get_project_for_user_by_telegram(user.id, input.telegram_id)
if not project:
log.warning(f'Project channel {input.telegram_id} not found when permissions changed')
raise domain.ProjectNotFound()
if not missing_permissions:
if channel.status != domain.TargetChannelStatus.ACTIVE:
channel.status = domain.TargetChannelStatus.ACTIVE
await self.database.update_target_channel(channel)
if project.status != domain.ProjectStatus.ACTIVE:
project.status = domain.ProjectStatus.ACTIVE
await self.database.update_project(project)
await self.telegram_bot.send_message(
f'✅ Канал "{input.chat_title}" был активирован.\n\nВсе необходимые права боту предоставлены!',
input.user_telegram_id,
)
log.info(f'Target channel {input.telegram_id} reactivated - all permissions granted')
log.info(f'Project channel {input.telegram_id} reactivated - all permissions granted')
return
if channel.status == domain.TargetChannelStatus.ACTIVE:
channel.status = domain.TargetChannelStatus.INACTIVE
await self.database.update_target_channel(channel)
if project.status == domain.ProjectStatus.ACTIVE:
project.status = domain.ProjectStatus.INACTIVE
await self.database.update_project(project)
missed_permissions = '\n'.join(f'{p}' for p in missing_permissions)
await self.telegram_bot.send_message(
f'⚠️ Канал "{input.chat_title}" был деактивирован.\
\n\оту убрали необходимые права:\n{missed_permissions}',
(
f'⚠️ Канал "{input.chat_title}" был деактивирован.\n\n'
f'Боту убрали необходимые права:\n{missed_permissions}'
),
input.user_telegram_id,
)
log.warning(f'Target channel {input.telegram_id} deactivated due to missing permissions: {missing_permissions}')
log.warning(
'Project channel %s deactivated due to missing permissions: %s',
input.telegram_id,
missing_permissions,
)

View File

@@ -0,0 +1,67 @@
import logging
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def attach_channel_to_purchase_plan(
self: 'Usecase',
project_id: uuid.UUID,
workspace_id: uuid.UUID,
user_id: uuid.UUID,
input: dto.AttachChannelToPurchasePlanInput,
) -> dto.PurchasePlanChannelOutput:
await self.ensure_workspace_access(workspace_id, user_id)
project = await self.database.get_project(workspace_id, project_id=project_id)
if not project:
raise domain.ProjectNotFound(project_id)
plan = await self.ensure_active_purchase_plan(project)
# Поиск канала по username
channel = await self.database.get_channel(workspace_id, username=input.username)
if not channel:
# Создать неподтвержденный канал
channel = domain.Channel(
workspace_id=workspace_id,
username=input.username,
telegram_id=None,
title=None,
verification_status=domain.ChannelVerificationStatus.UNVERIFIED,
)
await self.database.create_channel(channel)
log.info('Created unverified channel with username %s', input.username)
else:
log.info('Found existing channel %s for username %s', channel.id, input.username)
plan_channel = await self.database.add_channel_to_purchase_plan(
plan.id,
channel.id,
status=input.status,
planned_cost=input.planned_cost,
comment=input.comment,
)
log.info('Channel %s attached to purchase plan %s (project %s)', channel.id, plan.id, project.id)
return dto.PurchasePlanChannelOutput(
id=plan_channel.id,
status=plan_channel.status,
planned_cost=plan_channel.planned_cost,
comment=plan_channel.comment,
channel=dto.ChannelOutput(
id=channel.id,
telegram_id=channel.telegram_id,
title=channel.title,
username=channel.username,
verification_status=channel.verification_status,
),
)

View File

@@ -0,0 +1,43 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def get_purchase_plan_channels(
self: 'Usecase', input: dto.GetPurchasePlanChannelsInput
) -> dto.GetPurchasePlanChannelsOutput:
await self.ensure_workspace_access(input.workspace_id, input.user_id)
project = await self.database.get_project(input.workspace_id, project_id=input.project_id)
if not project:
raise domain.ProjectNotFound(input.project_id)
plan = await self.ensure_active_purchase_plan(project)
plan_channels = await self.database.get_purchase_plan_channels(plan.id)
log.debug('Fetched %s channels for purchase plan %s (project %s)', len(plan_channels), plan.id, project.id)
return dto.GetPurchasePlanChannelsOutput(
channels=[
dto.PurchasePlanChannelOutput(
id=plan_channel.id,
status=plan_channel.status,
planned_cost=plan_channel.planned_cost,
comment=plan_channel.comment,
channel=dto.ChannelOutput(
id=plan_channel.channel.id,
telegram_id=plan_channel.channel.telegram_id,
title=plan_channel.channel.title,
username=plan_channel.channel.username,
verification_status=plan_channel.channel.verification_status,
),
)
for plan_channel in plan_channels
]
)

View File

@@ -0,0 +1,33 @@
import logging
import uuid
from typing import TYPE_CHECKING
from src import domain
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def remove_purchase_plan_channel(
self: 'Usecase',
channel_id: uuid.UUID,
project_id: uuid.UUID,
workspace_id: uuid.UUID,
user_id: uuid.UUID,
) -> None:
await self.ensure_workspace_access(workspace_id, user_id)
project = await self.database.get_project(workspace_id, project_id=project_id)
if not project:
raise domain.ProjectNotFound(project_id)
plan = await self.ensure_active_purchase_plan(project)
plan_channel = await self.database.get_purchase_plan_channel(channel_id, plan.id)
if not plan_channel:
raise domain.PurchasePlanChannelNotFound(channel_id)
await self.database.remove_channel_from_purchase_plan(plan.id, plan_channel.channel_id)
log.info('Removed channel %s from purchase plan %s (project %s)', plan_channel.channel_id, plan.id, project.id)

View File

@@ -0,0 +1,71 @@
import logging
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def update_purchase_plan_channel(
self: 'Usecase',
channel_id: uuid.UUID,
project_id: uuid.UUID,
workspace_id: uuid.UUID,
user_id: uuid.UUID,
input: dto.UpdatePurchasePlanChannelInput,
) -> dto.PurchasePlanChannelOutput:
await self.ensure_workspace_access(workspace_id, user_id)
project = await self.database.get_project(workspace_id, project_id=project_id)
if not project:
raise domain.ProjectNotFound(project_id)
plan = await self.ensure_active_purchase_plan(project)
plan_channel = await self.database.get_purchase_plan_channel(channel_id, plan.id)
if not plan_channel:
raise domain.PurchasePlanChannelNotFound(channel_id)
updated_fields: dict[str, object | None] = {}
if input.status is not None:
plan_channel.status = input.status
updated_fields['status'] = input.status
if input.planned_cost is not None:
plan_channel.planned_cost = input.planned_cost
updated_fields['planned_cost'] = input.planned_cost
if input.comment is not None:
plan_channel.comment = input.comment
updated_fields['comment'] = input.comment
if updated_fields:
await self.database.update_purchase_plan_channel(plan_channel)
log.info(
'Updated purchase plan channel %s for project %s fields %s',
channel_id,
project.id,
', '.join(updated_fields.keys()),
)
channel: domain.Channel | None = plan_channel.channel
if channel is None:
channel = await self.database.get_channel(workspace_id, channel_id=plan_channel.channel_id)
if channel is None:
raise domain.ChannelNotFound(plan_channel.channel_id)
return dto.PurchasePlanChannelOutput(
id=plan_channel.id,
status=plan_channel.status,
planned_cost=plan_channel.planned_cost,
comment=plan_channel.comment,
channel=dto.ChannelOutput(
id=channel.id,
telegram_id=channel.telegram_id,
title=channel.title,
username=channel.username,
verification_status=channel.verification_status,
),
)

View File

@@ -22,16 +22,23 @@ async def handle_subscription(
log.warning('Placement not found for invite_link: %s', invite_link)
return
subscriber = domain.Subscriber(
telegram_id=user_telegram_id,
username=username,
first_name=first_name,
last_name=last_name,
)
await self.database.upsert_subscriber(subscriber)
subscriber = await self.database.get_user(telegram_id=user_telegram_id)
if not subscriber:
subscriber = domain.User(
telegram_id=user_telegram_id,
username=username,
first_name=first_name,
last_name=last_name,
)
await self.database.create_user(subscriber)
else:
subscriber.username = username or subscriber.username
subscriber.first_name = first_name or subscriber.first_name
subscriber.last_name = last_name or subscriber.last_name
await self.database.update_user(subscriber)
active_subscription = await self.database.get_active_subscription_by_subscriber_and_channel(
subscriber.id, placement.target_channel.telegram_id
active_subscription = await self.database.get_active_subscription_by_subscriber_and_project(
subscriber.id, placement.project_id
)
if active_subscription:
@@ -43,7 +50,7 @@ async def handle_subscription(
'ignoring new subscription attempt via placement %s',
subscriber.id,
user_telegram_id,
placement.target_channel_id,
placement.project_id,
active_subscription.placement_id,
placement.id,
)
@@ -72,7 +79,6 @@ async def handle_subscription(
subscription = domain.Subscription(
placement_id=placement.id,
subscriber_id=subscriber.id,
target_channel_id=placement.target_channel_id,
invite_link=invite_link,
)
async with self.database.transaction():

View File

@@ -12,14 +12,17 @@ log = logging.getLogger(__name__)
async def handle_unsubscription(self: 'Usecase', user_telegram_id: int, channel_telegram_id: int) -> None:
subscriber = await self.database.get_subscriber(user_telegram_id)
subscriber = await self.database.get_user(telegram_id=user_telegram_id)
if not subscriber:
log.warning('Subscriber not found for telegram_id: %s', user_telegram_id)
return
subscriptions = await self.database.get_active_subscriptions_by_subscriber_and_channel(
subscriber.id, channel_telegram_id
)
project = await self.database.get_project_by_channel_telegram(channel_telegram_id)
if not project:
log.warning('Project not found for channel telegram_id: %s', channel_telegram_id)
return
subscriptions = await self.database.get_active_subscriptions_by_subscriber_and_project(subscriber.id, project.id)
if not subscriptions:
log.info(

View File

@@ -1,26 +0,0 @@
import logging
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def disconnect_target_chan_by_tg_id(self: 'Usecase', input: dto.DisconnectTargetChanByTgIdInput) -> None:
user = await self.database.get_user(telegram_id=input.user_telegram_id)
if not user:
log.warning(f'User with telegram_id {input.user_telegram_id} not found when disconnecting channel')
return
channel = await self.database.get_target_channel_for_user_by_telegram(user.id, input.telegram_id)
if not channel:
log.warning(f'Target channel {input.telegram_id} not found')
raise domain.TargetChannelNotFound()
channel.status = domain.TargetChannelStatus.INACTIVE
await self.database.update_target_channel(channel)
log.info(f'Target channel {input.telegram_id} deactivated')

View File

@@ -1,26 +0,0 @@
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
async def get_user_target_chans(self: 'Usecase', input: dto.GetUserTargetChansInput) -> dto.GetUserTargetChansOutput:
await self.ensure_workspace_access(input.workspace_id, input.user_id)
channels = await self.database.get_workspace_target_channels(input.workspace_id)
return dto.GetUserTargetChansOutput(
target_channels=[
dto.ConnectTargetChanOutput(
id=channel.id,
telegram_id=channel.telegram_id,
title=channel.title,
username=channel.username,
status=channel.status,
is_active=channel.status == domain.TargetChannelStatus.ACTIVE,
)
for channel in channels
]
)