change domen
This commit is contained in:
@@ -3,7 +3,6 @@ import typing
|
||||
from dataclasses import dataclass
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from aiogram.types import InlineKeyboardButton
|
||||
|
||||
@@ -13,8 +12,8 @@ from .analytics.get_channel_analytics import get_channel_analytics
|
||||
from .analytics.get_creatives_analytics import get_creatives_analytics
|
||||
from .analytics.get_placements_analytics import get_placements_analytics
|
||||
from .analytics.get_spending_analytics import get_spending_analytics
|
||||
from .auth.get_jwt_by_telegram_id import get_jwt_by_telegram_id
|
||||
from .auth.create_telegram_login_token import create_telegram_login_token
|
||||
from .auth.get_jwt_by_telegram_id import get_jwt_by_telegram_id
|
||||
from .auth.get_me import get_me
|
||||
from .auth.validate_login_token import validate_login_token
|
||||
from .channel.attach_channel_to_workspace import attach_channel_to_workspace
|
||||
@@ -25,22 +24,18 @@ from .creative.delete_creative import delete_creative
|
||||
from .creative.get_creative import get_creative
|
||||
from .creative.get_creatives import get_creatives
|
||||
from .creative.update_creative import update_creative
|
||||
from .placement.create_placement import create_placement
|
||||
from .placement.delete_placement import delete_placement
|
||||
from .placement.fetch_placement_post_cycle import fetch_placement_post_cycle
|
||||
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_project import get_project
|
||||
from .project.get_workspace_projects import get_workspace_projects
|
||||
from .project.tg_add_project import tg_add_project
|
||||
from .project.update_project_invite_link_type import update_project_invite_link_type
|
||||
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 .purchase.create_purchase import create_purchase
|
||||
from .purchase.get_purchase import get_purchase
|
||||
from .purchase.get_purchases import get_purchases
|
||||
from .subscription.handle_subscription import handle_subscription
|
||||
from .subscription.handle_unsubscription import handle_unsubscription
|
||||
from .views.get_views_history import get_views_history
|
||||
@@ -155,31 +150,37 @@ class Database(typing.Protocol):
|
||||
self, workspace_id: UUID, allowed_project_ids: set[UUID] | None = None
|
||||
) -> list[domain.Project]: ...
|
||||
|
||||
async def get_active_purchase_plan(self, project_id: UUID) -> domain.PurchasePlan | None: ...
|
||||
async def get_active_purchase(self, project_id: UUID, creative_id: UUID) -> domain.Purchase | None: ...
|
||||
|
||||
async def get_purchase_plan(self, workspace_id: UUID, plan_id: UUID) -> domain.PurchasePlan | None: ...
|
||||
async def get_purchase(self, workspace_id: UUID, purchase_id: UUID) -> domain.Purchase | None: ...
|
||||
|
||||
async def create_purchase_plan(self, plan: domain.PurchasePlan) -> None: ...
|
||||
async def get_project_purchases(self, workspace_id: UUID, project_id: UUID) -> list[domain.Purchase]: ...
|
||||
|
||||
async def get_purchase_plan_channels(self, plan_id: UUID) -> list[domain.PurchasePlanChannel]: ...
|
||||
async def create_purchase(self, purchase: domain.Purchase) -> None: ...
|
||||
|
||||
async def get_purchase_plan_channel(
|
||||
self, plan_channel_id: UUID, plan_id: UUID
|
||||
) -> domain.PurchasePlanChannel | None: ...
|
||||
async def get_purchase_channels(self, purchase_id: UUID) -> list[domain.PurchaseChannel]: ...
|
||||
|
||||
async def add_channel_to_purchase_plan(
|
||||
async def get_purchase_channel(
|
||||
self, purchase_channel_id: UUID, purchase_id: UUID
|
||||
) -> domain.PurchaseChannel | None: ...
|
||||
|
||||
async def get_purchase_channels_by_project(self, project_id: UUID) -> list[domain.PurchaseChannel]: ...
|
||||
|
||||
async def add_channel_to_purchase(
|
||||
self,
|
||||
plan_id: UUID,
|
||||
purchase_id: UUID,
|
||||
channel_id: UUID,
|
||||
invite_link: str,
|
||||
invite_link_type: domain.purchase.InviteLinkType,
|
||||
*,
|
||||
status: domain.PurchasePlanChannelStatus | None = None,
|
||||
status: domain.PurchaseChannelStatus | None = None,
|
||||
planned_cost: float | None = None,
|
||||
comment: str | None = None,
|
||||
) -> domain.PurchasePlanChannel: ...
|
||||
) -> domain.PurchaseChannel: ...
|
||||
|
||||
async def update_purchase_plan_channel(self, plan_channel: domain.PurchasePlanChannel) -> None: ...
|
||||
async def update_purchase_channel(self, purchase_channel: domain.PurchaseChannel) -> None: ...
|
||||
|
||||
async def remove_channel_from_purchase_plan(self, plan_id: UUID, channel_id: UUID) -> None: ...
|
||||
async def remove_channel_from_purchase(self, purchase_id: UUID, channel_id: UUID) -> None: ...
|
||||
|
||||
async def get_creative(self, workspace_id: UUID, creative_id: UUID) -> domain.Creative | None: ...
|
||||
|
||||
@@ -197,10 +198,6 @@ class Database(typing.Protocol):
|
||||
|
||||
async def get_placement(self, workspace_id: UUID, placement_id: UUID) -> domain.Placement | None: ...
|
||||
|
||||
async def create_placement(self, placement: domain.Placement) -> None: ...
|
||||
|
||||
async def update_placement(self, placement: domain.Placement) -> None: ...
|
||||
|
||||
async def get_workspace_placements(
|
||||
self,
|
||||
workspace_id: UUID,
|
||||
@@ -264,14 +261,6 @@ class Database(typing.Protocol):
|
||||
|
||||
async def get_latest_views_data_batch(self, post_ids: list[UUID]) -> dict[UUID, tuple[int, datetime.datetime]]: ...
|
||||
|
||||
async def get_telegram_state(self, telegram_id: int) -> domain.TelegramState | None: ...
|
||||
|
||||
async def set_telegram_state(
|
||||
self, telegram_id: int, state: domain.TelegramStateEnum, context: dict[str, typing.Any]
|
||||
) -> domain.TelegramState: ...
|
||||
|
||||
async def clear_telegram_state(self, telegram_id: int) -> None: ...
|
||||
|
||||
|
||||
class TelegramBotWriter(typing.Protocol):
|
||||
async def send_message(self, text: str, chat_id: int) -> None: ...
|
||||
@@ -362,14 +351,14 @@ 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
|
||||
async def ensure_active_purchase(self, project: domain.Project, creative: domain.Creative) -> domain.Purchase:
|
||||
purchase = await self.database.get_active_purchase(project.id, creative.id)
|
||||
if purchase:
|
||||
return purchase
|
||||
|
||||
plan = domain.PurchasePlan(workspace_id=project.workspace_id, project_id=project.id)
|
||||
await self.database.create_purchase_plan(plan)
|
||||
return plan
|
||||
purchase = domain.Purchase(workspace_id=project.workspace_id, project_id=project.id, creative_id=creative.id)
|
||||
await self.database.create_purchase(purchase)
|
||||
return purchase
|
||||
|
||||
validate_login_token = validate_login_token
|
||||
create_telegram_login_token = create_telegram_login_token
|
||||
@@ -384,10 +373,9 @@ class Usecase:
|
||||
get_channels = get_channels
|
||||
get_channel = get_channel
|
||||
attach_channel_to_workspace = attach_channel_to_workspace
|
||||
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
|
||||
create_purchase = create_purchase
|
||||
get_purchases = get_purchases
|
||||
get_purchase = get_purchase
|
||||
get_creatives = get_creatives
|
||||
get_creative = get_creative
|
||||
create_creative = create_creative
|
||||
@@ -395,9 +383,6 @@ class Usecase:
|
||||
delete_creative = delete_creative
|
||||
get_placements = get_placements
|
||||
get_placement = get_placement
|
||||
create_placement = create_placement
|
||||
update_placement = update_placement
|
||||
delete_placement = delete_placement
|
||||
fetch_placement_post_cycle = fetch_placement_post_cycle
|
||||
handle_subscription = handle_subscription
|
||||
handle_unsubscription = handle_unsubscription
|
||||
|
||||
@@ -25,9 +25,8 @@ async def get_channel_analytics(
|
||||
if not project:
|
||||
raise domain.ProjectNotFound(input.project_id)
|
||||
context.ensure_project_permission(domain.PermissionKey.ANALYTICS_READ, 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]
|
||||
purchase_channels = await self.database.get_purchase_channels_by_project(project.id)
|
||||
channels = [pc.channel for pc in purchase_channels]
|
||||
allowed_project_filter = None
|
||||
else:
|
||||
allowed_project_filter = allowed_project_ids
|
||||
|
||||
@@ -61,7 +61,7 @@ async def get_placements_analytics(
|
||||
placement_channel_title=p.placement_channel.title,
|
||||
creative_id=p.creative_id,
|
||||
creative_name=p.creative.name,
|
||||
placement_date=p.placement_date,
|
||||
placement_date=p.wanted_placement_date,
|
||||
cost=p.cost,
|
||||
subscriptions_count=subscriptions_counts.get(p.id, 0),
|
||||
views_count=views_map[p.post.id][0] if p.post and p.post.id in views_map else None,
|
||||
|
||||
@@ -56,8 +56,8 @@ async def get_spending_analytics(
|
||||
filtered = [
|
||||
p
|
||||
for p in placements
|
||||
if (not input.date_from or p.placement_date >= input.date_from)
|
||||
and (not input.date_to or p.placement_date <= input.date_to)
|
||||
if (not input.date_from or p.wanted_placement_date >= input.date_from)
|
||||
and (not input.date_to or p.wanted_placement_date <= input.date_to)
|
||||
]
|
||||
|
||||
total_cost = 0.0
|
||||
@@ -82,7 +82,7 @@ async def get_spending_analytics(
|
||||
period_data: dict[str, PeriodData] = defaultdict(PeriodData)
|
||||
|
||||
for p in filtered:
|
||||
period = _format_period(p.placement_date, input.grouping)
|
||||
period = _format_period(p.wanted_placement_date, input.grouping)
|
||||
pd = period_data[period]
|
||||
|
||||
if p.cost is not None:
|
||||
|
||||
@@ -41,12 +41,10 @@ async def attach_channel_to_workspace(
|
||||
await self.database.create_project(project)
|
||||
log.info('Project created for channel %s in workspace %s', channel.id, workspace.id)
|
||||
|
||||
await self.ensure_active_purchase_plan(project)
|
||||
|
||||
return dto.ProjectOutput(
|
||||
id=project.id,
|
||||
telegram_id=channel.telegram_id,
|
||||
title=channel.title,
|
||||
username=channel.username,
|
||||
status=project.status,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import domain.purchase
|
||||
from src import domain, dto
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -35,7 +36,7 @@ async def create_placement(
|
||||
raise domain.CreativeNotFound(input.creative_id)
|
||||
|
||||
# Берем тип ссылки из настроек проекта
|
||||
requires_approval = project.invite_link_type == domain.InviteLinkType.APPROVAL
|
||||
requires_approval = project.purchase_invite_type_default == domain.purchase.InviteLinkType.APPROVAL
|
||||
|
||||
if project.channel.telegram_id is None:
|
||||
raise domain.ChannelNotFound(project.channel.id)
|
||||
@@ -64,11 +65,11 @@ async def create_placement(
|
||||
placement_channel_title=placement_channel.title,
|
||||
creative_id=placement.creative_id,
|
||||
creative_name=creative.name,
|
||||
placement_date=placement.placement_date,
|
||||
placement_date=placement.wanted_placement_date,
|
||||
cost=placement.cost,
|
||||
comment=placement.comment,
|
||||
ad_post_url=None,
|
||||
invite_link_type=project.invite_link_type,
|
||||
invite_link_type=project.purchase_invite_type_default,
|
||||
invite_link=placement.invite_link,
|
||||
status=placement.status,
|
||||
subscriptions_count=0,
|
||||
|
||||
@@ -12,52 +12,56 @@ log = logging.getLogger(__name__)
|
||||
async def fetch_placement_post_cycle(self: 'Usecase', interval_seconds: int) -> None:
|
||||
log.debug('Starting fetch_placement_post_cycle')
|
||||
|
||||
placements_without_post = (
|
||||
await domain.Placement.filter(
|
||||
post_id__isnull=True,
|
||||
)
|
||||
.prefetch_related('placement_channel')
|
||||
purchase_channels = (
|
||||
await domain.PurchaseChannel.filter(purchase__status=domain.PurchaseStatus.ACTIVE)
|
||||
.prefetch_related('channel', 'purchase')
|
||||
.all()
|
||||
)
|
||||
|
||||
if not placements_without_post:
|
||||
log.debug('No placements without posts found')
|
||||
if not purchase_channels:
|
||||
log.debug('No active purchase channels found')
|
||||
return
|
||||
|
||||
log.info(f'Found {len(placements_without_post)} placements without posts')
|
||||
created_count = 0
|
||||
|
||||
updated_count = 0
|
||||
|
||||
for placement in placements_without_post:
|
||||
try:
|
||||
matching_post = await domain.Post.filter(
|
||||
channel_id=placement.placement_channel_id,
|
||||
text__contains=placement.invite_link,
|
||||
deleted_from_channel_at__isnull=True,
|
||||
).first()
|
||||
|
||||
if matching_post:
|
||||
# Validate channel consistency
|
||||
if matching_post.channel_id != placement.placement_channel_id:
|
||||
log.error(
|
||||
'Post %s channel_id mismatch: expected %s, got %s. Skipping placement %s',
|
||||
matching_post.id,
|
||||
placement.placement_channel_id,
|
||||
matching_post.channel_id,
|
||||
placement.id,
|
||||
)
|
||||
continue
|
||||
|
||||
placement.post = matching_post
|
||||
await placement.save()
|
||||
updated_count += 1
|
||||
|
||||
log.info(
|
||||
f'Linked post {matching_post.id} (message_id={matching_post.message_id}) '
|
||||
f'to placement {placement.id}'
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(f'Error processing placement {placement.id}: {e}')
|
||||
for purchase_channel in purchase_channels:
|
||||
if purchase_channel.channel is None or purchase_channel.purchase is None:
|
||||
log.warning('Purchase channel %s missing channel or purchase, skipping', purchase_channel.id)
|
||||
continue
|
||||
|
||||
log.info(f'Fetch placement post cycle completed. Updated {updated_count} placements')
|
||||
posts = await domain.Post.filter(
|
||||
channel_id=purchase_channel.channel_id,
|
||||
text__contains=purchase_channel.invite_link,
|
||||
deleted_from_channel_at__isnull=True,
|
||||
).all()
|
||||
|
||||
for post in posts:
|
||||
existing = await domain.Placement.filter(post_id=post.id).first()
|
||||
if existing:
|
||||
continue
|
||||
|
||||
placement = domain.Placement(
|
||||
wanted_placement_date=post.created_at,
|
||||
cost=None,
|
||||
comment=None,
|
||||
invite_link=purchase_channel.invite_link,
|
||||
workspace_id=purchase_channel.purchase.workspace_id,
|
||||
project_id=purchase_channel.purchase.project_id,
|
||||
creative_id=purchase_channel.purchase.creative_id,
|
||||
placement_channel_id=purchase_channel.channel_id,
|
||||
purchase_id=purchase_channel.purchase_id,
|
||||
purchase_channel_id=purchase_channel.id,
|
||||
post=post,
|
||||
status=domain.PlacementStatus.ACTIVE,
|
||||
)
|
||||
|
||||
await self.database.create_placement(placement)
|
||||
created_count += 1
|
||||
log.info(
|
||||
'Created placement %s for purchase_channel %s from post %s',
|
||||
placement.id,
|
||||
purchase_channel.id,
|
||||
post.id,
|
||||
)
|
||||
|
||||
log.info('Fetch placement post cycle completed. Created %s placements', created_count)
|
||||
|
||||
@@ -33,11 +33,11 @@ async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.Pl
|
||||
placement_channel_title=placement.placement_channel.title,
|
||||
creative_id=placement.creative_id,
|
||||
creative_name=placement.creative.name,
|
||||
placement_date=placement.placement_date,
|
||||
placement_date=placement.wanted_placement_date,
|
||||
cost=placement.cost,
|
||||
comment=placement.comment,
|
||||
ad_post_url=ad_post_url,
|
||||
invite_link_type=placement.project.invite_link_type,
|
||||
invite_link_type=placement.project.purchase_invite_type_default,
|
||||
invite_link=placement.invite_link,
|
||||
status=placement.status,
|
||||
subscriptions_count=subscriptions_count,
|
||||
|
||||
@@ -42,11 +42,11 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> list
|
||||
placement_channel_title=placement.placement_channel.title,
|
||||
creative_id=placement.creative_id,
|
||||
creative_name=placement.creative.name,
|
||||
placement_date=placement.placement_date,
|
||||
placement_date=placement.wanted_placement_date,
|
||||
cost=placement.cost,
|
||||
comment=placement.comment,
|
||||
ad_post_url=ad_post_url,
|
||||
invite_link_type=placement.project.invite_link_type,
|
||||
invite_link_type=placement.project.purchase_invite_type_default,
|
||||
invite_link=placement.invite_link,
|
||||
status=placement.status,
|
||||
subscriptions_count=subscriptions_counts.get(placement.id, 0),
|
||||
|
||||
@@ -27,7 +27,7 @@ async def update_placement(
|
||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_WRITE, placement.project_id)
|
||||
|
||||
if input.placement_date is not None:
|
||||
placement.placement_date = input.placement_date
|
||||
placement.wanted_placement_date = input.placement_date
|
||||
if input.cost is not None:
|
||||
placement.cost = input.cost
|
||||
if input.comment is not None:
|
||||
@@ -49,11 +49,11 @@ async def update_placement(
|
||||
placement_channel_title=placement.placement_channel.title,
|
||||
creative_id=placement.creative_id,
|
||||
creative_name=placement.creative.name,
|
||||
placement_date=placement.placement_date,
|
||||
placement_date=placement.wanted_placement_date,
|
||||
cost=placement.cost,
|
||||
comment=placement.comment,
|
||||
ad_post_url=ad_post_url,
|
||||
invite_link_type=placement.project.invite_link_type,
|
||||
invite_link_type=placement.project.purchase_invite_type_default,
|
||||
invite_link=placement.invite_link,
|
||||
status=placement.status,
|
||||
subscriptions_count=subscriptions_count,
|
||||
|
||||
@@ -20,5 +20,5 @@ async def get_project(self: 'Usecase', input: dto.GetProjectInput) -> dto.Projec
|
||||
title=project.channel.title,
|
||||
username=project.channel.username,
|
||||
status=project.status,
|
||||
invite_link_type=project.invite_link_type,
|
||||
purchase_invite_type_default=project.purchase_invite_type_default,
|
||||
)
|
||||
|
||||
@@ -6,9 +6,7 @@ if TYPE_CHECKING:
|
||||
from .. import Usecase
|
||||
|
||||
|
||||
async def get_workspace_projects(
|
||||
self: 'Usecase', input: dto.GetWorkspaceProjectsInput
|
||||
) -> list[dto.ProjectOutput]:
|
||||
async def get_workspace_projects(self: 'Usecase', input: dto.GetWorkspaceProjectsInput) -> list[dto.ProjectOutput]:
|
||||
context = await self.ensure_workspace_permission(
|
||||
input.workspace_id, input.user_id, domain.PermissionKey.PROJECTS_READ
|
||||
)
|
||||
@@ -24,7 +22,7 @@ async def get_workspace_projects(
|
||||
title=project.channel.title,
|
||||
username=project.channel.username,
|
||||
status=project.status,
|
||||
invite_link_type=project.invite_link_type,
|
||||
purchase_invite_type_default=project.purchase_invite_type_default,
|
||||
)
|
||||
for project in projects
|
||||
]
|
||||
|
||||
@@ -104,8 +104,6 @@ async def tg_add_project(self: 'Usecase', input: dto.ConnectProjectInput) -> dto
|
||||
)
|
||||
await self.database.create_project(project)
|
||||
|
||||
await self.ensure_active_purchase_plan(project)
|
||||
|
||||
log.info(
|
||||
'Project for channel %s connected/updated successfully in workspace %s by user %s',
|
||||
input.telegram_id,
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from src import domain
|
||||
from src import domain, dto
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Usecase
|
||||
@@ -14,9 +14,9 @@ async def update_project_invite_link_type(
|
||||
self: 'Usecase',
|
||||
workspace_id: uuid.UUID,
|
||||
project_id: uuid.UUID,
|
||||
invite_link_type: domain.InviteLinkType,
|
||||
purchase_invite_type_default: domain.purchase.InviteLinkType,
|
||||
user_id: uuid.UUID,
|
||||
) -> domain.Project:
|
||||
) -> dto.ProjectOutput:
|
||||
await self.ensure_workspace_permission(
|
||||
workspace_id,
|
||||
user_id,
|
||||
@@ -24,19 +24,19 @@ async def update_project_invite_link_type(
|
||||
for_project_id=project_id,
|
||||
)
|
||||
|
||||
async with self.database.transaction():
|
||||
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||
if not project:
|
||||
raise domain.ProjectNotFound(project_id)
|
||||
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||
if not project:
|
||||
raise domain.ProjectNotFound(project_id)
|
||||
|
||||
project.invite_link_type = invite_link_type
|
||||
await project.save()
|
||||
project.purchase_invite_type_default = purchase_invite_type_default
|
||||
|
||||
log.info(
|
||||
'Project %s invite_link_type updated to %s by user %s',
|
||||
project_id,
|
||||
invite_link_type,
|
||||
user_id,
|
||||
)
|
||||
await self.database.update_project(project)
|
||||
|
||||
return project
|
||||
return dto.ProjectOutput(
|
||||
id=project.id,
|
||||
telegram_id=project.channel.telegram_id,
|
||||
title=project.channel.title,
|
||||
username=project.channel.username,
|
||||
status=project.status,
|
||||
purchase_invite_type_default=project.purchase_invite_type_default,
|
||||
)
|
||||
|
||||
117
src/usecase/purchase/create_purchase.py
Normal file
117
src/usecase/purchase/create_purchase.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import logging
|
||||
import random
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from src import domain, dto
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Usecase
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _build_purchase_output(
|
||||
purchase: domain.Purchase, purchase_channels: list[domain.PurchaseChannel]
|
||||
) -> dto.PurchaseOutput:
|
||||
channels_output: list[dto.PurchaseChannelOutput] = []
|
||||
for purchase_channel in purchase_channels:
|
||||
channel = purchase_channel.channel
|
||||
if channel is None:
|
||||
log.warning('Purchase channel %s has no channel prefetched', purchase_channel.id)
|
||||
continue
|
||||
channels_output.append(
|
||||
dto.PurchaseChannelOutput(
|
||||
id=purchase_channel.id,
|
||||
status=purchase_channel.status,
|
||||
planned_cost=purchase_channel.planned_cost,
|
||||
comment=purchase_channel.comment,
|
||||
invite_link=purchase_channel.invite_link,
|
||||
invite_link_type=purchase_channel.invite_link_type,
|
||||
channel=dto.ChannelOutput(
|
||||
id=channel.id,
|
||||
telegram_id=channel.telegram_id,
|
||||
title=channel.title,
|
||||
username=channel.username,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return dto.PurchaseOutput(
|
||||
id=purchase.id,
|
||||
status=purchase.status,
|
||||
creative_id=purchase.creative_id,
|
||||
channels=channels_output,
|
||||
)
|
||||
|
||||
|
||||
async def create_purchase(
|
||||
self: 'Usecase',
|
||||
project_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
input: dto.CreatePurchaseInput,
|
||||
) -> dto.PurchaseOutput:
|
||||
context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE)
|
||||
|
||||
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||
if not project:
|
||||
raise domain.ProjectNotFound(project_id)
|
||||
|
||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_WRITE, project.id)
|
||||
|
||||
creative = await self.database.get_creative(workspace_id, input.creative_id)
|
||||
if not creative or creative.project_id != project.id:
|
||||
raise domain.CreativeNotFound(input.creative_id)
|
||||
|
||||
existing_purchase = await self.database.get_active_purchase(project.id, creative.id)
|
||||
if existing_purchase:
|
||||
channels = await self.database.get_purchase_channels(existing_purchase.id)
|
||||
return _build_purchase_output(existing_purchase, channels)
|
||||
|
||||
purchase = domain.Purchase(workspace_id=workspace_id, project_id=project.id, creative_id=creative.id)
|
||||
await self.database.create_purchase(purchase)
|
||||
|
||||
if project.channel.telegram_id is None:
|
||||
raise domain.ChannelNotFound(project.channel.id)
|
||||
|
||||
invite_link_type = project.purchase_invite_type_default
|
||||
requires_approval = invite_link_type == domain.purchase.InviteLinkType.APPROVAL
|
||||
|
||||
purchase_channels: list[domain.PurchaseChannel] = []
|
||||
|
||||
for channel_input in input.channels:
|
||||
channel = await self.database.get_channel(username=channel_input.username)
|
||||
|
||||
if not channel:
|
||||
channel = domain.Channel(
|
||||
username=channel_input.username,
|
||||
telegram_id=random.randint(1, 10000),
|
||||
title='parser_response.title',
|
||||
)
|
||||
await self.database.create_channel(channel)
|
||||
log.info('Created channel @%s with telegram_id=%s', channel_input.username, channel.telegram_id)
|
||||
|
||||
invite_link = await self.telegram_bot.create_chat_invite_link(project.channel.telegram_id, requires_approval)
|
||||
|
||||
purchase_channel = await self.database.add_channel_to_purchase(
|
||||
purchase.id,
|
||||
channel.id,
|
||||
invite_link=invite_link,
|
||||
invite_link_type=invite_link_type,
|
||||
status=channel_input.status,
|
||||
planned_cost=channel_input.planned_cost,
|
||||
comment=channel_input.comment,
|
||||
)
|
||||
purchase_channel.channel = channel
|
||||
purchase_channels.append(purchase_channel)
|
||||
|
||||
log.info(
|
||||
'Purchase %s created for project %s (creative %s) with %s channels',
|
||||
purchase.id,
|
||||
project.id,
|
||||
creative.id,
|
||||
len(purchase_channels),
|
||||
)
|
||||
|
||||
return _build_purchase_output(purchase, purchase_channels)
|
||||
30
src/usecase/purchase/get_purchase.py
Normal file
30
src/usecase/purchase/get_purchase.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from src import domain, dto
|
||||
from .create_purchase import _build_purchase_output
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Usecase
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_purchase(self: 'Usecase', input: dto.GetPurchaseInput) -> dto.PurchaseOutput:
|
||||
context = await self.ensure_workspace_permission(
|
||||
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
|
||||
)
|
||||
|
||||
project = await self.database.get_project(input.workspace_id, project_id=input.project_id)
|
||||
if not project:
|
||||
raise domain.ProjectNotFound(input.project_id)
|
||||
|
||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, project.id)
|
||||
|
||||
purchase = await self.database.get_purchase(input.workspace_id, input.purchase_id)
|
||||
if not purchase or purchase.project_id != project.id:
|
||||
raise domain.PurchaseNotFound(input.purchase_id)
|
||||
|
||||
channels = await self.database.get_purchase_channels(purchase.id)
|
||||
|
||||
return _build_purchase_output(purchase, channels)
|
||||
32
src/usecase/purchase/get_purchases.py
Normal file
32
src/usecase/purchase/get_purchases.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from src import domain, dto
|
||||
from .create_purchase import _build_purchase_output
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Usecase
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_purchases(self: 'Usecase', input: dto.GetPurchasesInput) -> list[dto.PurchaseOutput]:
|
||||
context = await self.ensure_workspace_permission(
|
||||
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
|
||||
)
|
||||
|
||||
project = await self.database.get_project(input.workspace_id, project_id=input.project_id)
|
||||
if not project:
|
||||
raise domain.ProjectNotFound(input.project_id)
|
||||
|
||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, project.id)
|
||||
|
||||
purchases = await self.database.get_project_purchases(input.workspace_id, project.id)
|
||||
log.debug('Fetched %s purchases for project %s', len(purchases), project.id)
|
||||
|
||||
output: list[dto.PurchaseOutput] = []
|
||||
for purchase in purchases:
|
||||
channels = await self.database.get_purchase_channels(purchase.id)
|
||||
output.append(_build_purchase_output(purchase, channels))
|
||||
|
||||
return output
|
||||
@@ -1,67 +0,0 @@
|
||||
import logging
|
||||
import random
|
||||
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:
|
||||
context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE)
|
||||
|
||||
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||
if not project:
|
||||
raise domain.ProjectNotFound(project_id)
|
||||
|
||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_WRITE, project.id)
|
||||
|
||||
plan = await self.ensure_active_purchase_plan(project)
|
||||
|
||||
channel = await self.database.get_channel(username=input.username)
|
||||
|
||||
if not channel:
|
||||
# parser_response = await self.parser.fetch_telegram_channel(input.username)
|
||||
# if not parser_response:
|
||||
# raise domain.TelegramChannelNotFound(input.username)
|
||||
|
||||
channel = domain.Channel(
|
||||
username=input.username,
|
||||
telegram_id=random.randint(1, 10000),
|
||||
title='parser_response.title',
|
||||
)
|
||||
await self.database.create_channel(channel)
|
||||
log.info('Created channel @%s with telegram_id=%s', input.username, channel.telegram_id)
|
||||
|
||||
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,
|
||||
),
|
||||
)
|
||||
@@ -1,44 +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_purchase_plan_channels(
|
||||
self: 'Usecase', input: dto.GetPurchasePlanChannelsInput
|
||||
) -> list[dto.PurchasePlanChannelOutput]:
|
||||
context = await self.ensure_workspace_permission(
|
||||
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
|
||||
)
|
||||
|
||||
project = await self.database.get_project(input.workspace_id, project_id=input.project_id)
|
||||
if not project:
|
||||
raise domain.ProjectNotFound(input.project_id)
|
||||
|
||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, 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.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,
|
||||
),
|
||||
)
|
||||
for plan_channel in plan_channels
|
||||
]
|
||||
@@ -1,35 +0,0 @@
|
||||
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:
|
||||
context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE)
|
||||
|
||||
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||
if not project:
|
||||
raise domain.ProjectNotFound(project_id)
|
||||
|
||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_WRITE, 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)
|
||||
@@ -1,72 +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_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:
|
||||
context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE)
|
||||
|
||||
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||
if not project:
|
||||
raise domain.ProjectNotFound(project_id)
|
||||
|
||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_WRITE, 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(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,
|
||||
),
|
||||
)
|
||||
Reference in New Issue
Block a user