change domen
This commit is contained in:
@@ -257,37 +257,60 @@ class Postgres(DatabaseBase, Database):
|
||||
|
||||
return await query.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_active_purchase(self, project_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Purchase | None:
|
||||
return await domain.Purchase.get_or_none(
|
||||
project_id=project_id, creative_id=creative_id, status=domain.PurchaseStatus.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 get_purchase(self, workspace_id: uuid.UUID, purchase_id: uuid.UUID) -> domain.Purchase | None:
|
||||
return await domain.Purchase.get_or_none(id=purchase_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:
|
||||
async def get_project_purchases(self, workspace_id: uuid.UUID, project_id: uuid.UUID) -> list[domain.Purchase]:
|
||||
return (
|
||||
await domain.PurchasePlanChannel.filter(id=plan_channel_id, purchase_plan_id=plan_id)
|
||||
await domain.Purchase.filter(workspace_id=workspace_id, project_id=project_id)
|
||||
.prefetch_related('channels', 'channels__channel')
|
||||
.all()
|
||||
)
|
||||
|
||||
async def create_purchase(self, purchase: domain.Purchase) -> None:
|
||||
await purchase.save()
|
||||
|
||||
async def get_purchase_channels(self, purchase_id: uuid.UUID) -> list[domain.PurchaseChannel]:
|
||||
return await domain.PurchaseChannel.filter(purchase_id=purchase_id).prefetch_related('channel').all()
|
||||
|
||||
async def get_purchase_channel(
|
||||
self, purchase_channel_id: uuid.UUID, purchase_id: uuid.UUID
|
||||
) -> domain.PurchaseChannel | None:
|
||||
return (
|
||||
await domain.PurchaseChannel.filter(id=purchase_channel_id, purchase_id=purchase_id)
|
||||
.prefetch_related('channel')
|
||||
.first()
|
||||
)
|
||||
|
||||
async def add_channel_to_purchase_plan(
|
||||
async def get_purchase_channels_by_project(self, project_id: uuid.UUID) -> list[domain.PurchaseChannel]:
|
||||
return (
|
||||
await domain.PurchaseChannel.filter(
|
||||
purchase__project_id=project_id, purchase__status=domain.PurchaseStatus.ACTIVE
|
||||
)
|
||||
.prefetch_related('channel')
|
||||
.all()
|
||||
)
|
||||
|
||||
async def add_channel_to_purchase(
|
||||
self,
|
||||
plan_id: uuid.UUID,
|
||||
purchase_id: uuid.UUID,
|
||||
channel_id: uuid.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:
|
||||
defaults: dict[str, object | None] = {}
|
||||
) -> domain.PurchaseChannel:
|
||||
defaults: dict[str, object | None] = {
|
||||
'invite_link': invite_link,
|
||||
'invite_link_type': invite_link_type,
|
||||
}
|
||||
if status is not None:
|
||||
defaults['status'] = status
|
||||
if planned_cost is not None:
|
||||
@@ -295,24 +318,24 @@ class Postgres(DatabaseBase, Database):
|
||||
if comment is not None:
|
||||
defaults['comment'] = comment
|
||||
|
||||
plan_channel, created = await domain.PurchasePlanChannel.get_or_create(
|
||||
purchase_plan_id=plan_id,
|
||||
purchase_channel, created = await domain.PurchaseChannel.get_or_create(
|
||||
purchase_id=purchase_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()
|
||||
setattr(purchase_channel, field, value)
|
||||
await purchase_channel.save()
|
||||
|
||||
return plan_channel
|
||||
return purchase_channel
|
||||
|
||||
async def update_purchase_plan_channel(self, plan_channel: domain.PurchasePlanChannel) -> None:
|
||||
await plan_channel.save()
|
||||
async def update_purchase_channel(self, purchase_channel: domain.PurchaseChannel) -> None:
|
||||
await purchase_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 remove_channel_from_purchase(self, purchase_id: uuid.UUID, channel_id: uuid.UUID) -> None:
|
||||
await domain.PurchaseChannel.filter(purchase_id=purchase_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(
|
||||
@@ -485,27 +508,6 @@ class Postgres(DatabaseBase, Database):
|
||||
|
||||
return results
|
||||
|
||||
async def get_telegram_state(self, telegram_id: int) -> domain.TelegramState | None:
|
||||
return await domain.TelegramState.get_or_none(telegram_id=telegram_id)
|
||||
|
||||
async def set_telegram_state(
|
||||
self, telegram_id: int, state: domain.TelegramStateEnum, context: dict[str, typing.Any]
|
||||
) -> domain.TelegramState:
|
||||
existing = await self.get_telegram_state(telegram_id)
|
||||
|
||||
if existing:
|
||||
existing.state = state
|
||||
existing.context = context
|
||||
existing.updated_at = timezone.now()
|
||||
await existing.save()
|
||||
return existing
|
||||
|
||||
new_state = await domain.TelegramState.create(telegram_id=telegram_id, state=state, context=context)
|
||||
return new_state
|
||||
|
||||
async def clear_telegram_state(self, telegram_id: int) -> None:
|
||||
await domain.TelegramState.filter(telegram_id=telegram_id).delete()
|
||||
|
||||
# Count methods
|
||||
async def count_placements_by_creative(self, creative_id: uuid.UUID) -> int:
|
||||
return await domain.Placement.filter(creative_id=creative_id).count()
|
||||
|
||||
@@ -7,7 +7,7 @@ from src.controller.http_v1.creatives import creatives_router
|
||||
from src.controller.http_v1.internal import internal_router
|
||||
from src.controller.http_v1.placements import placements_router
|
||||
from src.controller.http_v1.projects import projects_router
|
||||
from src.controller.http_v1.purchase_plans import purchase_plan_channels_router
|
||||
from src.controller.http_v1.purchases import purchase_router
|
||||
from src.controller.http_v1.views import views_router
|
||||
from src.controller.http_v1.workspace_invites import workspace_invites_router, workspace_invites_global_router
|
||||
from src.controller.http_v1.workspace_members import workspace_members_router
|
||||
@@ -21,7 +21,7 @@ api_v1_router.include_router(auth_router)
|
||||
api_v1_router.include_router(internal_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(purchase_router)
|
||||
api_v1_router.include_router(creatives_router)
|
||||
api_v1_router.include_router(placements_router)
|
||||
api_v1_router.include_router(views_router)
|
||||
|
||||
@@ -48,40 +48,4 @@ async def get_placement(
|
||||
return await deps.get_usecase().get_placement(input=input)
|
||||
|
||||
|
||||
@placements_router.post('')
|
||||
async def create_placement(
|
||||
request: dto.CreatePlacementInput,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.PlacementOutput:
|
||||
return await deps.get_usecase().create_placement(request, current_user.user_id, workspace_id)
|
||||
|
||||
|
||||
@placements_router.patch('/{placement_id}')
|
||||
async def update_placement(
|
||||
placement_id: uuid.UUID,
|
||||
request: dto.UpdatePlacementInput,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.PlacementOutput:
|
||||
return await deps.get_usecase().update_placement(
|
||||
placement_id=placement_id,
|
||||
input=request,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
|
||||
@placements_router.delete('/{placement_id}')
|
||||
async def delete_placement(
|
||||
placement_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> None:
|
||||
input = dto.DeletePlacementInput(
|
||||
placement_id=placement_id,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
await deps.get_usecase().delete_placement(input)
|
||||
# Placement creation/updates are handled by worker; API exposes only read endpoints.
|
||||
|
||||
@@ -12,9 +12,8 @@ projects_router = APIRouter(prefix='/workspaces/{workspace_id}/projects', tags=[
|
||||
|
||||
|
||||
@projects_router.get('')
|
||||
async def get_workspace_projects(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
async def get_projects(
|
||||
workspace_id: uuid.UUID, current_user: Annotated[JWTPayload, Depends(deps.get_current_user)]
|
||||
) -> Page[dto.ProjectOutput]:
|
||||
input = dto.GetWorkspaceProjectsInput(
|
||||
user_id=current_user.user_id,
|
||||
@@ -32,18 +31,9 @@ async def update_project_invite_link_type(
|
||||
input: dto.UpdateProjectInviteLinkTypeInput,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.ProjectOutput:
|
||||
project = await deps.get_usecase().update_project_invite_link_type(
|
||||
return await deps.get_usecase().update_project_invite_link_type(
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
invite_link_type=input.invite_link_type,
|
||||
purchase_invite_type_default=input.purchase_invite_type_default,
|
||||
user_id=current_user.user_id,
|
||||
)
|
||||
|
||||
return dto.ProjectOutput(
|
||||
id=project.id,
|
||||
telegram_id=project.channel.telegram_id,
|
||||
title=project.channel.title,
|
||||
username=project.channel.username,
|
||||
status=project.status,
|
||||
invite_link_type=project.invite_link_type,
|
||||
)
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi.routing import APIRouter
|
||||
from fastapi_pagination import Page, paginate
|
||||
|
||||
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)],
|
||||
) -> Page[dto.PurchasePlanChannelOutput]:
|
||||
input = dto.GetPurchasePlanChannelsInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
)
|
||||
result = await deps.get_usecase().get_purchase_plan_channels(input=input)
|
||||
return paginate(result) # type: ignore[no-any-return]
|
||||
|
||||
|
||||
@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,
|
||||
)
|
||||
60
src/controller/http_v1/purchases.py
Normal file
60
src/controller/http_v1/purchases.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi.routing import APIRouter
|
||||
from fastapi_pagination import Page, paginate
|
||||
|
||||
from src import deps, dto
|
||||
from src.adapter.jwt import JWTPayload
|
||||
|
||||
purchase_router = APIRouter(
|
||||
prefix='/workspaces/{workspace_id}/projects/{project_id}/purchase',
|
||||
tags=['purchase'],
|
||||
)
|
||||
|
||||
|
||||
@purchase_router.post('')
|
||||
async def create_purchase(
|
||||
request: dto.CreatePurchaseInput,
|
||||
workspace_id: uuid.UUID,
|
||||
project_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.PurchaseOutput:
|
||||
return await deps.get_usecase().create_purchase(
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
user_id=current_user.user_id,
|
||||
input=request,
|
||||
)
|
||||
|
||||
|
||||
@purchase_router.get('')
|
||||
async def get_purchases(
|
||||
workspace_id: uuid.UUID,
|
||||
project_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> Page[dto.PurchaseOutput]:
|
||||
input = dto.GetPurchasesInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
)
|
||||
result = await deps.get_usecase().get_purchases(input=input)
|
||||
return paginate(result) # type: ignore[no-any-return]
|
||||
|
||||
|
||||
@purchase_router.get('/{purchase_id}')
|
||||
async def get_purchase(
|
||||
purchase_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
project_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.PurchaseOutput:
|
||||
input = dto.GetPurchaseInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
purchase_id=purchase_id,
|
||||
)
|
||||
return await deps.get_usecase().get_purchase(input=input)
|
||||
@@ -15,18 +15,16 @@ __all__ = (
|
||||
'Channel',
|
||||
'Project',
|
||||
'ProjectStatus',
|
||||
'PurchasePlan',
|
||||
'PurchasePlanChannel',
|
||||
'PurchasePlanStatus',
|
||||
'PurchasePlanChannelStatus',
|
||||
'Purchase',
|
||||
'PurchaseChannel',
|
||||
'PurchaseStatus',
|
||||
'PurchaseChannelStatus',
|
||||
'Creative',
|
||||
'replace_invite_link_with_tag',
|
||||
'Placement',
|
||||
'Post',
|
||||
'Subscription',
|
||||
'PostViewsHistory',
|
||||
'TelegramState',
|
||||
'TelegramStateEnum',
|
||||
'ChannelNotFound',
|
||||
'ProjectNotFound',
|
||||
'CreativeStatus',
|
||||
@@ -45,8 +43,8 @@ __all__ = (
|
||||
'LoginTokenExpired',
|
||||
'LoginTokenAlreadyUsed',
|
||||
'ProjectNotFound',
|
||||
'PurchasePlanNotFound',
|
||||
'PurchasePlanChannelNotFound',
|
||||
'PurchaseNotFound',
|
||||
'PurchaseChannelNotFound',
|
||||
'ChannelNotFound',
|
||||
'ChannelAlreadyExists',
|
||||
'ChannelNoAdminRights',
|
||||
@@ -74,8 +72,8 @@ from .error import (
|
||||
LoginTokenNotFound,
|
||||
PlacementNotFound,
|
||||
ProjectNotFound,
|
||||
PurchasePlanChannelNotFound,
|
||||
PurchasePlanNotFound,
|
||||
PurchaseChannelNotFound,
|
||||
PurchaseNotFound,
|
||||
TelegramChannelNotFound,
|
||||
UserByUsernameNotFound,
|
||||
UserNotFound,
|
||||
@@ -89,15 +87,15 @@ from .login_token import LoginToken
|
||||
from .placement import Placement, PlacementStatus, PostViewsAvailability
|
||||
from .post import Post
|
||||
from .post_views_history import PostViewsHistory
|
||||
from .project import InviteLinkType, Project, ProjectStatus
|
||||
from .purchase_plan import (
|
||||
PurchasePlan,
|
||||
PurchasePlanChannel,
|
||||
PurchasePlanChannelStatus,
|
||||
PurchasePlanStatus,
|
||||
from .project import Project, ProjectStatus
|
||||
from .purchase import (
|
||||
InviteLinkType,
|
||||
Purchase,
|
||||
PurchaseChannel,
|
||||
PurchaseChannelStatus,
|
||||
PurchaseStatus,
|
||||
)
|
||||
from .subscription import Subscription, SubscriptionStatus
|
||||
from .telegram_state import TelegramState, TelegramStateEnum
|
||||
from .user import User
|
||||
from .workspace import (
|
||||
PermissionKey,
|
||||
|
||||
@@ -43,16 +43,16 @@ def ProjectNotFound(project_id: uuid.UUID | None = None) -> HTTPException:
|
||||
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 PurchaseNotFound(purchase_id: uuid.UUID | None = None) -> HTTPException:
|
||||
if purchase_id is None:
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'Purchase not found')
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'Purchase {purchase_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 PurchaseChannelNotFound(purchase_channel_id: uuid.UUID | None = None) -> HTTPException:
|
||||
if purchase_channel_id is None:
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'Purchase channel not found')
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'Purchase channel {purchase_channel_id} not found')
|
||||
|
||||
|
||||
def ChannelAlreadyExists(telegram_id: int) -> HTTPException:
|
||||
|
||||
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
|
||||
from .creative import Creative
|
||||
from .post import Post
|
||||
from .project import Project
|
||||
from .purchase import Purchase, PurchaseChannel
|
||||
from .workspace import Workspace
|
||||
|
||||
|
||||
@@ -27,35 +28,44 @@ class PostViewsAvailability(str, enum.Enum):
|
||||
|
||||
|
||||
class Placement(TimestampedModel):
|
||||
placement_date = fields.DatetimeField()
|
||||
wanted_placement_date = fields.DatetimeField(db_column='placement_date')
|
||||
cost = fields.FloatField(null=True)
|
||||
comment = fields.TextField(null=True)
|
||||
invite_link = fields.CharField(max_length=512)
|
||||
|
||||
status = fields.CharEnumField(PlacementStatus, default=PlacementStatus.ACTIVE)
|
||||
|
||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||
'models.Workspace', 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
|
||||
)
|
||||
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
|
||||
)
|
||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||
'models.Workspace', 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
|
||||
)
|
||||
purchase: fields.ForeignKeyRelation['Purchase'] | None = fields.ForeignKeyField(
|
||||
'models.Purchase', related_name='placements', on_delete=fields.CASCADE, null=True, index=True
|
||||
)
|
||||
purchase_channel: fields.ForeignKeyRelation['PurchaseChannel'] | None = fields.ForeignKeyField(
|
||||
'models.PurchaseChannel', related_name='placements', on_delete=fields.CASCADE, null=True, index=True
|
||||
)
|
||||
|
||||
post: fields.ForeignKeyRelation['Post'] | None = fields.ForeignKeyField(
|
||||
'models.Post', related_name='placements', on_delete=fields.SET_NULL, null=True, index=True
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
project_id: UUID
|
||||
placement_channel_id: UUID
|
||||
creative_id: UUID
|
||||
workspace_id: UUID
|
||||
project_id: UUID
|
||||
creative_id: UUID
|
||||
placement_channel_id: UUID
|
||||
purchase_id: UUID | None
|
||||
post_id: UUID | None
|
||||
purchase_channel_id: UUID | None
|
||||
|
||||
class Meta:
|
||||
table = 'placement'
|
||||
|
||||
@@ -4,6 +4,7 @@ from uuid import UUID
|
||||
|
||||
from tortoise import fields
|
||||
|
||||
from .purchase import InviteLinkType
|
||||
from .base import TimestampedModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -17,25 +18,21 @@ class ProjectStatus(str, enum.Enum):
|
||||
ARCHIVED = 'archived'
|
||||
|
||||
|
||||
class InviteLinkType(str, enum.Enum):
|
||||
PUBLIC = 'public' # открытая ссылка
|
||||
APPROVAL = 'approval' # с одобрением ботом
|
||||
|
||||
|
||||
class Project(TimestampedModel):
|
||||
status = fields.CharEnumField(ProjectStatus, default=ProjectStatus.ACTIVE)
|
||||
invite_link_type = fields.CharEnumField(InviteLinkType, default=InviteLinkType.APPROVAL, max_length=10)
|
||||
purchase_invite_type_default = fields.CharEnumField(InviteLinkType, default=InviteLinkType.APPROVAL, max_length=10)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||
'models.Workspace', related_name='projects', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
workspace_id: UUID
|
||||
channel_id: UUID
|
||||
workspace_id: UUID
|
||||
|
||||
class Meta:
|
||||
table = 'project'
|
||||
|
||||
74
src/domain/purchase.py
Normal file
74
src/domain/purchase.py
Normal file
@@ -0,0 +1,74 @@
|
||||
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 .creative import Creative
|
||||
from .project import Project
|
||||
from .workspace import Workspace
|
||||
|
||||
|
||||
class PurchaseStatus(str, enum.Enum):
|
||||
ACTIVE = 'active'
|
||||
ARCHIVED = 'archived'
|
||||
|
||||
|
||||
class PurchaseChannelStatus(str, enum.Enum):
|
||||
PLANNED = 'planned'
|
||||
APPROVED = 'approved'
|
||||
REJECTED = 'rejected'
|
||||
IN_PROGRESS = 'in_progress'
|
||||
COMPLETED = 'completed'
|
||||
|
||||
|
||||
class Purchase(TimestampedModel):
|
||||
status = fields.CharEnumField(PurchaseStatus, default=PurchaseStatus.ACTIVE)
|
||||
|
||||
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
|
||||
'models.Project', related_name='purchases', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField(
|
||||
'models.Creative', related_name='purchases', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||
'models.Workspace', related_name='purchases', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
table = 'purchase'
|
||||
indexes = (('project', 'status'),)
|
||||
|
||||
|
||||
class InviteLinkType(str, enum.Enum):
|
||||
PUBLIC = 'public' # открытая ссылка
|
||||
APPROVAL = 'approval' # с одобрением ботом
|
||||
|
||||
|
||||
class PurchaseChannel(TimestampedModel):
|
||||
status = fields.CharEnumField(PurchaseChannelStatus, default=PurchaseChannelStatus.PLANNED)
|
||||
planned_cost = fields.FloatField(null=True)
|
||||
comment = fields.TextField(null=True)
|
||||
|
||||
invite_link = fields.CharField(max_length=512)
|
||||
invite_link_type = fields.CharEnumField(InviteLinkType)
|
||||
|
||||
purchase: fields.ForeignKeyRelation['Purchase'] = fields.ForeignKeyField(
|
||||
'models.Purchase', related_name='channels', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField(
|
||||
'models.Channel', related_name='purchase_channels', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
purchase_id: uuid.UUID
|
||||
channel_id: uuid.UUID
|
||||
|
||||
class Meta:
|
||||
table = 'purchase_channel'
|
||||
unique_together = (('purchase_id', 'channel_id'),)
|
||||
@@ -1,61 +0,0 @@
|
||||
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):
|
||||
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):
|
||||
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,22 +0,0 @@
|
||||
import enum
|
||||
|
||||
from tortoise import fields
|
||||
|
||||
from .base import TimestampedModel
|
||||
|
||||
|
||||
class TelegramStateEnum(str, enum.Enum):
|
||||
CREATIVE_WAITING_WORKSPACE = 'creative_waiting_workspace'
|
||||
CREATIVE_WAITING_CHANNEL = 'creative_waiting_channel'
|
||||
CREATIVE_WAITING_NAME = 'creative_waiting_name'
|
||||
CREATIVE_WAITING_TEXT = 'creative_waiting_text'
|
||||
PROJECT_WAITING_WORKSPACE = 'project_waiting_workspace'
|
||||
|
||||
|
||||
class TelegramState(TimestampedModel):
|
||||
telegram_id = fields.BigIntField(unique=True, index=True)
|
||||
state = fields.CharEnumField(TelegramStateEnum)
|
||||
context = fields.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
table = 'telegram_state'
|
||||
@@ -9,18 +9,19 @@ __all__ = (
|
||||
'ProjectOutput',
|
||||
'ValidateLoginTokenInput',
|
||||
'ValidateLoginTokenOutput',
|
||||
'TelegramLoginInput',
|
||||
'ChannelBotPermissions',
|
||||
'ChannelOutput',
|
||||
'GetChannelInput',
|
||||
'GetChannelsInput',
|
||||
'GetChannelsOutput',
|
||||
'AttachChannelToWorkspaceInput',
|
||||
'PurchasePlanChannelOutput',
|
||||
'GetPurchasePlanChannelsInput',
|
||||
'GetPurchasePlanChannelsOutput',
|
||||
'AttachChannelToPurchasePlanInput',
|
||||
'UpdatePurchasePlanChannelInput',
|
||||
'PurchaseOutput',
|
||||
'PurchaseChannelOutput',
|
||||
'CreatePurchaseInput',
|
||||
'CreatePurchaseChannelInput',
|
||||
'GetPurchasesInput',
|
||||
'GetPurchasesOutput',
|
||||
'GetPurchaseInput',
|
||||
'CreativeOutput',
|
||||
'GetCreativesInput',
|
||||
'GetCreativesOutput',
|
||||
@@ -32,9 +33,6 @@ __all__ = (
|
||||
'GetPlacementsInput',
|
||||
'GetPlacementsOutput',
|
||||
'GetPlacementInput',
|
||||
'CreatePlacementInput',
|
||||
'UpdatePlacementInput',
|
||||
'DeletePlacementInput',
|
||||
'PostViewsHistoryOutput',
|
||||
'GetViewsHistoryInput',
|
||||
'GetViewsHistoryOutput',
|
||||
@@ -104,15 +102,7 @@ from .creative import (
|
||||
GetCreativesOutput,
|
||||
UpdateCreativeInput,
|
||||
)
|
||||
from .placement import (
|
||||
CreatePlacementInput,
|
||||
DeletePlacementInput,
|
||||
GetPlacementInput,
|
||||
GetPlacementsInput,
|
||||
GetPlacementsOutput,
|
||||
PlacementOutput,
|
||||
UpdatePlacementInput,
|
||||
)
|
||||
from .placement import GetPlacementInput, GetPlacementsInput, GetPlacementsOutput, PlacementOutput
|
||||
from .project import (
|
||||
ChannelBotPermissions,
|
||||
ConnectProjectInput,
|
||||
@@ -124,14 +114,15 @@ from .project import (
|
||||
UpdateProjectInviteLinkTypeInput,
|
||||
UpdateProjectPermissionsInput,
|
||||
)
|
||||
from .purchase_plan import (
|
||||
AttachChannelToPurchasePlanInput,
|
||||
GetPurchasePlanChannelsInput,
|
||||
GetPurchasePlanChannelsOutput,
|
||||
PurchasePlanChannelOutput,
|
||||
UpdatePurchasePlanChannelInput,
|
||||
from .purchase import (
|
||||
CreatePurchaseChannelInput,
|
||||
CreatePurchaseInput,
|
||||
GetPurchaseInput,
|
||||
GetPurchasesInput,
|
||||
GetPurchasesOutput,
|
||||
PurchaseChannelOutput,
|
||||
PurchaseOutput,
|
||||
)
|
||||
from .telegram_login import TelegramLoginInput
|
||||
from .user import UserOutput
|
||||
from .validate_login_token import ValidateLoginTokenInput, ValidateLoginTokenOutput
|
||||
from .views import (
|
||||
@@ -162,4 +153,4 @@ from .workspace import (
|
||||
|
||||
class CreateLoginTokenRequest(BaseModel):
|
||||
telegram_id: int
|
||||
username: str | None
|
||||
username: str | None
|
||||
|
||||
@@ -18,10 +18,12 @@ class PlacementOutput(pydantic.BaseModel):
|
||||
cost: float | None
|
||||
comment: str | None
|
||||
ad_post_url: str | None
|
||||
invite_link_type: domain.InviteLinkType
|
||||
invite_link_type: domain.purchase.InviteLinkType
|
||||
invite_link: str
|
||||
status: domain.PlacementStatus
|
||||
subscriptions_count: int
|
||||
purchase_id: uuid.UUID | None = None
|
||||
purchase_channel_id: uuid.UUID | None = None
|
||||
created_at: datetime.datetime
|
||||
|
||||
|
||||
|
||||
@@ -32,11 +32,11 @@ class ProjectOutput(pydantic.BaseModel):
|
||||
title: str
|
||||
username: str | None
|
||||
status: ProjectStatus
|
||||
invite_link_type: domain.InviteLinkType
|
||||
purchase_invite_type_default: domain.purchase.InviteLinkType
|
||||
|
||||
|
||||
class UpdateProjectInviteLinkTypeInput(pydantic.BaseModel):
|
||||
invite_link_type: domain.InviteLinkType
|
||||
purchase_invite_type_default: domain.purchase.InviteLinkType
|
||||
|
||||
|
||||
class GetWorkspaceProjectsInput(pydantic.BaseModel):
|
||||
|
||||
53
src/dto/purchase.py
Normal file
53
src/dto/purchase.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import uuid
|
||||
|
||||
import pydantic
|
||||
|
||||
from src.domain.purchase import InviteLinkType, PurchaseChannelStatus, PurchaseStatus
|
||||
|
||||
from .channel import ChannelOutput
|
||||
|
||||
|
||||
class PurchaseChannelOutput(pydantic.BaseModel):
|
||||
id: uuid.UUID
|
||||
status: PurchaseChannelStatus
|
||||
planned_cost: float | None = None
|
||||
comment: str | None = None
|
||||
invite_link: str
|
||||
invite_link_type: InviteLinkType
|
||||
channel: ChannelOutput
|
||||
|
||||
|
||||
class PurchaseOutput(pydantic.BaseModel):
|
||||
id: uuid.UUID
|
||||
status: PurchaseStatus
|
||||
creative_id: uuid.UUID
|
||||
channels: list[PurchaseChannelOutput]
|
||||
|
||||
|
||||
class CreatePurchaseChannelInput(pydantic.BaseModel):
|
||||
username: str = pydantic.Field(pattern=r'^[^@]')
|
||||
status: PurchaseChannelStatus | None = None
|
||||
planned_cost: float | None = None
|
||||
comment: str | None = None
|
||||
|
||||
|
||||
class CreatePurchaseInput(pydantic.BaseModel):
|
||||
creative_id: uuid.UUID
|
||||
channels: list[CreatePurchaseChannelInput]
|
||||
|
||||
|
||||
class GetPurchasesInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
project_id: uuid.UUID
|
||||
|
||||
|
||||
class GetPurchasesOutput(pydantic.BaseModel):
|
||||
purchases: list[PurchaseOutput]
|
||||
|
||||
|
||||
class GetPurchaseInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
project_id: uuid.UUID
|
||||
purchase_id: uuid.UUID
|
||||
@@ -1,38 +0,0 @@
|
||||
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 = pydantic.Field(pattern=r'^[^@]')
|
||||
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
|
||||
@@ -1,7 +0,0 @@
|
||||
import pydantic
|
||||
|
||||
|
||||
class TelegramLoginInput(pydantic.BaseModel):
|
||||
telegram_id: int
|
||||
username: str | None
|
||||
chat_id: int
|
||||
@@ -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