feat: add workspace

This commit is contained in:
Artem Tsyrulnikov
2025-12-13 22:09:55 +03:00
parent 2fdd56c3e0
commit af32e2a507
55 changed files with 677 additions and 227 deletions

View File

@@ -22,6 +22,7 @@ from .creative.get_creative import get_creative
from .creative.get_creatives import get_creatives
from .creative.handle_creative_callback import handle_creative_callback
from .creative.handle_creative_text_input import handle_creative_text_input
from .creative.handle_creative_workspace_callback import handle_creative_workspace_callback
from .creative.start_creative_creation import start_creative_creation
from .creative.update_creative import update_creative
from .external_channel.create_external_channel import create_external_channel
@@ -41,7 +42,10 @@ from .target_channel.disconnect_target_chan_by_tg_id import disconnect_target_ch
from .target_channel.get_user_target_chans import get_user_target_chans
from .target_channel.update_target_chan_permissions import update_target_chan_permissions
from .views.get_views_history import get_views_history
from .views.update_views_manually import update_views_manually
from .workspace.create_workspace import create_workspace
from .workspace.delete_workspace import delete_workspace
from .workspace.get_workspaces import get_workspaces
from .workspace.update_workspace import update_workspace
class Database(typing.Protocol):
@@ -51,6 +55,24 @@ class Database(typing.Protocol):
async def create_user(self, user: domain.User) -> None: ...
async def create_workspace(self, workspace: domain.Workspace) -> None: ...
async def update_workspace(self, workspace: domain.Workspace) -> None: ...
async def delete_workspace(self, workspace_id: UUID) -> None: ...
async def add_user_to_workspace(self, workspace_id: UUID, user_id: UUID) -> None: ...
async def get_user_workspaces(self, user_id: UUID) -> list[domain.WorkspaceUser]: ...
async def get_workspace(self, workspace_id: UUID) -> domain.Workspace | None: ...
async def get_workspace_for_user(self, workspace_id: UUID, user_id: UUID) -> domain.Workspace | None: ...
async def get_default_workspace_for_user(self, user_id: UUID) -> domain.Workspace | None: ...
async def get_workspace_membership(self, workspace_id: UUID, user_id: UUID) -> domain.WorkspaceUser | None: ...
async def create_login_token(self, login_token: domain.LoginToken) -> None: ...
async def create_creative(self, creative: domain.Creative) -> None: ...
@@ -60,24 +82,28 @@ class Database(typing.Protocol):
async def mark_token_as_used(self, token: str) -> None: ...
async def get_target_channel(
self, user_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
self, workspace_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
) -> domain.TargetChannel | None: ...
async def get_target_channel_for_user_by_telegram(
self, user_id: UUID, telegram_id: int
) -> domain.TargetChannel | None: ...
async def upsert_target_channel(self, channel: domain.TargetChannel) -> None: ...
async def get_user_target_channels(self, user_id: UUID) -> list[domain.TargetChannel]: ...
async def get_workspace_target_channels(self, workspace_id: UUID) -> list[domain.TargetChannel]: ...
async def update_target_channel(self, channel: domain.TargetChannel) -> None: ...
async def get_external_channel(
self, user_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
self, workspace_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
) -> domain.ExternalChannel | None: ...
async def create_external_channel(self, channel: domain.ExternalChannel) -> None: ...
async def get_external_channels_for_target(self, target_channel_id: UUID) -> list[domain.ExternalChannel]: ...
async def get_user_external_channels(self, user_id: UUID) -> list[domain.ExternalChannel]: ...
async def get_workspace_external_channels(self, workspace_id: UUID) -> list[domain.ExternalChannel]: ...
async def add_external_channel_to_targets(
self, external_channel: domain.ExternalChannel, target_channel_ids: list[UUID]
@@ -89,25 +115,25 @@ class Database(typing.Protocol):
async def update_external_channel(self, channel: domain.ExternalChannel) -> None: ...
async def get_creative(self, user_id: UUID, creative_id: UUID) -> domain.Creative | 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_user_creatives(
self, user_id: UUID, target_channel_id: UUID | None = None, include_archived: bool = False
async def get_workspace_creatives(
self, workspace_id: UUID, target_channel_id: UUID | None = None, include_archived: bool = False
) -> list[domain.Creative]: ...
async def delete_creative(self, creative_id: UUID) -> None: ...
async def get_placement(self, user_id: UUID, placement_id: UUID) -> domain.Placement | None: ...
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_user_placements(
async def get_workspace_placements(
self,
user_id: UUID,
workspace_id: UUID,
target_channel_id: UUID | None = None,
external_channel_id: UUID | None = None,
creative_id: UUID | None = None,
@@ -181,6 +207,13 @@ class Usecase:
telegram_bot: TelegramBotWriter
jwt_encoder: JWTEncoder
async def ensure_workspace_access(self, workspace_id: UUID, user_id: UUID) -> domain.Workspace:
workspace = await self.database.get_workspace_for_user(workspace_id, user_id)
if not workspace:
raise domain.WorkspaceNotFound(workspace_id)
return workspace
validate_login_token = validate_login_token
telegram_login = telegram_login
telegram_start = telegram_start
@@ -201,6 +234,7 @@ class Usecase:
delete_creative = delete_creative
start_creative_creation = start_creative_creation
handle_creative_callback = handle_creative_callback
handle_creative_workspace_callback = handle_creative_workspace_callback
handle_creative_text_input = handle_creative_text_input
get_placements = get_placements
get_placement = get_placement
@@ -210,8 +244,11 @@ class Usecase:
handle_subscription = handle_subscription
handle_unsubscription = handle_unsubscription
get_views_history = get_views_history
update_views_manually = update_views_manually
get_placements_analytics = get_placements_analytics
get_creatives_analytics = get_creatives_analytics
get_external_channels_analytics = get_external_channels_analytics
get_spending_analytics = get_spending_analytics
get_workspaces = get_workspaces
create_workspace = create_workspace
update_workspace = update_workspace
delete_workspace = delete_workspace

View File

@@ -14,13 +14,19 @@ log = logging.getLogger(__name__)
async def get_creatives_analytics(
self: 'Usecase', input: dto.GetCreativesAnalyticsInput
) -> 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.user_id, 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)
creatives = await self.database.get_user_creatives(input.user_id, input.target_channel_id, include_archived=False)
placements = await self.database.get_user_placements(input.user_id, input.target_channel_id, include_archived=False)
creatives = await self.database.get_workspace_creatives(
input.workspace_id, input.target_channel_id, include_archived=False
)
placements = await self.database.get_workspace_placements(
input.workspace_id, input.target_channel_id, include_archived=False
)
@dataclass
class CreativeStats:

View File

@@ -14,15 +14,19 @@ log = logging.getLogger(__name__)
async def get_external_channels_analytics(
self: 'Usecase', input: dto.GetExternalChannelsAnalyticsInput
) -> dto.GetExternalChannelsAnalyticsOutput:
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.user_id, 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)
else:
external_channels = await self.database.get_user_external_channels(input.user_id)
external_channels = await self.database.get_workspace_external_channels(input.workspace_id)
placements = await self.database.get_user_placements(input.user_id, input.target_channel_id, include_archived=False)
placements = await self.database.get_workspace_placements(
input.workspace_id, input.target_channel_id, include_archived=False
)
@dataclass
class ChannelStats:

View File

@@ -24,12 +24,16 @@ def _calculate_cpm(cost: float | None, views: int | None) -> float | None:
async def get_placements_analytics(
self: 'Usecase', input: dto.GetPlacementsAnalyticsInput
) -> 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.user_id, 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)
placements = await self.database.get_user_placements(input.user_id, input.target_channel_id, include_archived=False)
placements = await self.database.get_workspace_placements(
input.workspace_id, input.target_channel_id, include_archived=False
)
result = [
dto.PlacementAnalyticsOutput(

View File

@@ -33,12 +33,16 @@ def _format_period(dt: 'datetime.datetime', grouping: dto.DateGrouping) -> str:
async def get_spending_analytics(
self: 'Usecase', input: dto.GetSpendingAnalyticsInput
) -> 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.user_id, 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)
placements = await self.database.get_user_placements(input.user_id, input.target_channel_id, include_archived=False)
placements = await self.database.get_workspace_placements(
input.workspace_id, input.target_channel_id, include_archived=False
)
filtered = [
p

View File

@@ -10,8 +10,12 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__)
async def create_creative(self: 'Usecase', input: dto.CreateCreativeInput, user_id: uuid.UUID) -> dto.CreativeOutput:
target_channel = await self.database.get_target_channel(user_id, channel_id=input.target_channel_id)
async def create_creative(
self: 'Usecase', input: dto.CreateCreativeInput, user_id: uuid.UUID, workspace_id: uuid.UUID
) -> 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)
@@ -21,9 +25,8 @@ async def create_creative(self: 'Usecase', input: dto.CreateCreativeInput, user_
text=input.text,
status=domain.CreativeStatus.ACTIVE,
target_channel_id=target_channel.id,
user_id=user_id,
workspace_id=workspace_id,
)
await self.database.create_creative(creative)
return dto.CreativeOutput(

View File

@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
async def delete_creative(self: 'Usecase', input: dto.DeleteCreativeInput) -> None:
creative = await self.database.get_creative(input.user_id, input.creative_id)
await self.ensure_workspace_access(input.workspace_id, input.user_id)
creative = await self.database.get_creative(input.workspace_id, input.creative_id)
if not creative:
log.warning('User %s attempted to delete unavailable creative %s', input.user_id, input.creative_id)
raise domain.CreativeNotFound(input.creative_id)

View File

@@ -10,9 +10,10 @@ log = logging.getLogger(__name__)
async def get_creative(self: 'Usecase', input: dto.GetCreativeInput) -> dto.CreativeOutput:
creative = await self.database.get_creative(input.user_id, input.creative_id)
await self.ensure_workspace_access(input.workspace_id, input.user_id)
creative = await self.database.get_creative(input.workspace_id, input.creative_id)
if not creative:
log.warning('User %s attempted to access unavailable creative %s', input.user_id, input.creative_id)
raise domain.CreativeNotFound(input.creative_id)
return dto.CreativeOutput(

View File

@@ -7,7 +7,11 @@ if TYPE_CHECKING:
async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> dto.GetCreativesOutput:
creatives = await self.database.get_user_creatives(input.user_id, input.target_channel_id, input.include_archived)
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
)
return dto.GetCreativesOutput(
creatives=[

View File

@@ -11,9 +11,15 @@ log = logging.getLogger(__name__)
async def update_creative(
self: 'Usecase', creative_id: uuid.UUID, input: dto.UpdateCreativeInput, user_id: uuid.UUID
self: 'Usecase',
creative_id: uuid.UUID,
input: dto.UpdateCreativeInput,
user_id: uuid.UUID,
workspace_id: uuid.UUID,
) -> dto.CreativeOutput:
creative = await self.database.get_creative(user_id, creative_id)
await self.ensure_workspace_access(workspace_id, user_id)
creative = await self.database.get_creative(workspace_id, creative_id)
if not creative:
log.warning('User %s attempted to update unavailable creative %s', user_id, creative_id)
raise domain.CreativeNotFound(creative_id)

View File

@@ -11,17 +11,18 @@ log = logging.getLogger(__name__)
async def create_external_channel(
self: 'Usecase', input: dto.CreateExternalChannelInput, user_id: uuid.UUID
self: 'Usecase', input: dto.CreateExternalChannelInput, user_id: uuid.UUID, workspace_id: uuid.UUID
) -> dto.ExternalChannelOutput:
existing_channel = await self.database.get_external_channel(user_id, telegram_id=input.telegram_id)
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(user_id, channel_id=target_id)
target_channel = await self.database.get_target_channel(workspace_id, channel_id=target_id)
if not target_channel:
log.warning('User %s attempted to add external channel to unavailable target %s', user_id, target_id)
raise domain.TargetChannelNotFound(target_id)
async with self.database.transaction():
@@ -31,7 +32,7 @@ async def create_external_channel(
username=input.username,
description=input.description,
subscribers_count=input.subscribers_count,
user_id=user_id,
workspace_id=workspace_id,
)
await self.database.create_external_channel(channel)

View File

@@ -10,9 +10,10 @@ log = logging.getLogger(__name__)
async def delete_external_channel(self: 'Usecase', input: dto.DeleteExternalChannelInput) -> None:
channel = await self.database.get_external_channel(input.user_id, channel_id=input.channel_id)
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:
log.warning('User %s attempted to delete unavailable external channel %s', input.user_id, input.channel_id)
raise domain.ExternalChannelNotFound(input.channel_id)
await self.database.delete_external_channel(input.channel_id)

View File

@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
async def get_external_channels(self: 'Usecase', input: dto.GetExternalChannelsInput) -> dto.GetExternalChannelsOutput:
target_channel = await self.database.get_target_channel(input.user_id, channel_id=input.target_channel_id)
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)

View File

@@ -11,9 +11,15 @@ log = logging.getLogger(__name__)
async def update_external_channel(
self: 'Usecase', channel_id: uuid.UUID, input: dto.UpdateExternalChannelInput, user_id: uuid.UUID
self: 'Usecase',
channel_id: uuid.UUID,
input: dto.UpdateExternalChannelInput,
user_id: uuid.UUID,
workspace_id: uuid.UUID,
) -> dto.ExternalChannelOutput:
external_channel = await self.database.get_external_channel(user_id, channel_id=channel_id)
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)

View File

@@ -11,22 +11,28 @@ log = logging.getLogger(__name__)
async def update_external_channel_links(
self: 'Usecase', channel_id: uuid.UUID, input: dto.UpdateExternalChannelLinksInput, user_id: uuid.UUID
self: 'Usecase',
channel_id: uuid.UUID,
input: dto.UpdateExternalChannelLinksInput,
user_id: uuid.UUID,
workspace_id: uuid.UUID,
) -> dto.ExternalChannelOutput:
external_channel = await self.database.get_external_channel(user_id, channel_id=channel_id)
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(user_id, channel_id=target_id)
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(channel_id, 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)

View File

@@ -10,16 +10,20 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__)
async def create_placement(self: 'Usecase', input: dto.CreatePlacementInput, user_id: uuid.UUID) -> dto.PlacementOutput:
target_channel = await self.database.get_target_channel(user_id, channel_id=input.target_channel_id)
async def create_placement(
self: 'Usecase', input: dto.CreatePlacementInput, user_id: uuid.UUID, workspace_id: uuid.UUID
) -> 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)
external_channel = await self.database.get_external_channel(user_id, channel_id=input.external_channel_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)
creative = await self.database.get_creative(user_id, input.creative_id)
creative = await self.database.get_creative(workspace_id, input.creative_id)
if not creative:
raise domain.CreativeNotFound(input.creative_id)
@@ -30,7 +34,7 @@ async def create_placement(self: 'Usecase', input: dto.CreatePlacementInput, use
target_channel_id=input.target_channel_id,
external_channel_id=input.external_channel_id,
creative_id=input.creative_id,
user_id=user_id,
workspace_id=workspace_id,
placement_date=input.placement_date,
cost=input.cost,
comment=input.comment,

View File

@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
async def delete_placement(self: 'Usecase', input: dto.DeletePlacementInput) -> None:
placement = await self.database.get_placement(input.user_id, input.placement_id)
await self.ensure_workspace_access(input.workspace_id, input.user_id)
placement = await self.database.get_placement(input.workspace_id, input.placement_id)
if not placement:
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
raise domain.PlacementNotFound(input.placement_id)
@@ -20,7 +22,7 @@ async def delete_placement(self: 'Usecase', input: dto.DeletePlacementInput) ->
return
async with self.database.transaction():
creative = await self.database.get_creative(input.user_id, placement.creative_id)
creative = await self.database.get_creative(input.workspace_id, placement.creative_id)
if creative and creative.placements_count > 0:
creative.placements_count -= 1
await self.database.update_creative(creative)

View File

@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.PlacementOutput:
placement = await self.database.get_placement(input.user_id, input.placement_id)
await self.ensure_workspace_access(input.workspace_id, input.user_id)
placement = await self.database.get_placement(input.workspace_id, input.placement_id)
if not placement:
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
raise domain.PlacementNotFound(input.placement_id)

View File

@@ -7,8 +7,10 @@ if TYPE_CHECKING:
async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.GetPlacementsOutput:
placements = await self.database.get_user_placements(
input.user_id,
await self.ensure_workspace_access(input.workspace_id, input.user_id)
placements = await self.database.get_workspace_placements(
input.workspace_id,
target_channel_id=input.target_channel_id,
external_channel_id=input.external_channel_id,
creative_id=input.creative_id,

View File

@@ -11,9 +11,15 @@ log = logging.getLogger(__name__)
async def update_placement(
self: 'Usecase', placement_id: uuid.UUID, input: dto.UpdatePlacementInput, user_id: uuid.UUID
self: 'Usecase',
placement_id: uuid.UUID,
input: dto.UpdatePlacementInput,
user_id: uuid.UUID,
workspace_id: uuid.UUID,
) -> dto.PlacementOutput:
placement = await self.database.get_placement(user_id, placement_id)
await self.ensure_workspace_access(workspace_id, user_id)
placement = await self.database.get_placement(workspace_id, placement_id)
if not placement:
log.warning('Placement %s not found for user %s', placement_id, user_id)
raise domain.PlacementNotFound(placement_id)

View File

@@ -15,7 +15,7 @@ async def disconnect_target_chan_by_tg_id(self: 'Usecase', input: dto.Disconnect
log.warning(f'User with telegram_id {input.user_telegram_id} not found when disconnecting channel')
return
channel = await self.database.get_target_channel(user.id, telegram_id=input.telegram_id)
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()

View File

@@ -7,7 +7,9 @@ if TYPE_CHECKING:
async def get_user_target_chans(self: 'Usecase', input: dto.GetUserTargetChansInput) -> dto.GetUserTargetChansOutput:
channels = await self.database.get_user_target_channels(input.user_id)
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=[

View File

@@ -22,7 +22,7 @@ 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(user.id, telegram_id=input.telegram_id)
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()

View File

@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) -> dto.GetViewsHistoryOutput:
placement = await self.database.get_placement(input.user_id, input.placement_id)
await self.ensure_workspace_access(input.workspace_id, input.user_id)
placement = await self.database.get_placement(input.workspace_id, input.placement_id)
if not placement:
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
raise domain.PlacementNotFound(input.placement_id)

View File

@@ -1,57 +0,0 @@
import logging
from typing import TYPE_CHECKING
from tortoise import timezone
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
log = logging.getLogger(__name__)
async def update_views_manually(self: 'Usecase', input: dto.UpdateViewsManuallyInput) -> dto.PlacementOutput:
placement = await self.database.get_placement(input.user_id, input.placement_id)
if not placement:
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
raise domain.PlacementNotFound(input.placement_id)
fetched_at = timezone.now()
async with self.database.transaction():
placement.views_count = input.views_count
placement.views_availability = domain.PostViewsAvailability.MANUAL
placement.last_views_fetch_at = fetched_at
await self.database.update_placement(placement)
history = domain.PlacementViewsHistory(
placement_id=placement.id,
views_count=input.views_count,
fetched_at=fetched_at,
)
await self.database.create_placement_views_history(history)
log.info('Views manually updated for placement %s: %d', placement.id, input.views_count)
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,
creative_id=placement.creative_id,
creative_name=placement.creative.name,
placement_date=placement.placement_date,
cost=placement.cost,
comment=placement.comment,
ad_post_url=placement.ad_post_url,
invite_link_type=placement.invite_link_type,
invite_link=placement.invite_link,
status=placement.status,
subscriptions_count=placement.subscriptions_count,
views_count=placement.views_count,
views_availability=placement.views_availability,
last_views_fetch_at=placement.last_views_fetch_at,
created_at=placement.created_at,
)

View File

@@ -0,0 +1,28 @@
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
async def create_workspace(
self: 'Usecase', user_id: uuid.UUID, input: dto.CreateWorkspaceInput
) -> dto.CreateWorkspaceOutput:
user = await self.database.get_user(user_id=user_id)
if not user:
raise domain.UserNotFound(user_id)
workspace = domain.Workspace(
name=input.name,
)
async with self.database.transaction():
await self.database.create_workspace(workspace)
await self.database.add_user_to_workspace(workspace.id, user_id)
return dto.CreateWorkspaceOutput(
id=workspace.id,
name=workspace.name,
)

View File

@@ -0,0 +1,11 @@
import uuid
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .. import Usecase
async def delete_workspace(self: 'Usecase', workspace_id: uuid.UUID, user_id: uuid.UUID) -> None:
self.ensure_workspace_access(workspace_id, user_id)
await self.database.delete_workspace(workspace_id)

View File

@@ -0,0 +1,21 @@
import uuid
from typing import TYPE_CHECKING
from src import dto
if TYPE_CHECKING:
from .. import Usecase
async def get_workspaces(self: 'Usecase', user_id: uuid.UUID) -> dto.GetWorkspacesOutput:
memberships = await self.database.get_user_workspaces(user_id)
return dto.GetWorkspacesOutput(
workspaces=[
dto.WorkspaceMembershipOutput(
id=membership.workspace_id,
name=membership.workspace.name,
)
for membership in memberships
]
)

View File

@@ -0,0 +1,26 @@
import uuid
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
async def update_workspace(
self: 'Usecase', workspace_id: uuid.UUID, user_id: uuid.UUID, input: dto.UpdateWorkspaceInput
) -> dto.WorkspaceMembershipOutput:
membership = await self.database.get_workspace_membership(workspace_id, user_id)
if not membership:
raise domain.WorkspaceNotFound(workspace_id)
workspace = membership.workspace
if input.name is not None:
workspace.name = input.name
await self.database.update_workspace(workspace)
return dto.WorkspaceMembershipOutput(
id=workspace.id,
name=workspace.name,
)