From 552a3ddef2f41201a6b92ac6e7eae7f78b4b62a1 Mon Sep 17 00:00:00 2001 From: Artem Tsyrulnikov Date: Tue, 11 Nov 2025 23:10:08 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D0=BB=D0=BD=D1=8B=D0=B9=20?= =?UTF-8?q?patch=20External=20Channel=20=D0=B0=D1=80=D1=85=D0=B8=D0=B2?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20creative=20?= =?UTF-8?q?=D0=B2=20patch=20=D0=B8=20purchase,=20=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D1=83=20=D0=BD=D0=B8=D1=85=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=BF=20=D1=80=D1=83=D1=87=D0=BA=D0=B8/usecase/dto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/adapter/postgres.py | 5 +++ src/controller/http/creatives.py | 11 ----- src/controller/http/external_channels.py | 14 ++++++ src/controller/http/purchases.py | 11 ----- src/dto/__init__.py | 6 +-- src/dto/creative.py | 6 +-- src/dto/external_channel.py | 9 +++- src/dto/purchase.py | 6 +-- src/usecase/__init__.py | 8 ++-- src/usecase/creative/archive_creative.py | 32 -------------- src/usecase/creative/update_creative.py | 2 + .../update_external_channel.py | 42 ++++++++++++++++++ src/usecase/purchase/archive_purchase.py | 43 ------------------- src/usecase/purchase/update_purchase.py | 2 + 14 files changed, 80 insertions(+), 117 deletions(-) delete mode 100644 src/usecase/creative/archive_creative.py create mode 100644 src/usecase/external_channel/update_external_channel.py delete mode 100644 src/usecase/purchase/archive_purchase.py diff --git a/src/adapter/postgres.py b/src/adapter/postgres.py index 3e6ae74..c7d641d 100644 --- a/src/adapter/postgres.py +++ b/src/adapter/postgres.py @@ -178,6 +178,11 @@ class Postgres(DatabaseBase): await self.session.execute(stmt) await self.session.flush() + async def update_external_channel(self, channel: domain.ExternalChannel) -> domain.ExternalChannel: + await self.session.flush() + await self.session.refresh(channel) + return channel + async def get_creative(self, user_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Creative | None: stmt = ( select(domain.Creative) diff --git a/src/controller/http/creatives.py b/src/controller/http/creatives.py index aa48942..bf65b33 100644 --- a/src/controller/http/creatives.py +++ b/src/controller/http/creatives.py @@ -59,17 +59,6 @@ async def update_creative( ) -@creatives_router.post('/{creative_id}/archive') -async def archive_creative( - creative_id: uuid.UUID, - current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)], -) -> dto.CreativeOutput: - return await dependencies.get_usecase().archive_creative( - creative_id=creative_id, - user_id=current_user.user_id, - ) - - @creatives_router.delete('/{creative_id}') async def delete_creative( creative_id: uuid.UUID, diff --git a/src/controller/http/external_channels.py b/src/controller/http/external_channels.py index 2d5cc86..6795b12 100644 --- a/src/controller/http/external_channels.py +++ b/src/controller/http/external_channels.py @@ -36,6 +36,20 @@ async def create_external_channel( ) +@external_channels_router.patch('/{channel_id}') +async def update_external_channel( + channel_id: uuid.UUID, + request: dto.UpdateExternalChannelInput, + current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)], +) -> dto.ExternalChannelOutput: + """Update external channel fields (title, username, description, subscribers_count).""" + return await dependencies.get_usecase().update_external_channel( + channel_id=channel_id, + input=request, + user_id=current_user.user_id, + ) + + @external_channels_router.patch('/{channel_id}/links') async def update_external_channel_links( channel_id: uuid.UUID, diff --git a/src/controller/http/purchases.py b/src/controller/http/purchases.py index 3e0f246..6280af7 100644 --- a/src/controller/http/purchases.py +++ b/src/controller/http/purchases.py @@ -63,17 +63,6 @@ async def update_purchase( ) -@purchases_router.post('/{purchase_id}/archive') -async def archive_purchase( - purchase_id: uuid.UUID, - current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)], -) -> dto.PurchaseOutput: - return await dependencies.get_usecase().archive_purchase( - purchase_id=purchase_id, - user_id=current_user.user_id, - ) - - @purchases_router.delete('/{purchase_id}') async def delete_purchase( purchase_id: uuid.UUID, diff --git a/src/dto/__init__.py b/src/dto/__init__.py index 94ddec6..33438e6 100644 --- a/src/dto/__init__.py +++ b/src/dto/__init__.py @@ -15,6 +15,7 @@ __all__ = ( 'GetExternalChannelsOutput', 'CreateExternalChannelInput', 'DeleteExternalChannelInput', + 'UpdateExternalChannelInput', 'UpdateExternalChannelLinksInput', 'ImportExternalChannelsInput', 'ImportExternalChannelsOutput', @@ -24,7 +25,6 @@ __all__ = ( 'GetCreativeInput', 'CreateCreativeInput', 'UpdateCreativeInput', - 'ArchiveCreativeInput', 'DeleteCreativeInput', 'PurchaseOutput', 'GetPurchasesInput', @@ -32,7 +32,6 @@ __all__ = ( 'GetPurchaseInput', 'CreatePurchaseInput', 'UpdatePurchaseInput', - 'ArchivePurchaseInput', 'DeletePurchaseInput', 'ViewsSnapshotOutput', 'GetViewsHistoryInput', @@ -43,7 +42,6 @@ __all__ = ( ) from .creative import ( - ArchiveCreativeInput, CreateCreativeInput, CreativeOutput, DeleteCreativeInput, @@ -60,10 +58,10 @@ from .external_channel import ( GetExternalChannelsOutput, ImportExternalChannelsInput, ImportExternalChannelsOutput, + UpdateExternalChannelInput, UpdateExternalChannelLinksInput, ) from .purchase import ( - ArchivePurchaseInput, CreatePurchaseInput, DeletePurchaseInput, GetPurchaseInput, diff --git a/src/dto/creative.py b/src/dto/creative.py index 8e332b6..9cf2fea 100644 --- a/src/dto/creative.py +++ b/src/dto/creative.py @@ -41,11 +41,7 @@ class CreateCreativeInput(pydantic.BaseModel): class UpdateCreativeInput(pydantic.BaseModel): name: str | None = None text: str | None = None - - -class ArchiveCreativeInput(pydantic.BaseModel): - creative_id: uuid.UUID - user_id: uuid.UUID + status: domain.CreativeStatus | None = None class DeleteCreativeInput(pydantic.BaseModel): diff --git a/src/dto/external_channel.py b/src/dto/external_channel.py index fe2a5da..9f9729d 100644 --- a/src/dto/external_channel.py +++ b/src/dto/external_channel.py @@ -35,9 +35,14 @@ class DeleteExternalChannelInput(pydantic.BaseModel): user_id: uuid.UUID -class UpdateExternalChannelLinksInput(pydantic.BaseModel): - """Request body for PATCH endpoint - links to add or remove.""" +class UpdateExternalChannelInput(pydantic.BaseModel): + title: str | None = None + username: str | None = None + description: str | None = None + subscribers_count: int | None = None + +class UpdateExternalChannelLinksInput(pydantic.BaseModel): add_target_channel_ids: list[uuid.UUID] = [] remove_target_channel_ids: list[uuid.UUID] = [] diff --git a/src/dto/purchase.py b/src/dto/purchase.py index 1eb523c..b4e5d06 100644 --- a/src/dto/purchase.py +++ b/src/dto/purchase.py @@ -61,11 +61,7 @@ class UpdatePurchaseInput(pydantic.BaseModel): cost: float | None = None comment: str | None = None ad_post_url: str | None = None - - -class ArchivePurchaseInput(pydantic.BaseModel): - purchase_id: uuid.UUID - user_id: uuid.UUID + status: domain.PurchaseStatus | None = None class DeletePurchaseInput(pydantic.BaseModel): diff --git a/src/usecase/__init__.py b/src/usecase/__init__.py index 0561230..40c152d 100644 --- a/src/usecase/__init__.py +++ b/src/usecase/__init__.py @@ -5,7 +5,6 @@ from uuid import UUID from src import domain -from .creative.archive_creative import archive_creative from .creative.create_creative import create_creative from .creative.delete_creative import delete_creative from .creative.get_creative import get_creative @@ -15,8 +14,8 @@ from .external_channel.create_external_channel import create_external_channel from .external_channel.delete_external_channel import delete_external_channel from .external_channel.get_external_channels import get_external_channels from .external_channel.import_external_channels_from_excel import import_external_channels_from_excel +from .external_channel.update_external_channel import update_external_channel from .external_channel.update_external_channel_links import update_external_channel_links -from .purchase.archive_purchase import archive_purchase from .purchase.create_purchase import create_purchase from .purchase.delete_purchase import delete_purchase from .purchase.get_purchase import get_purchase @@ -78,6 +77,8 @@ class Database(typing.Protocol): async def delete_external_channel(self, channel_id: UUID) -> None: ... + async def update_external_channel(self, channel: domain.ExternalChannel) -> domain.ExternalChannel: ... + async def get_creative(self, user_id: UUID, creative_id: UUID) -> domain.Creative | None: ... async def create_creative(self, creative: domain.Creative) -> domain.Creative: ... @@ -165,19 +166,18 @@ class Usecase: get_external_channels = get_external_channels create_external_channel = create_external_channel delete_external_channel = delete_external_channel + update_external_channel = update_external_channel update_external_channel_links = update_external_channel_links import_external_channels_from_excel = import_external_channels_from_excel get_creatives = get_creatives get_creative = get_creative create_creative = create_creative update_creative = update_creative - archive_creative = archive_creative delete_creative = delete_creative get_purchases = get_purchases get_purchase = get_purchase create_purchase = create_purchase update_purchase = update_purchase - archive_purchase = archive_purchase delete_purchase = delete_purchase handle_subscription = handle_subscription fetch_views = fetch_views diff --git a/src/usecase/creative/archive_creative.py b/src/usecase/creative/archive_creative.py deleted file mode 100644 index 8a8820f..0000000 --- a/src/usecase/creative/archive_creative.py +++ /dev/null @@ -1,32 +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 archive_creative(self: 'Usecase', creative_id: uuid.UUID, user_id: uuid.UUID) -> dto.CreativeOutput: - async with self.database.transaction(): - creative = await self.database.get_creative(user_id, creative_id) - if not creative: - log.warning('User %s attempted to change archive status for unavailable creative %s', user_id, creative_id) - raise domain.CreativeNotFound(creative_id) - - creative.status = domain.CreativeStatus.ARCHIVED - updated = await self.database.update_creative(creative) - - return dto.CreativeOutput( - id=updated.id, - name=updated.name, - text=updated.text, - target_channel_id=updated.target_channel_id, - target_channel_title=updated.target_channel.title, - created_at=updated.created_at, - status=updated.status, - purchases_count=updated.purchases_count, - ) diff --git a/src/usecase/creative/update_creative.py b/src/usecase/creative/update_creative.py index 7771f3a..57cac16 100644 --- a/src/usecase/creative/update_creative.py +++ b/src/usecase/creative/update_creative.py @@ -23,6 +23,8 @@ async def update_creative( creative.name = input.name if input.text is not None: creative.text = input.text + if input.status is not None: + creative.status = input.status updated = await self.database.update_creative(creative) diff --git a/src/usecase/external_channel/update_external_channel.py b/src/usecase/external_channel/update_external_channel.py new file mode 100644 index 0000000..4638d27 --- /dev/null +++ b/src/usecase/external_channel/update_external_channel.py @@ -0,0 +1,42 @@ +import logging +import uuid +from typing import TYPE_CHECKING + +from src import domain, dto + +if TYPE_CHECKING: + from .. import Usecase + +log = logging.getLogger(__name__) + + +async def update_external_channel( + self: 'Usecase', channel_id: uuid.UUID, input: dto.UpdateExternalChannelInput, user_id: uuid.UUID +) -> dto.ExternalChannelOutput: + async with self.database.transaction(): + external_channel = await self.database.get_external_channel(user_id, channel_id=channel_id) + if not external_channel: + log.warning('External channel %s not found', channel_id) + raise domain.ExternalChannelNotFound(channel_id) + + if input.title is not None: + external_channel.title = input.title + if input.username is not None: + external_channel.username = input.username + if input.description is not None: + external_channel.description = input.description + if input.subscribers_count is not None: + external_channel.subscribers_count = input.subscribers_count + + updated = await self.database.update_external_channel(external_channel) + + log.info('External channel %s updated', channel_id) + + return dto.ExternalChannelOutput( + id=updated.id, + telegram_id=updated.telegram_id, + title=updated.title, + username=updated.username, + description=updated.description, + subscribers_count=updated.subscribers_count, + ) diff --git a/src/usecase/purchase/archive_purchase.py b/src/usecase/purchase/archive_purchase.py deleted file mode 100644 index ec427d4..0000000 --- a/src/usecase/purchase/archive_purchase.py +++ /dev/null @@ -1,43 +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 archive_purchase(self: 'Usecase', purchase_id: uuid.UUID, user_id: uuid.UUID) -> dto.PurchaseOutput: - async with self.database.transaction(): - purchase = await self.database.get_purchase(user_id, purchase_id) - if not purchase: - log.warning('Purchase %s not found for user %s', purchase_id, user_id) - raise domain.PurchaseNotFound(purchase_id) - - purchase.status = domain.PurchaseStatus.ARCHIVED - updated = await self.database.update_purchase(purchase) - - return dto.PurchaseOutput( - id=updated.id, - target_channel_id=updated.target_channel_id, - target_channel_title=updated.target_channel.title, - external_channel_id=updated.external_channel_id, - external_channel_title=updated.external_channel.title, - creative_id=updated.creative_id, - creative_name=updated.creative.name, - placement_date=updated.placement_date, - cost=updated.cost, - comment=updated.comment, - ad_post_url=updated.ad_post_url, - invite_link_type=updated.invite_link_type, - invite_link=updated.invite_link, - status=updated.status, - subscriptions_count=updated.subscriptions_count, - views_count=updated.views_count, - views_availability=updated.views_availability, - last_views_fetch_at=updated.last_views_fetch_at, - created_at=updated.created_at, - ) diff --git a/src/usecase/purchase/update_purchase.py b/src/usecase/purchase/update_purchase.py index 8e4c215..03d5a7a 100644 --- a/src/usecase/purchase/update_purchase.py +++ b/src/usecase/purchase/update_purchase.py @@ -28,6 +28,8 @@ async def update_purchase( purchase.comment = input.comment if input.ad_post_url is not None: purchase.ad_post_url = input.ad_post_url + if input.status is not None: + purchase.status = input.status updated = await self.database.update_purchase(purchase)