feat: переименование доменной области
This commit is contained in:
@@ -19,6 +19,9 @@ class Postgres(DatabaseBase):
|
||||
async def create_user(self, user: domain.User) -> None:
|
||||
await user.save()
|
||||
|
||||
async def update_user(self, user: domain.User) -> None:
|
||||
await user.save()
|
||||
|
||||
async def create_workspace(self, workspace: domain.Workspace) -> None:
|
||||
await workspace.save()
|
||||
|
||||
@@ -28,10 +31,12 @@ class Postgres(DatabaseBase):
|
||||
async def delete_workspace(self, workspace_id: uuid.UUID) -> None:
|
||||
await domain.Workspace.filter(id=workspace_id).delete()
|
||||
|
||||
async def add_user_to_workspace(
|
||||
self, workspace_id: uuid.UUID, user_id: uuid.UUID, *, is_owner: bool = False
|
||||
) -> None:
|
||||
await domain.WorkspaceUser.create(workspace_id=workspace_id, user_id=user_id, is_owner=is_owner)
|
||||
async def add_user_to_workspace(self, workspace_id: uuid.UUID, user_id: uuid.UUID) -> None:
|
||||
await domain.WorkspaceUser.create(
|
||||
workspace_id=workspace_id,
|
||||
user_id=user_id,
|
||||
status=domain.WorkspaceUserStatus.ACTIVE,
|
||||
)
|
||||
|
||||
async def get_user_workspaces(self, user_id: uuid.UUID) -> list[domain.WorkspaceUser]:
|
||||
return (
|
||||
@@ -71,9 +76,6 @@ class Postgres(DatabaseBase):
|
||||
async def create_login_token(self, login_token: domain.LoginToken) -> None:
|
||||
await login_token.save()
|
||||
|
||||
async def create_external_channel(self, channel: domain.ExternalChannel) -> None:
|
||||
await channel.save()
|
||||
|
||||
async def create_creative(self, creative: domain.Creative) -> None:
|
||||
await creative.save()
|
||||
|
||||
@@ -94,120 +96,165 @@ class Postgres(DatabaseBase):
|
||||
if updated == 0:
|
||||
raise domain.LoginTokenAlreadyUsed() # либо нет токена, либо он уже использован
|
||||
|
||||
async def get_target_channel(
|
||||
self, workspace_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None
|
||||
) -> domain.TargetChannel | None:
|
||||
if not channel_id and not telegram_id:
|
||||
raise ValueError('Either channel_id or telegram_id must be provided')
|
||||
|
||||
q = domain.TargetChannel.filter(workspace_id=workspace_id)
|
||||
if channel_id:
|
||||
q = q.filter(id=channel_id)
|
||||
if telegram_id:
|
||||
q = q.filter(telegram_id=telegram_id)
|
||||
|
||||
return await q.first()
|
||||
|
||||
async def get_target_channel_for_user_by_telegram(
|
||||
self, user_id: uuid.UUID, telegram_id: int
|
||||
) -> domain.TargetChannel | None:
|
||||
return (
|
||||
await domain.TargetChannel.filter(telegram_id=telegram_id, workspace__workspace_users__user_id=user_id)
|
||||
.prefetch_related('workspace')
|
||||
.first()
|
||||
)
|
||||
|
||||
async def upsert_target_channel(self, channel: domain.TargetChannel) -> None:
|
||||
existing = await self.get_target_channel(workspace_id=channel.workspace_id, telegram_id=channel.telegram_id)
|
||||
|
||||
if not existing:
|
||||
await channel.save()
|
||||
return
|
||||
|
||||
existing.title = channel.title
|
||||
existing.username = channel.username
|
||||
existing.workspace_id = channel.workspace_id
|
||||
existing.status = channel.status
|
||||
existing.deleted_at = None
|
||||
existing.updated_at = timezone.now()
|
||||
await existing.save()
|
||||
|
||||
async def get_workspace_target_channels(self, workspace_id: uuid.UUID) -> list[domain.TargetChannel]:
|
||||
return await domain.TargetChannel.filter(
|
||||
workspace_id=workspace_id, status=domain.TargetChannelStatus.ACTIVE
|
||||
).all()
|
||||
|
||||
async def update_target_channel(self, channel: domain.TargetChannel) -> None:
|
||||
await channel.save()
|
||||
|
||||
async def get_external_channel(
|
||||
self, workspace_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None
|
||||
) -> domain.ExternalChannel | None:
|
||||
query = domain.ExternalChannel.filter(workspace_id=workspace_id)
|
||||
async def get_channel(
|
||||
self,
|
||||
workspace_id: uuid.UUID,
|
||||
channel_id: uuid.UUID | None = None,
|
||||
telegram_id: int | None = None,
|
||||
username: str | None = None,
|
||||
) -> domain.Channel | None:
|
||||
query = domain.Channel.filter(workspace_id=workspace_id)
|
||||
if channel_id:
|
||||
query = query.filter(id=channel_id)
|
||||
if telegram_id:
|
||||
query = query.filter(telegram_id=telegram_id)
|
||||
|
||||
if username:
|
||||
query = query.filter(username=username)
|
||||
return await query.first()
|
||||
|
||||
async def get_external_channels_for_target(self, target_channel_id: uuid.UUID) -> list[domain.ExternalChannel]:
|
||||
return await domain.ExternalChannel.filter(target_channels__id=target_channel_id).all()
|
||||
async def get_workspace_channels(self, workspace_id: uuid.UUID) -> list[domain.Channel]:
|
||||
return await domain.Channel.filter(workspace_id=workspace_id).all()
|
||||
|
||||
async def get_workspace_external_channels(self, workspace_id: uuid.UUID) -> list[domain.ExternalChannel]:
|
||||
return await domain.ExternalChannel.filter(workspace_id=workspace_id).all()
|
||||
|
||||
async def add_external_channel_to_targets(
|
||||
self, external_channel: domain.ExternalChannel, target_channel_ids: list[uuid.UUID]
|
||||
) -> None:
|
||||
target_channels = await domain.TargetChannel.filter(id__in=target_channel_ids).all()
|
||||
await external_channel.target_channels.add(*target_channels)
|
||||
|
||||
async def remove_external_channel_from_target(
|
||||
self, external_channel_id: uuid.UUID, target_channel_id: uuid.UUID
|
||||
) -> None:
|
||||
external_channel = await domain.ExternalChannel.get_or_none(id=external_channel_id)
|
||||
|
||||
if not external_channel:
|
||||
raise ValueError(f'ExternalChannel {external_channel_id} not found')
|
||||
|
||||
target_channel = await domain.TargetChannel.get_or_none(id=target_channel_id)
|
||||
if target_channel:
|
||||
await external_channel.target_channels.remove(target_channel)
|
||||
|
||||
async def delete_external_channel(self, channel_id: uuid.UUID) -> None:
|
||||
await domain.ExternalChannel.filter(id=channel_id).delete()
|
||||
|
||||
async def update_external_channel(self, channel: domain.ExternalChannel) -> None:
|
||||
async def create_channel(self, channel: domain.Channel) -> None:
|
||||
await channel.save()
|
||||
|
||||
async def update_channel(self, channel: domain.Channel) -> None:
|
||||
await channel.save()
|
||||
|
||||
async def delete_channel(self, channel_id: uuid.UUID) -> None:
|
||||
await domain.Channel.filter(id=channel_id).delete()
|
||||
|
||||
async def search_channels(self, username_query: str | None = None) -> list[domain.Channel]:
|
||||
query = domain.Channel.all()
|
||||
|
||||
if username_query:
|
||||
# Частичный поиск (case-insensitive)
|
||||
query = query.filter(username__icontains=username_query)
|
||||
|
||||
return await query.all()
|
||||
|
||||
async def get_project(
|
||||
self, workspace_id: uuid.UUID, project_id: uuid.UUID | None = None, channel_id: uuid.UUID | None = None
|
||||
) -> domain.Project | None:
|
||||
query = domain.Project.filter(workspace_id=workspace_id).prefetch_related('channel')
|
||||
if project_id:
|
||||
query = query.filter(id=project_id)
|
||||
if channel_id:
|
||||
query = query.filter(channel_id=channel_id)
|
||||
return await query.first()
|
||||
|
||||
async def get_project_for_user_by_telegram(
|
||||
self, user_id: uuid.UUID, channel_telegram_id: int
|
||||
) -> domain.Project | None:
|
||||
return (
|
||||
await domain.Project.filter(
|
||||
channel__telegram_id=channel_telegram_id, workspace__workspace_users__user_id=user_id
|
||||
)
|
||||
.prefetch_related('channel')
|
||||
.first()
|
||||
)
|
||||
|
||||
async def get_project_by_channel_telegram(self, channel_telegram_id: int) -> domain.Project | None:
|
||||
return await domain.Project.filter(channel__telegram_id=channel_telegram_id).prefetch_related('channel').first()
|
||||
|
||||
async def create_project(self, project: domain.Project) -> None:
|
||||
await project.save()
|
||||
|
||||
async def update_project(self, project: domain.Project) -> None:
|
||||
await project.save()
|
||||
|
||||
async def get_workspace_projects(self, workspace_id: uuid.UUID) -> list[domain.Project]:
|
||||
return (
|
||||
await domain.Project.filter(workspace_id=workspace_id)
|
||||
.prefetch_related('channel')
|
||||
.order_by('created_at')
|
||||
.all()
|
||||
)
|
||||
|
||||
async def get_active_purchase_plan(self, project_id: uuid.UUID) -> domain.PurchasePlan | None:
|
||||
return await domain.PurchasePlan.get_or_none(project_id=project_id, status=domain.PurchasePlanStatus.ACTIVE)
|
||||
|
||||
async def get_purchase_plan(self, workspace_id: uuid.UUID, plan_id: uuid.UUID) -> domain.PurchasePlan | None:
|
||||
return await domain.PurchasePlan.get_or_none(id=plan_id, workspace_id=workspace_id)
|
||||
|
||||
async def create_purchase_plan(self, plan: domain.PurchasePlan) -> None:
|
||||
await plan.save()
|
||||
|
||||
async def get_purchase_plan_channels(self, plan_id: uuid.UUID) -> list[domain.PurchasePlanChannel]:
|
||||
return await domain.PurchasePlanChannel.filter(purchase_plan_id=plan_id).prefetch_related('channel').all()
|
||||
|
||||
async def get_purchase_plan_channel(
|
||||
self, plan_channel_id: uuid.UUID, plan_id: uuid.UUID
|
||||
) -> domain.PurchasePlanChannel | None:
|
||||
return (
|
||||
await domain.PurchasePlanChannel.filter(id=plan_channel_id, purchase_plan_id=plan_id)
|
||||
.prefetch_related('channel')
|
||||
.first()
|
||||
)
|
||||
|
||||
async def add_channel_to_purchase_plan(
|
||||
self,
|
||||
plan_id: uuid.UUID,
|
||||
channel_id: uuid.UUID,
|
||||
*,
|
||||
status: domain.PurchasePlanChannelStatus | None = None,
|
||||
planned_cost: float | None = None,
|
||||
comment: str | None = None,
|
||||
) -> domain.PurchasePlanChannel:
|
||||
defaults: dict[str, object | None] = {}
|
||||
if status is not None:
|
||||
defaults['status'] = status
|
||||
if planned_cost is not None:
|
||||
defaults['planned_cost'] = planned_cost
|
||||
if comment is not None:
|
||||
defaults['comment'] = comment
|
||||
|
||||
plan_channel, created = await domain.PurchasePlanChannel.get_or_create(
|
||||
purchase_plan_id=plan_id,
|
||||
channel_id=channel_id,
|
||||
defaults=defaults,
|
||||
)
|
||||
|
||||
if not created and defaults:
|
||||
for field, value in defaults.items():
|
||||
setattr(plan_channel, field, value)
|
||||
await plan_channel.save()
|
||||
|
||||
return plan_channel
|
||||
|
||||
async def update_purchase_plan_channel(self, plan_channel: domain.PurchasePlanChannel) -> None:
|
||||
await plan_channel.save()
|
||||
|
||||
async def remove_channel_from_purchase_plan(self, plan_id: uuid.UUID, channel_id: uuid.UUID) -> None:
|
||||
await domain.PurchasePlanChannel.filter(purchase_plan_id=plan_id, channel_id=channel_id).delete()
|
||||
|
||||
async def get_creative(self, workspace_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Creative | None:
|
||||
return await domain.Creative.get_or_none(id=creative_id, workspace_id=workspace_id).prefetch_related(
|
||||
'target_channel'
|
||||
'project', 'project__channel'
|
||||
)
|
||||
|
||||
async def update_creative(self, creative: domain.Creative) -> None:
|
||||
await creative.save()
|
||||
|
||||
async def get_workspace_creatives(
|
||||
self, workspace_id: uuid.UUID, target_channel_id: uuid.UUID | None = None, include_archived: bool = False
|
||||
self, workspace_id: uuid.UUID, project_id: uuid.UUID | None = None, include_archived: bool = False
|
||||
) -> list[domain.Creative]:
|
||||
query = domain.Creative.filter(workspace_id=workspace_id)
|
||||
|
||||
if target_channel_id:
|
||||
query = query.filter(target_channel_id=target_channel_id)
|
||||
if project_id:
|
||||
query = query.filter(project_id=project_id)
|
||||
|
||||
if not include_archived:
|
||||
query = query.filter(status=domain.CreativeStatus.ACTIVE)
|
||||
|
||||
return await query.prefetch_related('target_channel').order_by('-created_at').all()
|
||||
return await query.prefetch_related('project', 'project__channel').order_by('-created_at').all()
|
||||
|
||||
async def delete_creative(self, creative_id: uuid.UUID) -> None:
|
||||
await domain.Creative.filter(id=creative_id).delete()
|
||||
|
||||
async def get_placement(self, workspace_id: uuid.UUID, placement_id: uuid.UUID) -> domain.Placement | None:
|
||||
return await domain.Placement.get_or_none(id=placement_id, workspace_id=workspace_id).prefetch_related(
|
||||
'target_channel', 'external_channel', 'creative'
|
||||
'project', 'project__channel', 'placement_channel', 'creative'
|
||||
)
|
||||
|
||||
async def create_placement(self, placement: domain.Placement) -> None:
|
||||
@@ -219,17 +266,17 @@ class Postgres(DatabaseBase):
|
||||
async def get_workspace_placements(
|
||||
self,
|
||||
workspace_id: uuid.UUID,
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
external_channel_id: uuid.UUID | None = None,
|
||||
project_id: uuid.UUID | None = None,
|
||||
placement_channel_id: uuid.UUID | None = None,
|
||||
creative_id: uuid.UUID | None = None,
|
||||
include_archived: bool = False,
|
||||
) -> list[domain.Placement]:
|
||||
query = domain.Placement.filter(workspace_id=workspace_id)
|
||||
|
||||
if target_channel_id:
|
||||
query = query.filter(target_channel_id=target_channel_id)
|
||||
if external_channel_id:
|
||||
query = query.filter(external_channel_id=external_channel_id)
|
||||
if project_id:
|
||||
query = query.filter(project_id=project_id)
|
||||
if placement_channel_id:
|
||||
query = query.filter(placement_channel_id=placement_channel_id)
|
||||
if creative_id:
|
||||
query = query.filter(creative_id=creative_id)
|
||||
|
||||
@@ -237,7 +284,7 @@ class Postgres(DatabaseBase):
|
||||
query = query.filter(status=domain.PlacementStatus.ACTIVE)
|
||||
|
||||
return (
|
||||
await query.prefetch_related('target_channel', 'external_channel', 'creative')
|
||||
await query.prefetch_related('project', 'project__channel', 'placement_channel', 'creative')
|
||||
.order_by('-placement_date')
|
||||
.all()
|
||||
)
|
||||
@@ -247,23 +294,9 @@ class Postgres(DatabaseBase):
|
||||
|
||||
async def get_placement_by_invite_link(self, invite_link: str) -> domain.Placement | None:
|
||||
return await domain.Placement.get_or_none(invite_link=invite_link).prefetch_related(
|
||||
'target_channel', 'external_channel', 'creative'
|
||||
'project', 'project__channel', 'placement_channel', 'creative'
|
||||
)
|
||||
|
||||
async def get_subscriber(self, telegram_id: int) -> domain.Subscriber | None:
|
||||
return await domain.Subscriber.get_or_none(telegram_id=telegram_id)
|
||||
|
||||
async def upsert_subscriber(self, subscriber: domain.Subscriber) -> None:
|
||||
existing = await self.get_subscriber(subscriber.telegram_id)
|
||||
if not existing:
|
||||
await subscriber.save()
|
||||
return
|
||||
|
||||
existing.username = subscriber.username
|
||||
existing.first_name = subscriber.first_name
|
||||
existing.last_name = subscriber.last_name
|
||||
await existing.save()
|
||||
|
||||
async def create_subscription(self, subscription: domain.Subscription) -> None:
|
||||
await subscription.save()
|
||||
|
||||
@@ -275,26 +308,26 @@ class Postgres(DatabaseBase):
|
||||
async def update_subscription(self, subscription: domain.Subscription) -> None:
|
||||
await subscription.save()
|
||||
|
||||
async def get_active_subscriptions_by_subscriber_and_channel(
|
||||
self, subscriber_id: uuid.UUID, channel_telegram_id: int
|
||||
async def get_active_subscriptions_by_subscriber_and_project(
|
||||
self, subscriber_id: uuid.UUID, project_id: uuid.UUID
|
||||
) -> list[domain.Subscription]:
|
||||
return (
|
||||
await domain.Subscription.filter(
|
||||
subscriber_id=subscriber_id,
|
||||
target_channel__telegram_id=channel_telegram_id,
|
||||
placement__project_id=project_id,
|
||||
status=domain.SubscriptionStatus.ACTIVE,
|
||||
)
|
||||
.prefetch_related('placement', 'subscriber')
|
||||
.all()
|
||||
)
|
||||
|
||||
async def get_active_subscription_by_subscriber_and_channel(
|
||||
self, subscriber_id: uuid.UUID, channel_telegram_id: int
|
||||
async def get_active_subscription_by_subscriber_and_project(
|
||||
self, subscriber_id: uuid.UUID, project_id: uuid.UUID
|
||||
) -> domain.Subscription | None:
|
||||
return (
|
||||
await domain.Subscription.filter(
|
||||
subscriber_id=subscriber_id,
|
||||
target_channel__telegram_id=channel_telegram_id,
|
||||
placement__project_id=project_id,
|
||||
status=domain.SubscriptionStatus.ACTIVE,
|
||||
)
|
||||
.prefetch_related('placement', 'subscriber')
|
||||
|
||||
@@ -2,10 +2,11 @@ from fastapi import APIRouter
|
||||
|
||||
from src.controller.http.analytics import analytics_router
|
||||
from src.controller.http.auth import auth_router
|
||||
from src.controller.http.channels import channels_router
|
||||
from src.controller.http.creatives import creatives_router
|
||||
from src.controller.http.external_channels import external_channels_router
|
||||
from src.controller.http.placements import placements_router
|
||||
from src.controller.http.target_channels import target_channels_router
|
||||
from src.controller.http.projects import projects_router
|
||||
from src.controller.http.purchase_plans import purchase_plan_channels_router
|
||||
from src.controller.http.views import views_router
|
||||
from src.controller.http.workspaces import workspaces_router
|
||||
|
||||
@@ -14,8 +15,9 @@ api_router = APIRouter()
|
||||
# API v1 endpoints
|
||||
api_v1_router = APIRouter(prefix='/api/v1')
|
||||
api_v1_router.include_router(auth_router)
|
||||
api_v1_router.include_router(target_channels_router)
|
||||
api_v1_router.include_router(external_channels_router)
|
||||
api_v1_router.include_router(channels_router)
|
||||
api_v1_router.include_router(projects_router)
|
||||
api_v1_router.include_router(purchase_plan_channels_router)
|
||||
api_v1_router.include_router(creatives_router)
|
||||
api_v1_router.include_router(placements_router)
|
||||
api_v1_router.include_router(views_router)
|
||||
|
||||
@@ -15,12 +15,12 @@ analytics_router = APIRouter(prefix='/workspaces/{workspace_id}/analytics', tags
|
||||
async def get_placements_analytics(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
project_id: uuid.UUID | None = None,
|
||||
) -> dto.GetPlacementsAnalyticsOutput:
|
||||
input = dto.GetPlacementsAnalyticsInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
target_channel_id=target_channel_id,
|
||||
project_id=project_id,
|
||||
)
|
||||
return await deps.get_usecase().get_placements_analytics(input)
|
||||
|
||||
@@ -29,35 +29,35 @@ async def get_placements_analytics(
|
||||
async def get_creatives_analytics(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
project_id: uuid.UUID | None = None,
|
||||
) -> dto.GetCreativesAnalyticsOutput:
|
||||
input = dto.GetCreativesAnalyticsInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
target_channel_id=target_channel_id,
|
||||
project_id=project_id,
|
||||
)
|
||||
return await deps.get_usecase().get_creatives_analytics(input)
|
||||
|
||||
|
||||
@analytics_router.get('/external-channels')
|
||||
async def get_external_channels_analytics(
|
||||
@analytics_router.get('/channels')
|
||||
async def get_channel_analytics(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
) -> dto.GetExternalChannelsAnalyticsOutput:
|
||||
input = dto.GetExternalChannelsAnalyticsInput(
|
||||
project_id: uuid.UUID | None = None,
|
||||
) -> dto.GetChannelAnalyticsOutput:
|
||||
input = dto.GetChannelAnalyticsInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
target_channel_id=target_channel_id,
|
||||
project_id=project_id,
|
||||
)
|
||||
return await deps.get_usecase().get_external_channels_analytics(input)
|
||||
return await deps.get_usecase().get_channel_analytics(input)
|
||||
|
||||
|
||||
@analytics_router.get('/spending')
|
||||
async def get_spending_analytics(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
project_id: uuid.UUID | None = None,
|
||||
date_from: datetime.datetime | None = None,
|
||||
date_to: datetime.datetime | None = None,
|
||||
grouping: dto.DateGrouping = dto.DateGrouping.DAY,
|
||||
@@ -65,7 +65,7 @@ async def get_spending_analytics(
|
||||
input = dto.GetSpendingAnalyticsInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
target_channel_id=target_channel_id,
|
||||
project_id=project_id,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
grouping=grouping,
|
||||
|
||||
18
src/controller/http/channels.py
Normal file
18
src/controller/http/channels.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi.routing import APIRouter
|
||||
|
||||
from src import deps, dto
|
||||
from src.adapter.jwt import JWTPayload
|
||||
|
||||
channels_router = APIRouter(prefix='/channels', tags=['channels'])
|
||||
|
||||
|
||||
@channels_router.get('')
|
||||
async def get_channels(
|
||||
_: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
username: str | None = None,
|
||||
) -> dto.GetChannelsOutput:
|
||||
input = dto.GetChannelsInput(username=username)
|
||||
return await deps.get_usecase().get_channels(input=input)
|
||||
@@ -14,13 +14,13 @@ creatives_router = APIRouter(prefix='/workspaces/{workspace_id}/creatives', tags
|
||||
async def list_creatives(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
project_id: uuid.UUID | None = None,
|
||||
include_archived: bool = False,
|
||||
) -> dto.GetCreativesOutput:
|
||||
input = dto.GetCreativesInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
target_channel_id=target_channel_id,
|
||||
project_id=project_id,
|
||||
include_archived=include_archived,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi.routing import APIRouter
|
||||
|
||||
from src import deps, dto
|
||||
from src.adapter.jwt import JWTPayload
|
||||
|
||||
external_channels_router = APIRouter(prefix='/workspaces/{workspace_id}/external_channels', tags=['external_channels'])
|
||||
|
||||
|
||||
@external_channels_router.get('/target/{target_channel_id}')
|
||||
async def get_external_channels(
|
||||
target_channel_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.GetExternalChannelsOutput:
|
||||
"""Get all external channels for a specific target channel."""
|
||||
input = dto.GetExternalChannelsInput(
|
||||
target_channel_id=target_channel_id,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
return await deps.get_usecase().get_external_channels(input=input)
|
||||
|
||||
|
||||
@external_channels_router.post('')
|
||||
async def create_external_channel(
|
||||
request: dto.CreateExternalChannelInput,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.ExternalChannelOutput:
|
||||
"""Create a new external channel and link it to target channels."""
|
||||
return await deps.get_usecase().create_external_channel(
|
||||
input=request,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
|
||||
@external_channels_router.patch('/{channel_id}')
|
||||
async def update_external_channel(
|
||||
channel_id: uuid.UUID,
|
||||
request: dto.UpdateExternalChannelInput,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.ExternalChannelOutput:
|
||||
"""Update external channel fields (title, username, description, subscribers_count)."""
|
||||
return await deps.get_usecase().update_external_channel(
|
||||
channel_id=channel_id,
|
||||
input=request,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
|
||||
@external_channels_router.patch('/{channel_id}/links')
|
||||
async def update_external_channel_links(
|
||||
channel_id: uuid.UUID,
|
||||
request: dto.UpdateExternalChannelLinksInput,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.ExternalChannelOutput:
|
||||
"""
|
||||
Update target channel links for an existing external channel.
|
||||
|
||||
You can add new target channels, remove existing ones, or both in a single request.
|
||||
"""
|
||||
return await deps.get_usecase().update_external_channel_links(
|
||||
channel_id=channel_id,
|
||||
input=request,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
|
||||
@external_channels_router.delete('/{channel_id}')
|
||||
async def delete_external_channel(
|
||||
channel_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> None:
|
||||
"""Delete an external channel."""
|
||||
input = dto.DeleteExternalChannelInput(
|
||||
channel_id=channel_id,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
await deps.get_usecase().delete_external_channel(input=input)
|
||||
@@ -14,16 +14,16 @@ placements_router = APIRouter(prefix='/workspaces/{workspace_id}/placements', ta
|
||||
async def list_placements(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
external_channel_id: uuid.UUID | None = None,
|
||||
project_id: uuid.UUID | None = None,
|
||||
placement_channel_id: uuid.UUID | None = None,
|
||||
creative_id: uuid.UUID | None = None,
|
||||
include_archived: bool = False,
|
||||
) -> dto.GetPlacementsOutput:
|
||||
input = dto.GetPlacementsInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
target_channel_id=target_channel_id,
|
||||
external_channel_id=external_channel_id,
|
||||
project_id=project_id,
|
||||
placement_channel_id=placement_channel_id,
|
||||
creative_id=creative_id,
|
||||
include_archived=include_archived,
|
||||
)
|
||||
|
||||
@@ -7,17 +7,17 @@ from fastapi.routing import APIRouter
|
||||
from src import deps, dto
|
||||
from src.adapter.jwt import JWTPayload
|
||||
|
||||
target_channels_router = APIRouter(prefix='/workspaces/{workspace_id}/target_channels', tags=['target_channels'])
|
||||
projects_router = APIRouter(prefix='/workspaces/{workspace_id}/projects', tags=['projects'])
|
||||
|
||||
|
||||
@target_channels_router.get('')
|
||||
async def get_user_target_chans(
|
||||
@projects_router.get('')
|
||||
async def get_workspace_projects(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.GetUserTargetChansOutput:
|
||||
input = dto.GetUserTargetChansInput(
|
||||
) -> dto.GetWorkspaceProjectsOutput:
|
||||
input = dto.GetWorkspaceProjectsInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
return await deps.get_usecase().get_user_target_chans(input=input)
|
||||
return await deps.get_usecase().get_workspace_projects(input=input)
|
||||
74
src/controller/http/purchase_plans.py
Normal file
74
src/controller/http/purchase_plans.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi.routing import APIRouter
|
||||
|
||||
from src import deps, dto
|
||||
from src.adapter.jwt import JWTPayload
|
||||
|
||||
purchase_plan_channels_router = APIRouter(
|
||||
prefix='/workspaces/{workspace_id}/projects/{project_id}/purchase-plan/channels',
|
||||
tags=['purchase plan'],
|
||||
)
|
||||
|
||||
|
||||
@purchase_plan_channels_router.get('')
|
||||
async def get_purchase_plan_channels(
|
||||
workspace_id: uuid.UUID,
|
||||
project_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.GetPurchasePlanChannelsOutput:
|
||||
input = dto.GetPurchasePlanChannelsInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
)
|
||||
return await deps.get_usecase().get_purchase_plan_channels(input=input)
|
||||
|
||||
|
||||
@purchase_plan_channels_router.post('')
|
||||
async def attach_channel_to_purchase_plan(
|
||||
request: dto.AttachChannelToPurchasePlanInput,
|
||||
workspace_id: uuid.UUID,
|
||||
project_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.PurchasePlanChannelOutput:
|
||||
return await deps.get_usecase().attach_channel_to_purchase_plan(
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
user_id=current_user.user_id,
|
||||
input=request,
|
||||
)
|
||||
|
||||
|
||||
@purchase_plan_channels_router.patch('/{channel_id}')
|
||||
async def update_purchase_plan_channel(
|
||||
channel_id: uuid.UUID,
|
||||
request: dto.UpdatePurchasePlanChannelInput,
|
||||
workspace_id: uuid.UUID,
|
||||
project_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.PurchasePlanChannelOutput:
|
||||
return await deps.get_usecase().update_purchase_plan_channel(
|
||||
channel_id=channel_id,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
user_id=current_user.user_id,
|
||||
input=request,
|
||||
)
|
||||
|
||||
|
||||
@purchase_plan_channels_router.delete('/{channel_id}')
|
||||
async def remove_purchase_plan_channel(
|
||||
channel_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
project_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> None:
|
||||
await deps.get_usecase().remove_purchase_plan_channel(
|
||||
channel_id=channel_id,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
user_id=current_user.user_id,
|
||||
)
|
||||
@@ -8,6 +8,6 @@ from . import ( # noqa: E402, F401
|
||||
chat_member_updated,
|
||||
creative_commands,
|
||||
my_chat_member,
|
||||
project_commands,
|
||||
start_with_login,
|
||||
target_channel_commands,
|
||||
)
|
||||
|
||||
@@ -29,12 +29,12 @@ async def on_bot_added_to_chat(event: ChatMemberUpdated) -> None:
|
||||
bot_removed = was_member and is_now_not_member
|
||||
|
||||
if bot_removed:
|
||||
disconnect_input = dto.DisconnectTargetChanByTgIdInput(
|
||||
disconnect_input = dto.DisconnectProjectByTgIdInput(
|
||||
telegram_id=event.chat.id,
|
||||
user_telegram_id=event.from_user.id,
|
||||
)
|
||||
try:
|
||||
await usecase.disconnect_target_chan_by_tg_id(input=disconnect_input)
|
||||
await usecase.disconnect_project_by_tg_id(input=disconnect_input)
|
||||
except HTTPException as e:
|
||||
log.error(e)
|
||||
|
||||
@@ -53,14 +53,14 @@ async def on_bot_added_to_chat(event: ChatMemberUpdated) -> None:
|
||||
)
|
||||
|
||||
if permissions_changed:
|
||||
permissions_input = dto.UpdateTargetChanPermissionsInput(
|
||||
permissions_input = dto.UpdateProjectPermissionsInput(
|
||||
telegram_id=event.chat.id,
|
||||
permissions=bot_permissions,
|
||||
chat_title=event.chat.title or f'Channel {event.chat.id}',
|
||||
user_telegram_id=event.from_user.id,
|
||||
)
|
||||
try:
|
||||
await usecase.update_target_chan_permissions(input=permissions_input)
|
||||
await usecase.update_project_permissions(input=permissions_input)
|
||||
except HTTPException as e:
|
||||
log.error(e)
|
||||
return
|
||||
@@ -71,7 +71,7 @@ async def on_bot_added_to_chat(event: ChatMemberUpdated) -> None:
|
||||
bot_added = was_not_member and is_now_member
|
||||
|
||||
if bot_added:
|
||||
connect_input = dto.ConnectTargetChanInput(
|
||||
connect_input = dto.ConnectProjectInput(
|
||||
telegram_id=event.chat.id,
|
||||
title=event.chat.title or f'Channel {event.chat.id}',
|
||||
username=event.chat.username,
|
||||
@@ -79,6 +79,6 @@ async def on_bot_added_to_chat(event: ChatMemberUpdated) -> None:
|
||||
bot_permissions=bot_permissions,
|
||||
)
|
||||
try:
|
||||
await usecase.tg_add_target_chan_1(input=connect_input)
|
||||
await usecase.tg_add_project_1(input=connect_input)
|
||||
except HTTPException as e:
|
||||
log.error(e)
|
||||
|
||||
@@ -9,14 +9,14 @@ from src.controller.telegram_callback import telegram_callback_router
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@telegram_callback_router.callback_query(F.data.startswith('tg_add_target_chan_2:'))
|
||||
async def callback_target_channel_workspace(callback: CallbackQuery) -> None:
|
||||
@telegram_callback_router.callback_query(F.data.startswith('tg_add_project_2:'))
|
||||
async def callback_project_workspace(callback: CallbackQuery) -> None:
|
||||
if not callback.from_user or not callback.message or not callback.data:
|
||||
log.error('Failed to get required data from callback query')
|
||||
return
|
||||
|
||||
usecase = deps.get_usecase()
|
||||
await usecase.tg_add_target_chan_2(
|
||||
await usecase.tg_add_project_2(
|
||||
telegram_id=callback.from_user.id,
|
||||
chat_id=callback.message.chat.id,
|
||||
callback_data=callback.data,
|
||||
@@ -2,16 +2,29 @@ __all__ = (
|
||||
'User',
|
||||
'Workspace',
|
||||
'WorkspaceUser',
|
||||
'TargetChannel',
|
||||
'ExternalChannel',
|
||||
'WorkspaceInvite',
|
||||
'WorkspaceInviteStatus',
|
||||
'WorkspaceUserStatus',
|
||||
'WorkspaceUserPermission',
|
||||
'WorkspaceUserPermissionScope',
|
||||
'PermissionKey',
|
||||
'PermissionScopeType',
|
||||
'Channel',
|
||||
'ChannelVerificationStatus',
|
||||
'Project',
|
||||
'ProjectStatus',
|
||||
'PurchasePlan',
|
||||
'PurchasePlanChannel',
|
||||
'PurchasePlanStatus',
|
||||
'PurchasePlanChannelStatus',
|
||||
'Creative',
|
||||
'Placement',
|
||||
'Subscription',
|
||||
'Subscriber',
|
||||
'PlacementViewsHistory',
|
||||
'TelegramState',
|
||||
'TelegramStateEnum',
|
||||
'TargetChannelStatus',
|
||||
'ChannelNotFound',
|
||||
'ProjectNotFound',
|
||||
'CreativeStatus',
|
||||
'PlacementStatus',
|
||||
'SubscriptionStatus',
|
||||
@@ -24,40 +37,57 @@ __all__ = (
|
||||
'LoginTokenNotFound',
|
||||
'LoginTokenExpired',
|
||||
'LoginTokenAlreadyUsed',
|
||||
'TargetChannelNotFound',
|
||||
'ProjectNotFound',
|
||||
'PurchasePlanNotFound',
|
||||
'PurchasePlanChannelNotFound',
|
||||
'ChannelNotFound',
|
||||
'ChannelAlreadyExists',
|
||||
'ChannelNoAdminRights',
|
||||
'ExternalChannelNotFound',
|
||||
'ExternalChannelAlreadyExists',
|
||||
'CreativeNotFound',
|
||||
'CreativeInUse',
|
||||
'PlacementNotFound',
|
||||
)
|
||||
|
||||
from .channel import Channel, ChannelVerificationStatus
|
||||
from .creative import Creative, CreativeStatus
|
||||
from .error import (
|
||||
ChannelAlreadyExists,
|
||||
ChannelNoAdminRights,
|
||||
ChannelNotFound,
|
||||
CreativeInUse,
|
||||
CreativeNotFound,
|
||||
ExternalChannelAlreadyExists,
|
||||
ExternalChannelNotFound,
|
||||
LoginTokenAlreadyUsed,
|
||||
LoginTokenExpired,
|
||||
LoginTokenNotFound,
|
||||
PlacementNotFound,
|
||||
TargetChannelNotFound,
|
||||
ProjectNotFound,
|
||||
PurchasePlanChannelNotFound,
|
||||
PurchasePlanNotFound,
|
||||
UserNotFound,
|
||||
WorkspaceAccessDenied,
|
||||
WorkspaceNotFound,
|
||||
)
|
||||
from .external_channel import ExternalChannel
|
||||
from .login_token import LoginToken
|
||||
from .placement import InviteLinkType, Placement, PlacementStatus, PostViewsAvailability
|
||||
from .placement_views_history import PlacementViewsHistory
|
||||
from .subscriber import Subscriber
|
||||
from .project import Project, ProjectStatus
|
||||
from .purchase_plan import (
|
||||
PurchasePlan,
|
||||
PurchasePlanChannel,
|
||||
PurchasePlanChannelStatus,
|
||||
PurchasePlanStatus,
|
||||
)
|
||||
from .subscription import Subscription, SubscriptionStatus
|
||||
from .target_channel import TargetChannel, TargetChannelStatus
|
||||
from .telegram_state import TelegramState, TelegramStateEnum
|
||||
from .user import User
|
||||
from .workspace import Workspace, WorkspaceUser
|
||||
from .workspace import (
|
||||
PermissionKey,
|
||||
PermissionScopeType,
|
||||
Workspace,
|
||||
WorkspaceInvite,
|
||||
WorkspaceInviteStatus,
|
||||
WorkspaceUser,
|
||||
WorkspaceUserPermission,
|
||||
WorkspaceUserPermissionScope,
|
||||
WorkspaceUserStatus,
|
||||
)
|
||||
|
||||
34
src/domain/channel.py
Normal file
34
src/domain/channel.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import enum
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from tortoise import fields
|
||||
|
||||
from .base import TimestampedModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .workspace import Workspace
|
||||
|
||||
|
||||
class ChannelVerificationStatus(str, enum.Enum):
|
||||
UNVERIFIED = 'unverified'
|
||||
VERIFIED = 'verified'
|
||||
FAILED = 'failed'
|
||||
|
||||
|
||||
class Channel(TimestampedModel):
|
||||
id = fields.UUIDField(pk=True)
|
||||
telegram_id = fields.BigIntField(null=True, unique=True, index=True)
|
||||
username = fields.CharField(max_length=255, null=True, index=True)
|
||||
title = fields.CharField(max_length=255, null=True)
|
||||
verification_status = fields.CharEnumField(ChannelVerificationStatus, default=ChannelVerificationStatus.UNVERIFIED)
|
||||
|
||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||
'models.Workspace', related_name='channels', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
workspace_id: uuid.UUID
|
||||
|
||||
class Meta:
|
||||
table = 'channel'
|
||||
@@ -7,7 +7,7 @@ from tortoise import fields
|
||||
from .base import TimestampedModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .target_channel import TargetChannel
|
||||
from .project import Project
|
||||
from .workspace import Workspace
|
||||
|
||||
|
||||
@@ -26,13 +26,13 @@ class Creative(TimestampedModel):
|
||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||
'models.Workspace', related_name='creatives', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
target_channel: fields.ForeignKeyRelation['TargetChannel'] = fields.ForeignKeyField(
|
||||
'models.TargetChannel', related_name='creatives', on_delete=fields.CASCADE, index=True
|
||||
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
|
||||
'models.Project', related_name='creatives', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
workspace_id: UUID
|
||||
target_channel_id: UUID
|
||||
project_id: UUID
|
||||
|
||||
class Meta:
|
||||
table = 'creative'
|
||||
|
||||
@@ -21,10 +21,28 @@ def LoginTokenAlreadyUsed() -> HTTPException:
|
||||
return HTTPException(status.HTTP_400_BAD_REQUEST, 'Login token has already been used')
|
||||
|
||||
|
||||
def TargetChannelNotFound(channel_id: uuid.UUID | None = None) -> HTTPException:
|
||||
def ChannelNotFound(channel_id: uuid.UUID | None = None) -> HTTPException:
|
||||
if channel_id is None:
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'Target channel not found')
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'Target channel {channel_id} not found')
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'Channel not found')
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'Channel {channel_id} not found')
|
||||
|
||||
|
||||
def ProjectNotFound(project_id: uuid.UUID | None = None) -> HTTPException:
|
||||
if project_id is None:
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'Project not found')
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'Project {project_id} not found')
|
||||
|
||||
|
||||
def PurchasePlanNotFound(plan_id: uuid.UUID | None = None) -> HTTPException:
|
||||
if plan_id is None:
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'Purchase plan not found')
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'Purchase plan {plan_id} not found')
|
||||
|
||||
|
||||
def PurchasePlanChannelNotFound(plan_channel_id: uuid.UUID | None = None) -> HTTPException:
|
||||
if plan_channel_id is None:
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'Purchase plan channel not found')
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'Purchase plan channel {plan_channel_id} not found')
|
||||
|
||||
|
||||
def ChannelAlreadyExists(telegram_id: int) -> HTTPException:
|
||||
@@ -37,16 +55,6 @@ def ChannelNoAdminRights() -> HTTPException:
|
||||
)
|
||||
|
||||
|
||||
def ExternalChannelNotFound(channel_id: uuid.UUID | None = None) -> HTTPException:
|
||||
if channel_id is None:
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'External channel not found')
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'External channel {channel_id} not found')
|
||||
|
||||
|
||||
def ExternalChannelAlreadyExists(telegram_id: int) -> HTTPException:
|
||||
return HTTPException(status.HTTP_409_CONFLICT, f'External channel {telegram_id} already exists')
|
||||
|
||||
|
||||
def CreativeNotFound(creative_id: uuid.UUID | None = None) -> HTTPException:
|
||||
if creative_id is None:
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'Creative not found')
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from tortoise import fields
|
||||
|
||||
from .base import TimestampedModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .target_channel import TargetChannel
|
||||
from .workspace import Workspace
|
||||
|
||||
|
||||
class ExternalChannel(TimestampedModel):
|
||||
id = fields.UUIDField(pk=True)
|
||||
telegram_id = fields.BigIntField(index=True)
|
||||
title = fields.CharField(max_length=255)
|
||||
username = fields.CharField(max_length=255, null=True, index=True)
|
||||
description = fields.TextField(null=True)
|
||||
subscribers_count = fields.IntField(null=True)
|
||||
|
||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||
'models.Workspace', related_name='external_channels', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
target_channels: fields.ManyToManyRelation['TargetChannel']
|
||||
workspace_id: UUID
|
||||
|
||||
class Meta:
|
||||
table = 'external_channel'
|
||||
@@ -7,9 +7,9 @@ from tortoise import fields
|
||||
from .base import TimestampedModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .channel import Channel
|
||||
from .creative import Creative
|
||||
from .external_channel import ExternalChannel
|
||||
from .target_channel import TargetChannel
|
||||
from .project import Project
|
||||
from .workspace import Workspace
|
||||
|
||||
|
||||
@@ -50,11 +50,11 @@ class Placement(TimestampedModel):
|
||||
views_availability = fields.CharEnumField(PostViewsAvailability, default=PostViewsAvailability.UNKNOWN)
|
||||
last_views_fetch_at = fields.DatetimeField(null=True)
|
||||
|
||||
target_channel: fields.ForeignKeyRelation['TargetChannel'] = fields.ForeignKeyField(
|
||||
'models.TargetChannel', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
|
||||
'models.Project', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
external_channel: fields.ForeignKeyRelation['ExternalChannel'] = fields.ForeignKeyField(
|
||||
'models.ExternalChannel', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||
placement_channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField(
|
||||
'models.Channel', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField(
|
||||
'models.Creative', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||
@@ -64,8 +64,8 @@ class Placement(TimestampedModel):
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
target_channel_id: UUID
|
||||
external_channel_id: UUID
|
||||
project_id: UUID
|
||||
placement_channel_id: UUID
|
||||
creative_id: UUID
|
||||
workspace_id: UUID
|
||||
|
||||
|
||||
37
src/domain/project.py
Normal file
37
src/domain/project.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import enum
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from tortoise import fields
|
||||
|
||||
from .base import TimestampedModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .channel import Channel
|
||||
from .workspace import Workspace
|
||||
|
||||
|
||||
class ProjectStatus(str, enum.Enum):
|
||||
ACTIVE = 'active'
|
||||
INACTIVE = 'inactive'
|
||||
ARCHIVED = 'archived'
|
||||
|
||||
|
||||
class Project(TimestampedModel):
|
||||
id = fields.UUIDField(pk=True)
|
||||
status = fields.CharEnumField(ProjectStatus, default=ProjectStatus.ACTIVE)
|
||||
|
||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||
'models.Workspace', related_name='projects', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField(
|
||||
'models.Channel', related_name='projects', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
workspace_id: UUID
|
||||
channel_id: UUID
|
||||
|
||||
class Meta:
|
||||
table = 'project'
|
||||
unique_together = (('workspace_id', 'channel_id'),)
|
||||
63
src/domain/purchase_plan.py
Normal file
63
src/domain/purchase_plan.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import enum
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from tortoise import fields
|
||||
|
||||
from .base import TimestampedModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .channel import Channel
|
||||
from .project import Project
|
||||
from .workspace import Workspace
|
||||
|
||||
|
||||
class PurchasePlanStatus(str, enum.Enum):
|
||||
ACTIVE = 'active'
|
||||
ARCHIVED = 'archived'
|
||||
|
||||
|
||||
class PurchasePlanChannelStatus(str, enum.Enum):
|
||||
PLANNED = 'planned'
|
||||
APPROVED = 'approved'
|
||||
REJECTED = 'rejected'
|
||||
IN_PROGRESS = 'in_progress'
|
||||
COMPLETED = 'completed'
|
||||
|
||||
|
||||
class PurchasePlan(TimestampedModel):
|
||||
id = fields.UUIDField(pk=True)
|
||||
status = fields.CharEnumField(PurchasePlanStatus, default=PurchasePlanStatus.ACTIVE)
|
||||
|
||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||
'models.Workspace', related_name='purchase_plans', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
|
||||
'models.Project', related_name='purchase_plans', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
table = 'purchase_plan'
|
||||
indexes = (('project', 'status'),)
|
||||
|
||||
|
||||
class PurchasePlanChannel(TimestampedModel):
|
||||
id = fields.UUIDField(pk=True)
|
||||
status = fields.CharEnumField(PurchasePlanChannelStatus, default=PurchasePlanChannelStatus.PLANNED)
|
||||
planned_cost = fields.FloatField(null=True)
|
||||
comment = fields.TextField(null=True)
|
||||
|
||||
purchase_plan: fields.ForeignKeyRelation['PurchasePlan'] = fields.ForeignKeyField(
|
||||
'models.PurchasePlan', related_name='channels', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField(
|
||||
'models.Channel', related_name='purchase_plan_channels', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
purchase_plan_id: uuid.UUID
|
||||
channel_id: uuid.UUID
|
||||
|
||||
class Meta:
|
||||
table = 'purchase_plan_channel'
|
||||
unique_together = (('purchase_plan_id', 'channel_id'),)
|
||||
@@ -1,14 +0,0 @@
|
||||
from tortoise import fields
|
||||
|
||||
from .base import TimestampedModel
|
||||
|
||||
|
||||
class Subscriber(TimestampedModel):
|
||||
id = fields.UUIDField(pk=True)
|
||||
telegram_id = fields.BigIntField(unique=True, index=True)
|
||||
username = fields.CharField(max_length=255, null=True)
|
||||
first_name = fields.CharField(max_length=255, null=True)
|
||||
last_name = fields.CharField(max_length=255, null=True)
|
||||
|
||||
class Meta:
|
||||
table = 'subscriber'
|
||||
@@ -8,8 +8,7 @@ from .base import TimestampedModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .placement import Placement
|
||||
from .subscriber import Subscriber
|
||||
from .target_channel import TargetChannel
|
||||
from .user import User
|
||||
|
||||
|
||||
class SubscriptionStatus(str, enum.Enum):
|
||||
@@ -26,17 +25,13 @@ class Subscription(TimestampedModel):
|
||||
placement: fields.ForeignKeyRelation['Placement'] = fields.ForeignKeyField(
|
||||
'models.Placement', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
subscriber: fields.ForeignKeyRelation['Subscriber'] = fields.ForeignKeyField(
|
||||
'models.Subscriber', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
target_channel: fields.ForeignKeyRelation['TargetChannel'] = fields.ForeignKeyField(
|
||||
'models.TargetChannel', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
||||
subscriber: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField(
|
||||
'models.User', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
placement_id: UUID
|
||||
subscriber_id: UUID
|
||||
target_channel_id: UUID
|
||||
|
||||
class Meta:
|
||||
table = 'subscription'
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import enum
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from tortoise import fields
|
||||
|
||||
from .base import TimestampedModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .external_channel import ExternalChannel
|
||||
from .workspace import Workspace
|
||||
|
||||
|
||||
class TargetChannelStatus(str, enum.Enum):
|
||||
ACTIVE = 'active'
|
||||
INACTIVE = 'inactive'
|
||||
ARCHIVED = 'archived'
|
||||
|
||||
|
||||
class TargetChannel(TimestampedModel):
|
||||
id = fields.UUIDField(pk=True)
|
||||
telegram_id = fields.BigIntField(unique=True, index=True)
|
||||
title = fields.CharField(max_length=255)
|
||||
username = fields.CharField(max_length=255, null=True, index=True)
|
||||
status = fields.CharEnumField(TargetChannelStatus, default=TargetChannelStatus.ACTIVE)
|
||||
|
||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||
'models.Workspace', related_name='target_channels', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
external_channels: fields.ManyToManyRelation['ExternalChannel'] = fields.ManyToManyField(
|
||||
'models.ExternalChannel',
|
||||
related_name='target_channels',
|
||||
through='target_external_channel',
|
||||
forward_key='external_channel_id',
|
||||
backward_key='target_channel_id',
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
workspace_id: UUID
|
||||
|
||||
class Meta:
|
||||
table = 'target_channel'
|
||||
@@ -10,7 +10,7 @@ class TelegramStateEnum(str, enum.Enum):
|
||||
CREATIVE_WAITING_CHANNEL = 'creative_waiting_channel'
|
||||
CREATIVE_WAITING_NAME = 'creative_waiting_name'
|
||||
CREATIVE_WAITING_TEXT = 'creative_waiting_text'
|
||||
TARGET_CHANNEL_WAITING_WORKSPACE = 'target_channel_waiting_workspace'
|
||||
PROJECT_WAITING_WORKSPACE = 'project_waiting_workspace'
|
||||
|
||||
|
||||
class TelegramState(TimestampedModel):
|
||||
|
||||
@@ -7,6 +7,8 @@ class User(TimestampedModel):
|
||||
id = fields.UUIDField(pk=True)
|
||||
telegram_id = fields.BigIntField(unique=True, index=True)
|
||||
username = fields.CharField(max_length=255, null=True)
|
||||
first_name = fields.CharField(max_length=255, null=True)
|
||||
last_name = fields.CharField(max_length=255, null=True)
|
||||
|
||||
class Meta:
|
||||
table = 'user'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -19,8 +20,15 @@ class Workspace(TimestampedModel):
|
||||
table = 'workspace'
|
||||
|
||||
|
||||
class WorkspaceUserStatus(str, enum.Enum):
|
||||
ACTIVE = 'active'
|
||||
INVITED = 'invited'
|
||||
REMOVED = 'removed'
|
||||
|
||||
|
||||
class WorkspaceUser(TimestampedModel):
|
||||
id = fields.UUIDField(pk=True)
|
||||
status = fields.CharEnumField(WorkspaceUserStatus, default=WorkspaceUserStatus.ACTIVE)
|
||||
|
||||
workspace: fields.ForeignKeyRelation[Workspace] = fields.ForeignKeyField(
|
||||
'models.Workspace', related_name='workspace_users', on_delete=fields.CASCADE, index=True
|
||||
@@ -36,3 +44,65 @@ class WorkspaceUser(TimestampedModel):
|
||||
class Meta:
|
||||
table = 'workspace_user'
|
||||
unique_together = (('workspace_id', 'user_id'),)
|
||||
|
||||
|
||||
class WorkspaceInviteStatus(str, enum.Enum):
|
||||
PENDING = 'pending'
|
||||
ACCEPTED = 'accepted'
|
||||
REVOKED = 'revoked'
|
||||
|
||||
|
||||
class WorkspaceInvite(TimestampedModel):
|
||||
id = fields.UUIDField(pk=True)
|
||||
status = fields.CharEnumField(WorkspaceInviteStatus, default=WorkspaceInviteStatus.PENDING)
|
||||
|
||||
workspace: fields.ForeignKeyRelation[Workspace] = fields.ForeignKeyField(
|
||||
'models.Workspace', related_name='invites', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
invited_by: fields.ForeignKeyRelation[User] = fields.ForeignKeyField(
|
||||
'models.User', related_name='sent_invites', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
user: fields.ForeignKeyRelation[User] = fields.ForeignKeyField(
|
||||
'models.User', related_name='workspace_invites', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
table = 'workspace_invite'
|
||||
unique_together = (('workspace_id', 'user_id'),)
|
||||
|
||||
|
||||
class PermissionKey(str, enum.Enum):
|
||||
MANAGE_PROJECTS = 'manage_projects'
|
||||
MANAGE_PLACEMENTS = 'manage_placements'
|
||||
VIEW_ANALYTICS = 'view_analytics'
|
||||
|
||||
|
||||
class PermissionScopeType(str, enum.Enum):
|
||||
WORKSPACE = 'workspace'
|
||||
PROJECT = 'project'
|
||||
|
||||
|
||||
class WorkspaceUserPermission(TimestampedModel):
|
||||
workspace_user: fields.ForeignKeyRelation[WorkspaceUser] = fields.ForeignKeyField(
|
||||
'models.WorkspaceUser', related_name='permissions', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
permission = fields.CharEnumField(PermissionKey)
|
||||
|
||||
class Meta:
|
||||
table = 'workspace_user_permission'
|
||||
unique_together = (('workspace_user_id', 'permission'),)
|
||||
|
||||
|
||||
class WorkspaceUserPermissionScope(TimestampedModel):
|
||||
id = fields.UUIDField(pk=True)
|
||||
permission = fields.CharEnumField(PermissionKey)
|
||||
scope_type = fields.CharEnumField(PermissionScopeType)
|
||||
scope_id = fields.UUIDField()
|
||||
|
||||
workspace_user: fields.ForeignKeyRelation[WorkspaceUser] = fields.ForeignKeyField(
|
||||
'models.WorkspaceUser', related_name='permission_scopes', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
table = 'workspace_user_permission_scope'
|
||||
unique_together = (('workspace_user_id', 'permission', 'scope_type', 'scope_id'),)
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
__all__ = (
|
||||
'UpdateTargetChanPermissionsInput',
|
||||
'GetUserTargetChansInput',
|
||||
'GetUserTargetChansOutput',
|
||||
'DisconnectTargetChanByTgIdInput',
|
||||
'ConnectTargetChanInput',
|
||||
'ConnectTargetChanOutput',
|
||||
'UpdateProjectPermissionsInput',
|
||||
'GetWorkspaceProjectsInput',
|
||||
'GetWorkspaceProjectsOutput',
|
||||
'DisconnectProjectByTgIdInput',
|
||||
'ConnectProjectInput',
|
||||
'ProjectOutput',
|
||||
'ValidateLoginTokenInput',
|
||||
'ValidateLoginTokenOutput',
|
||||
'TelegramLoginInput',
|
||||
'ChannelBotPermissions',
|
||||
'ExternalChannelOutput',
|
||||
'GetExternalChannelsInput',
|
||||
'GetExternalChannelsOutput',
|
||||
'CreateExternalChannelInput',
|
||||
'DeleteExternalChannelInput',
|
||||
'UpdateExternalChannelInput',
|
||||
'UpdateExternalChannelLinksInput',
|
||||
'ChannelOutput',
|
||||
'GetChannelsInput',
|
||||
'GetChannelsOutput',
|
||||
'PurchasePlanChannelOutput',
|
||||
'GetPurchasePlanChannelsInput',
|
||||
'GetPurchasePlanChannelsOutput',
|
||||
'AttachChannelToPurchasePlanInput',
|
||||
'UpdatePurchasePlanChannelInput',
|
||||
'CreativeOutput',
|
||||
'GetCreativesInput',
|
||||
'GetCreativesOutput',
|
||||
@@ -37,14 +38,14 @@ __all__ = (
|
||||
'UserOutput',
|
||||
'DateGrouping',
|
||||
'PlacementAnalyticsOutput',
|
||||
'ChannelAnalyticsOutput',
|
||||
'CreativeAnalyticsOutput',
|
||||
'ExternalChannelAnalyticsOutput',
|
||||
'GetPlacementsAnalyticsInput',
|
||||
'GetPlacementsAnalyticsOutput',
|
||||
'GetCreativesAnalyticsInput',
|
||||
'GetCreativesAnalyticsOutput',
|
||||
'GetExternalChannelsAnalyticsInput',
|
||||
'GetExternalChannelsAnalyticsOutput',
|
||||
'GetChannelAnalyticsInput',
|
||||
'GetChannelAnalyticsOutput',
|
||||
'SpendingDataPoint',
|
||||
'GetSpendingAnalyticsInput',
|
||||
'GetSpendingAnalyticsOutput',
|
||||
@@ -56,13 +57,13 @@ __all__ = (
|
||||
)
|
||||
|
||||
from .analytics import (
|
||||
ChannelAnalyticsOutput,
|
||||
CreativeAnalyticsOutput,
|
||||
DateGrouping,
|
||||
ExternalChannelAnalyticsOutput,
|
||||
GetChannelAnalyticsInput,
|
||||
GetChannelAnalyticsOutput,
|
||||
GetCreativesAnalyticsInput,
|
||||
GetCreativesAnalyticsOutput,
|
||||
GetExternalChannelsAnalyticsInput,
|
||||
GetExternalChannelsAnalyticsOutput,
|
||||
GetPlacementsAnalyticsInput,
|
||||
GetPlacementsAnalyticsOutput,
|
||||
GetSpendingAnalyticsInput,
|
||||
@@ -70,6 +71,7 @@ from .analytics import (
|
||||
PlacementAnalyticsOutput,
|
||||
SpendingDataPoint,
|
||||
)
|
||||
from .channel import ChannelOutput, GetChannelsInput, GetChannelsOutput
|
||||
from .creative import (
|
||||
CreateCreativeInput,
|
||||
CreativeOutput,
|
||||
@@ -79,15 +81,6 @@ from .creative import (
|
||||
GetCreativesOutput,
|
||||
UpdateCreativeInput,
|
||||
)
|
||||
from .external_channel import (
|
||||
CreateExternalChannelInput,
|
||||
DeleteExternalChannelInput,
|
||||
ExternalChannelOutput,
|
||||
GetExternalChannelsInput,
|
||||
GetExternalChannelsOutput,
|
||||
UpdateExternalChannelInput,
|
||||
UpdateExternalChannelLinksInput,
|
||||
)
|
||||
from .placement import (
|
||||
CreatePlacementInput,
|
||||
DeletePlacementInput,
|
||||
@@ -97,14 +90,21 @@ from .placement import (
|
||||
PlacementOutput,
|
||||
UpdatePlacementInput,
|
||||
)
|
||||
from .target_channel import (
|
||||
from .project import (
|
||||
ChannelBotPermissions,
|
||||
ConnectTargetChanInput,
|
||||
ConnectTargetChanOutput,
|
||||
DisconnectTargetChanByTgIdInput,
|
||||
GetUserTargetChansInput,
|
||||
GetUserTargetChansOutput,
|
||||
UpdateTargetChanPermissionsInput,
|
||||
ConnectProjectInput,
|
||||
DisconnectProjectByTgIdInput,
|
||||
GetWorkspaceProjectsInput,
|
||||
GetWorkspaceProjectsOutput,
|
||||
ProjectOutput,
|
||||
UpdateProjectPermissionsInput,
|
||||
)
|
||||
from .purchase_plan import (
|
||||
AttachChannelToPurchasePlanInput,
|
||||
GetPurchasePlanChannelsInput,
|
||||
GetPurchasePlanChannelsOutput,
|
||||
PurchasePlanChannelOutput,
|
||||
UpdatePurchasePlanChannelInput,
|
||||
)
|
||||
from .telegram_login import TelegramLoginInput
|
||||
from .user import UserOutput
|
||||
|
||||
@@ -15,10 +15,10 @@ class DateGrouping(StrEnum):
|
||||
|
||||
class PlacementAnalyticsOutput(pydantic.BaseModel):
|
||||
id: uuid.UUID
|
||||
target_channel_id: uuid.UUID
|
||||
target_channel_title: str
|
||||
external_channel_id: uuid.UUID
|
||||
external_channel_title: str
|
||||
project_id: uuid.UUID
|
||||
project_channel_title: str
|
||||
placement_channel_id: uuid.UUID
|
||||
placement_channel_title: str
|
||||
creative_id: uuid.UUID
|
||||
creative_name: str
|
||||
placement_date: datetime.datetime
|
||||
@@ -40,7 +40,7 @@ class CreativeAnalyticsOutput(pydantic.BaseModel):
|
||||
avg_cpm: float | None
|
||||
|
||||
|
||||
class ExternalChannelAnalyticsOutput(pydantic.BaseModel):
|
||||
class ChannelAnalyticsOutput(pydantic.BaseModel):
|
||||
id: uuid.UUID
|
||||
title: str
|
||||
username: str | None
|
||||
@@ -55,7 +55,7 @@ class ExternalChannelAnalyticsOutput(pydantic.BaseModel):
|
||||
class GetPlacementsAnalyticsInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
target_channel_id: uuid.UUID | None = None
|
||||
project_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
class GetPlacementsAnalyticsOutput(pydantic.BaseModel):
|
||||
@@ -65,21 +65,21 @@ class GetPlacementsAnalyticsOutput(pydantic.BaseModel):
|
||||
class GetCreativesAnalyticsInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
target_channel_id: uuid.UUID | None = None
|
||||
project_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
class GetCreativesAnalyticsOutput(pydantic.BaseModel):
|
||||
creatives: list[CreativeAnalyticsOutput]
|
||||
|
||||
|
||||
class GetExternalChannelsAnalyticsInput(pydantic.BaseModel):
|
||||
class GetChannelAnalyticsInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
target_channel_id: uuid.UUID | None = None
|
||||
project_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
class GetExternalChannelsAnalyticsOutput(pydantic.BaseModel):
|
||||
external_channels: list[ExternalChannelAnalyticsOutput]
|
||||
class GetChannelAnalyticsOutput(pydantic.BaseModel):
|
||||
channels: list[ChannelAnalyticsOutput]
|
||||
|
||||
|
||||
class SpendingDataPoint(pydantic.BaseModel):
|
||||
@@ -94,7 +94,7 @@ class SpendingDataPoint(pydantic.BaseModel):
|
||||
class GetSpendingAnalyticsInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
target_channel_id: uuid.UUID | None = None
|
||||
project_id: uuid.UUID | None = None
|
||||
date_from: datetime.datetime | None = None
|
||||
date_to: datetime.datetime | None = None
|
||||
grouping: DateGrouping = DateGrouping.DAY
|
||||
|
||||
21
src/dto/channel.py
Normal file
21
src/dto/channel.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import uuid
|
||||
|
||||
import pydantic
|
||||
|
||||
from src.domain.channel import ChannelVerificationStatus
|
||||
|
||||
|
||||
class ChannelOutput(pydantic.BaseModel):
|
||||
id: uuid.UUID
|
||||
telegram_id: int | None
|
||||
title: str | None
|
||||
username: str | None
|
||||
verification_status: ChannelVerificationStatus
|
||||
|
||||
|
||||
class GetChannelsInput(pydantic.BaseModel):
|
||||
username: str | None = None
|
||||
|
||||
|
||||
class GetChannelsOutput(pydantic.BaseModel):
|
||||
channels: list[ChannelOutput]
|
||||
@@ -10,8 +10,8 @@ class CreativeOutput(pydantic.BaseModel):
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
text: str
|
||||
target_channel_id: uuid.UUID
|
||||
target_channel_title: str
|
||||
project_id: uuid.UUID
|
||||
project_channel_title: str
|
||||
created_at: datetime.datetime
|
||||
status: domain.CreativeStatus
|
||||
placements_count: int
|
||||
@@ -20,7 +20,7 @@ class CreativeOutput(pydantic.BaseModel):
|
||||
class GetCreativesInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
target_channel_id: uuid.UUID | None = None
|
||||
project_id: uuid.UUID | None = None
|
||||
include_archived: bool = False
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class GetCreativeInput(pydantic.BaseModel):
|
||||
class CreateCreativeInput(pydantic.BaseModel):
|
||||
name: str
|
||||
text: str
|
||||
target_channel_id: uuid.UUID
|
||||
project_id: uuid.UUID
|
||||
|
||||
|
||||
class UpdateCreativeInput(pydantic.BaseModel):
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import uuid
|
||||
|
||||
import pydantic
|
||||
|
||||
|
||||
class ExternalChannelOutput(pydantic.BaseModel):
|
||||
id: uuid.UUID
|
||||
telegram_id: int
|
||||
title: str
|
||||
username: str | None
|
||||
description: str | None
|
||||
subscribers_count: int | None
|
||||
|
||||
|
||||
class GetExternalChannelsInput(pydantic.BaseModel):
|
||||
target_channel_id: uuid.UUID
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
|
||||
|
||||
class GetExternalChannelsOutput(pydantic.BaseModel):
|
||||
external_channels: list[ExternalChannelOutput]
|
||||
|
||||
|
||||
class CreateExternalChannelInput(pydantic.BaseModel):
|
||||
telegram_id: int
|
||||
title: str
|
||||
username: str | None = None
|
||||
description: str | None = None
|
||||
subscribers_count: int | None = None
|
||||
target_channel_ids: list[uuid.UUID]
|
||||
|
||||
|
||||
class DeleteExternalChannelInput(pydantic.BaseModel):
|
||||
channel_id: uuid.UUID
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
|
||||
|
||||
class UpdateExternalChannelInput(pydantic.BaseModel):
|
||||
title: str | None = None
|
||||
username: str | None = None
|
||||
description: str | None = None
|
||||
subscribers_count: int | None = None
|
||||
|
||||
|
||||
class UpdateExternalChannelLinksInput(pydantic.BaseModel):
|
||||
add_target_channel_ids: list[uuid.UUID] = []
|
||||
remove_target_channel_ids: list[uuid.UUID] = []
|
||||
@@ -8,10 +8,10 @@ from src import domain
|
||||
|
||||
class PlacementOutput(pydantic.BaseModel):
|
||||
id: uuid.UUID
|
||||
target_channel_id: uuid.UUID
|
||||
target_channel_title: str
|
||||
external_channel_id: uuid.UUID
|
||||
external_channel_title: str
|
||||
project_id: uuid.UUID
|
||||
project_channel_title: str
|
||||
placement_channel_id: uuid.UUID
|
||||
placement_channel_title: str
|
||||
creative_id: uuid.UUID
|
||||
creative_name: str
|
||||
placement_date: datetime.datetime
|
||||
@@ -31,8 +31,8 @@ class PlacementOutput(pydantic.BaseModel):
|
||||
class GetPlacementsInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
target_channel_id: uuid.UUID | None = None
|
||||
external_channel_id: uuid.UUID | None = None
|
||||
project_id: uuid.UUID | None = None
|
||||
placement_channel_id: uuid.UUID | None = None
|
||||
creative_id: uuid.UUID | None = None
|
||||
include_archived: bool = False
|
||||
|
||||
@@ -48,8 +48,8 @@ class GetPlacementInput(pydantic.BaseModel):
|
||||
|
||||
|
||||
class CreatePlacementInput(pydantic.BaseModel):
|
||||
target_channel_id: uuid.UUID
|
||||
external_channel_id: uuid.UUID
|
||||
project_id: uuid.UUID
|
||||
placement_channel_id: uuid.UUID
|
||||
creative_id: uuid.UUID
|
||||
placement_date: datetime.datetime
|
||||
cost: float | None = None
|
||||
|
||||
@@ -2,7 +2,7 @@ import uuid
|
||||
|
||||
import pydantic
|
||||
|
||||
from src.domain.target_channel import TargetChannelStatus
|
||||
from src.domain.project import ProjectStatus
|
||||
|
||||
|
||||
class ChannelBotPermissions(pydantic.BaseModel):
|
||||
@@ -17,7 +17,7 @@ class ChannelBotPermissions(pydantic.BaseModel):
|
||||
can_pin_messages: bool | None = None
|
||||
|
||||
|
||||
class ConnectTargetChanInput(pydantic.BaseModel):
|
||||
class ConnectProjectInput(pydantic.BaseModel):
|
||||
telegram_id: int
|
||||
title: str
|
||||
username: str | None
|
||||
@@ -25,31 +25,29 @@ class ConnectTargetChanInput(pydantic.BaseModel):
|
||||
bot_permissions: ChannelBotPermissions
|
||||
|
||||
|
||||
class ConnectTargetChanOutput(pydantic.BaseModel):
|
||||
class ProjectOutput(pydantic.BaseModel):
|
||||
id: uuid.UUID
|
||||
telegram_id: int
|
||||
title: str
|
||||
username: str | None
|
||||
status: TargetChannelStatus
|
||||
# Deprecated: используйте status. Оставлено для обратной совместимости с фронтендом
|
||||
is_active: bool
|
||||
status: ProjectStatus
|
||||
|
||||
|
||||
class GetUserTargetChansInput(pydantic.BaseModel):
|
||||
class GetWorkspaceProjectsInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
|
||||
|
||||
class GetUserTargetChansOutput(pydantic.BaseModel):
|
||||
target_channels: list[ConnectTargetChanOutput]
|
||||
class GetWorkspaceProjectsOutput(pydantic.BaseModel):
|
||||
projects: list[ProjectOutput]
|
||||
|
||||
|
||||
class DisconnectTargetChanByTgIdInput(pydantic.BaseModel):
|
||||
class DisconnectProjectByTgIdInput(pydantic.BaseModel):
|
||||
telegram_id: int
|
||||
user_telegram_id: int
|
||||
|
||||
|
||||
class UpdateTargetChanPermissionsInput(pydantic.BaseModel):
|
||||
class UpdateProjectPermissionsInput(pydantic.BaseModel):
|
||||
telegram_id: int
|
||||
permissions: ChannelBotPermissions
|
||||
chat_title: str
|
||||
38
src/dto/purchase_plan.py
Normal file
38
src/dto/purchase_plan.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import uuid
|
||||
|
||||
import pydantic
|
||||
|
||||
from src.domain.purchase_plan import PurchasePlanChannelStatus
|
||||
|
||||
from .channel import ChannelOutput
|
||||
|
||||
|
||||
class PurchasePlanChannelOutput(pydantic.BaseModel):
|
||||
id: uuid.UUID
|
||||
status: PurchasePlanChannelStatus
|
||||
planned_cost: float | None = None
|
||||
comment: str | None = None
|
||||
channel: ChannelOutput
|
||||
|
||||
|
||||
class GetPurchasePlanChannelsInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
project_id: uuid.UUID
|
||||
|
||||
|
||||
class GetPurchasePlanChannelsOutput(pydantic.BaseModel):
|
||||
channels: list[PurchasePlanChannelOutput]
|
||||
|
||||
|
||||
class AttachChannelToPurchasePlanInput(pydantic.BaseModel):
|
||||
username: str
|
||||
status: PurchasePlanChannelStatus | None = None
|
||||
planned_cost: float | None = None
|
||||
comment: str | None = None
|
||||
|
||||
|
||||
class UpdatePurchasePlanChannelInput(pydantic.BaseModel):
|
||||
status: PurchasePlanChannelStatus | None = None
|
||||
planned_cost: float | None = None
|
||||
comment: str | None = None
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
28
src/usecase/channel/get_channels.py
Normal file
28
src/usecase/channel/get_channels.py
Normal 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
|
||||
]
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
]
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
26
src/usecase/project/disconnect_project_by_tg_id.py
Normal file
26
src/usecase/project/disconnect_project_by_tg_id.py
Normal 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)
|
||||
27
src/usecase/project/get_workspace_projects.py
Normal file
27
src/usecase/project/get_workspace_projects.py
Normal 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
|
||||
]
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
@@ -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Боту убрали необходимые права:\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,
|
||||
)
|
||||
67
src/usecase/purchase_plan/attach_channel_to_purchase_plan.py
Normal file
67
src/usecase/purchase_plan/attach_channel_to_purchase_plan.py
Normal 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,
|
||||
),
|
||||
)
|
||||
43
src/usecase/purchase_plan/get_purchase_plan_channels.py
Normal file
43
src/usecase/purchase_plan/get_purchase_plan_channels.py
Normal 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
|
||||
]
|
||||
)
|
||||
33
src/usecase/purchase_plan/remove_purchase_plan_channel.py
Normal file
33
src/usecase/purchase_plan/remove_purchase_plan_channel.py
Normal 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)
|
||||
71
src/usecase/purchase_plan/update_purchase_plan_channel.py
Normal file
71
src/usecase/purchase_plan/update_purchase_plan_channel.py
Normal 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,
|
||||
),
|
||||
)
|
||||
@@ -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():
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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')
|
||||
@@ -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
|
||||
]
|
||||
)
|
||||
Reference in New Issue
Block a user