feat: purchases added
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import datetime
|
||||
import uuid
|
||||
from typing import Any, AsyncContextManager
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
@@ -17,9 +18,10 @@ class MockDatabase:
|
||||
self.target_channels_by_telegram_id: dict[int, domain.TargetChannel] = {}
|
||||
self.external_channels: dict[uuid.UUID, domain.ExternalChannel] = {}
|
||||
self.creatives: dict[uuid.UUID, domain.Creative] = {}
|
||||
self.purchases: dict[uuid.UUID, domain.Purchase] = {}
|
||||
self.m2m_target_external: dict[uuid.UUID, list[uuid.UUID]] = {}
|
||||
|
||||
def transaction(self) -> AsyncContextManager[None]:
|
||||
def transaction(self) -> AbstractAsyncContextManager[None]:
|
||||
return AsyncMock()
|
||||
|
||||
async def get_user(self, *, user_id: uuid.UUID | None = None, telegram_id: int | None = None) -> domain.User | None:
|
||||
@@ -192,10 +194,74 @@ class MockDatabase:
|
||||
async def delete_creative(self, creative_id: uuid.UUID) -> None:
|
||||
self.creatives.pop(creative_id, None)
|
||||
|
||||
async def create_purchase(self, purchase: domain.Purchase) -> domain.Purchase:
|
||||
if not purchase.id:
|
||||
purchase.id = uuid.uuid4()
|
||||
if not hasattr(purchase, 'created_at') or purchase.created_at is None:
|
||||
purchase.created_at = datetime.datetime.now(datetime.UTC)
|
||||
if not hasattr(purchase, 'updated_at') or purchase.updated_at is None:
|
||||
purchase.updated_at = datetime.datetime.now(datetime.UTC)
|
||||
if not hasattr(purchase, 'subscriptions_count') or purchase.subscriptions_count is None:
|
||||
purchase.subscriptions_count = 0
|
||||
if not hasattr(purchase, 'status') or purchase.status is None:
|
||||
purchase.status = domain.PurchaseStatus.ACTIVE
|
||||
# Загружаем связанные объекты
|
||||
target_ch = self.target_channels.get(purchase.target_channel_id)
|
||||
if target_ch:
|
||||
purchase.target_channel = target_ch
|
||||
ext_ch = self.external_channels.get(purchase.external_channel_id)
|
||||
if ext_ch:
|
||||
purchase.external_channel = ext_ch
|
||||
creative = self.creatives.get(purchase.creative_id)
|
||||
if creative:
|
||||
purchase.creative = creative
|
||||
self.purchases[purchase.id] = purchase
|
||||
return purchase
|
||||
|
||||
async def get_purchase(self, user_id: uuid.UUID, purchase_id: uuid.UUID) -> domain.Purchase | None:
|
||||
purchase = self.purchases.get(purchase_id)
|
||||
if purchase and purchase.user_id == user_id:
|
||||
return purchase
|
||||
return None
|
||||
|
||||
async def update_purchase(self, purchase: domain.Purchase) -> domain.Purchase:
|
||||
self.purchases[purchase.id] = purchase
|
||||
return purchase
|
||||
|
||||
async def get_user_purchases(
|
||||
self,
|
||||
user_id: uuid.UUID,
|
||||
*,
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
external_channel_id: uuid.UUID | None = None,
|
||||
creative_id: uuid.UUID | None = None,
|
||||
include_archived: bool = False,
|
||||
) -> list[domain.Purchase]:
|
||||
result = [p for p in self.purchases.values() if p.user_id == user_id]
|
||||
if target_channel_id:
|
||||
result = [p for p in result if p.target_channel_id == target_channel_id]
|
||||
if external_channel_id:
|
||||
result = [p for p in result if p.external_channel_id == external_channel_id]
|
||||
if creative_id:
|
||||
result = [p for p in result if p.creative_id == creative_id]
|
||||
if not include_archived:
|
||||
result = [p for p in result if p.status == domain.PurchaseStatus.ACTIVE]
|
||||
return sorted(result, key=lambda p: p.placement_date, reverse=True)
|
||||
|
||||
async def delete_purchase(self, purchase_id: uuid.UUID) -> None:
|
||||
self.purchases.pop(purchase_id, None)
|
||||
|
||||
async def check_creative_in_use(self, creative_id: uuid.UUID) -> bool:
|
||||
for purchase in self.purchases.values():
|
||||
if purchase.creative_id == creative_id and purchase.status == domain.PurchaseStatus.ACTIVE:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class MockTelegramWriter:
|
||||
def __init__(self) -> None:
|
||||
self.sent_messages: list[dict[str, Any]] = []
|
||||
self.invite_link_counter = 0
|
||||
|
||||
async def send_message(self, text: str, chat_id: int) -> None:
|
||||
self.sent_messages.append({'text': text, 'chat_id': chat_id, 'button': None})
|
||||
@@ -208,6 +274,11 @@ class MockTelegramWriter:
|
||||
async def get_chat_member(self, chat_id: int, user_id: int) -> dict[str, Any]:
|
||||
return {}
|
||||
|
||||
async def create_chat_invite_link(self, chat_id: int, requires_approval: bool = False) -> str:
|
||||
self.invite_link_counter += 1
|
||||
link_type = 'approval' if requires_approval else 'public'
|
||||
return f'https://t.me/+{link_type}_link_{self.invite_link_counter}_{chat_id}'
|
||||
|
||||
|
||||
class MockJWTEncoder:
|
||||
def encode_access_token(self, user_id: uuid.UUID, telegram_id: int, username: str | None = None) -> str:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Тесты работы с креативами."""
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
@@ -119,10 +120,18 @@ async def test_get_creatives_filtered_by_target_channel(usecases: usecase.Usecas
|
||||
await mock_database.upsert_target_channel(target2)
|
||||
|
||||
creative1 = domain.Creative(
|
||||
name='Creative 1', text='Text 1', target_channel_id=target1.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE
|
||||
name='Creative 1',
|
||||
text='Text 1',
|
||||
target_channel_id=target1.id,
|
||||
user_id=user.id,
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
)
|
||||
creative2 = domain.Creative(
|
||||
name='Creative 2', text='Text 2', target_channel_id=target2.id, user_id=user.id, status=domain.CreativeStatus.ACTIVE
|
||||
name='Creative 2',
|
||||
text='Text 2',
|
||||
target_channel_id=target2.id,
|
||||
user_id=user.id,
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_creative(creative1)
|
||||
await mock_database.create_creative(creative2)
|
||||
@@ -230,8 +239,5 @@ async def test_delete_creative(usecases: usecase.Usecase, mock_database: MockDat
|
||||
await usecases.delete_creative(dto.DeleteCreativeInput(creative_id=creative.id, user_id=user.id))
|
||||
|
||||
# Проверяем, что креатив удалён
|
||||
creatives_list = await usecases.get_creatives(
|
||||
dto.GetCreativesInput(user_id=user.id, include_archived=True)
|
||||
)
|
||||
creatives_list = await usecases.get_creatives(dto.GetCreativesInput(user_id=user.id, include_archived=True))
|
||||
assert len(creatives_list.creatives) == 0
|
||||
|
||||
|
||||
629
tests/test_purchases.py
Normal file
629
tests/test_purchases.py
Normal file
@@ -0,0 +1,629 @@
|
||||
"""Тесты работы с закупами."""
|
||||
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from src import domain, dto, usecase
|
||||
from tests.conftest import MockDatabase
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_purchase_with_public_invite_link(usecases: usecase.Usecase, mock_database: MockDatabase) -> None:
|
||||
"""Создание закупа с публичной пригласительной ссылкой."""
|
||||
user = domain.User(telegram_id=123456, username='testuser')
|
||||
await mock_database.create_user(user)
|
||||
|
||||
target_channel = domain.TargetChannel(
|
||||
telegram_id=-1001234567890,
|
||||
title='Target Channel',
|
||||
username='targetchannel',
|
||||
user_id=user.id,
|
||||
is_active=True,
|
||||
)
|
||||
await mock_database.upsert_target_channel(target_channel)
|
||||
|
||||
external_channel = domain.ExternalChannel(
|
||||
telegram_id=-1009876543210,
|
||||
title='External Channel',
|
||||
username='externalchannel',
|
||||
description='Ad channel',
|
||||
subscribers_count=50000,
|
||||
user_id=user.id,
|
||||
)
|
||||
await mock_database.create_external_channel(external_channel)
|
||||
|
||||
creative = domain.Creative(
|
||||
name='Summer Sale',
|
||||
text='Подписывайтесь на наш канал по ссылке: {link}',
|
||||
target_channel_id=target_channel.id,
|
||||
user_id=user.id,
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_creative(creative)
|
||||
|
||||
placement_date = datetime.datetime.now(datetime.UTC)
|
||||
input_data = dto.CreatePurchaseInput(
|
||||
target_channel_id=target_channel.id,
|
||||
external_channel_id=external_channel.id,
|
||||
creative_id=creative.id,
|
||||
placement_date=placement_date,
|
||||
cost=5000.0,
|
||||
comment='Утреннее размещение',
|
||||
ad_post_url='https://t.me/externalchannel/123',
|
||||
invite_link_type=domain.InviteLinkType.PUBLIC,
|
||||
)
|
||||
|
||||
result = await usecases.create_purchase(input_data, user.id)
|
||||
|
||||
assert result.target_channel_id == target_channel.id
|
||||
assert result.external_channel_id == external_channel.id
|
||||
assert result.creative_id == creative.id
|
||||
assert result.cost == 5000.0
|
||||
assert result.comment == 'Утреннее размещение'
|
||||
assert result.invite_link_type == domain.InviteLinkType.PUBLIC
|
||||
assert result.invite_link.startswith('https://t.me/+public_link_')
|
||||
assert result.status == domain.PurchaseStatus.ACTIVE
|
||||
assert result.subscriptions_count == 0
|
||||
|
||||
# Проверяем, что счётчик закупов у креатива увеличился
|
||||
updated_creative = await mock_database.get_creative(user.id, creative.id)
|
||||
assert updated_creative is not None
|
||||
assert updated_creative.purchases_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_purchase_with_approval_invite_link(
|
||||
usecases: usecase.Usecase, mock_database: MockDatabase
|
||||
) -> None:
|
||||
"""Создание закупа с пригласительной ссылкой, требующей одобрения."""
|
||||
user = domain.User(telegram_id=123456, username='testuser')
|
||||
await mock_database.create_user(user)
|
||||
|
||||
target_channel = domain.TargetChannel(
|
||||
telegram_id=-1001234567890,
|
||||
title='Target Channel',
|
||||
username='targetchannel',
|
||||
user_id=user.id,
|
||||
is_active=True,
|
||||
)
|
||||
await mock_database.upsert_target_channel(target_channel)
|
||||
|
||||
external_channel = domain.ExternalChannel(
|
||||
telegram_id=-1009876543210,
|
||||
title='External Channel',
|
||||
username='externalchannel',
|
||||
description=None,
|
||||
subscribers_count=30000,
|
||||
user_id=user.id,
|
||||
)
|
||||
await mock_database.create_external_channel(external_channel)
|
||||
|
||||
creative = domain.Creative(
|
||||
name='Premium Creative',
|
||||
text='Эксклюзивный контент: {link}',
|
||||
target_channel_id=target_channel.id,
|
||||
user_id=user.id,
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_creative(creative)
|
||||
|
||||
placement_date = datetime.datetime.now(datetime.UTC)
|
||||
input_data = dto.CreatePurchaseInput(
|
||||
target_channel_id=target_channel.id,
|
||||
external_channel_id=external_channel.id,
|
||||
creative_id=creative.id,
|
||||
placement_date=placement_date,
|
||||
invite_link_type=domain.InviteLinkType.APPROVAL,
|
||||
)
|
||||
|
||||
result = await usecases.create_purchase(input_data, user.id)
|
||||
|
||||
assert result.invite_link_type == domain.InviteLinkType.APPROVAL
|
||||
assert 'approval_link_' in result.invite_link
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_purchase_invalid_target_channel(usecases: usecase.Usecase, mock_database: MockDatabase) -> None:
|
||||
"""Создание закупа для несуществующего целевого канала вызывает ошибку."""
|
||||
user = domain.User(telegram_id=123456, username='testuser')
|
||||
await mock_database.create_user(user)
|
||||
|
||||
import uuid
|
||||
|
||||
fake_target_id = uuid.uuid4()
|
||||
fake_external_id = uuid.uuid4()
|
||||
fake_creative_id = uuid.uuid4()
|
||||
|
||||
input_data = dto.CreatePurchaseInput(
|
||||
target_channel_id=fake_target_id,
|
||||
external_channel_id=fake_external_id,
|
||||
creative_id=fake_creative_id,
|
||||
placement_date=datetime.datetime.now(datetime.UTC),
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await usecases.create_purchase(input_data, user.id)
|
||||
assert exc_info.value.status_code == 404
|
||||
assert 'Target channel' in str(exc_info.value.detail)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_purchases(usecases: usecase.Usecase, mock_database: MockDatabase) -> None:
|
||||
"""Получение списка закупов пользователя."""
|
||||
user = domain.User(telegram_id=123456, username='testuser')
|
||||
await mock_database.create_user(user)
|
||||
|
||||
target_channel = domain.TargetChannel(
|
||||
telegram_id=-1001234567890,
|
||||
title='Target Channel',
|
||||
username='targetchannel',
|
||||
user_id=user.id,
|
||||
is_active=True,
|
||||
)
|
||||
await mock_database.upsert_target_channel(target_channel)
|
||||
|
||||
external_channel = domain.ExternalChannel(
|
||||
telegram_id=-1009876543210,
|
||||
title='External Channel',
|
||||
username='externalchannel',
|
||||
description=None,
|
||||
subscribers_count=20000,
|
||||
user_id=user.id,
|
||||
)
|
||||
await mock_database.create_external_channel(external_channel)
|
||||
|
||||
creative = domain.Creative(
|
||||
name='Test Creative',
|
||||
text='Test text',
|
||||
target_channel_id=target_channel.id,
|
||||
user_id=user.id,
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_creative(creative)
|
||||
|
||||
# Создаём два закупа
|
||||
purchase1 = domain.Purchase(
|
||||
target_channel_id=target_channel.id,
|
||||
external_channel_id=external_channel.id,
|
||||
creative_id=creative.id,
|
||||
user_id=user.id,
|
||||
placement_date=datetime.datetime.now(datetime.UTC),
|
||||
cost=3000.0,
|
||||
invite_link='https://t.me/+link1',
|
||||
invite_link_type=domain.InviteLinkType.PUBLIC,
|
||||
status=domain.PurchaseStatus.ACTIVE,
|
||||
)
|
||||
purchase2 = domain.Purchase(
|
||||
target_channel_id=target_channel.id,
|
||||
external_channel_id=external_channel.id,
|
||||
creative_id=creative.id,
|
||||
user_id=user.id,
|
||||
placement_date=datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=1),
|
||||
cost=4000.0,
|
||||
invite_link='https://t.me/+link2',
|
||||
invite_link_type=domain.InviteLinkType.PUBLIC,
|
||||
status=domain.PurchaseStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_purchase(purchase1)
|
||||
await mock_database.create_purchase(purchase2)
|
||||
|
||||
result = await usecases.get_purchases(dto.GetPurchasesInput(user_id=user.id))
|
||||
|
||||
assert len(result.purchases) == 2
|
||||
# Проверяем сортировку по дате размещения (новые первые)
|
||||
assert result.purchases[0].id == purchase1.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_purchases_filtered_by_target_channel(usecases: usecase.Usecase, mock_database: MockDatabase) -> None:
|
||||
"""Получение закупов, отфильтрованных по целевому каналу."""
|
||||
user = domain.User(telegram_id=123456, username='testuser')
|
||||
await mock_database.create_user(user)
|
||||
|
||||
target1 = domain.TargetChannel(
|
||||
telegram_id=-1001234567890,
|
||||
title='Target 1',
|
||||
username='target1',
|
||||
user_id=user.id,
|
||||
is_active=True,
|
||||
)
|
||||
target2 = domain.TargetChannel(
|
||||
telegram_id=-1001234567891,
|
||||
title='Target 2',
|
||||
username='target2',
|
||||
user_id=user.id,
|
||||
is_active=True,
|
||||
)
|
||||
await mock_database.upsert_target_channel(target1)
|
||||
await mock_database.upsert_target_channel(target2)
|
||||
|
||||
external_channel = domain.ExternalChannel(
|
||||
telegram_id=-1009876543210,
|
||||
title='External Channel',
|
||||
username='externalchannel',
|
||||
description=None,
|
||||
subscribers_count=15000,
|
||||
user_id=user.id,
|
||||
)
|
||||
await mock_database.create_external_channel(external_channel)
|
||||
|
||||
creative1 = domain.Creative(
|
||||
name='Creative 1',
|
||||
text='Text 1',
|
||||
target_channel_id=target1.id,
|
||||
user_id=user.id,
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
)
|
||||
creative2 = domain.Creative(
|
||||
name='Creative 2',
|
||||
text='Text 2',
|
||||
target_channel_id=target2.id,
|
||||
user_id=user.id,
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_creative(creative1)
|
||||
await mock_database.create_creative(creative2)
|
||||
|
||||
purchase1 = domain.Purchase(
|
||||
target_channel_id=target1.id,
|
||||
external_channel_id=external_channel.id,
|
||||
creative_id=creative1.id,
|
||||
user_id=user.id,
|
||||
placement_date=datetime.datetime.now(datetime.UTC),
|
||||
invite_link='https://t.me/+link1',
|
||||
invite_link_type=domain.InviteLinkType.PUBLIC,
|
||||
status=domain.PurchaseStatus.ACTIVE,
|
||||
)
|
||||
purchase2 = domain.Purchase(
|
||||
target_channel_id=target2.id,
|
||||
external_channel_id=external_channel.id,
|
||||
creative_id=creative2.id,
|
||||
user_id=user.id,
|
||||
placement_date=datetime.datetime.now(datetime.UTC),
|
||||
invite_link='https://t.me/+link2',
|
||||
invite_link_type=domain.InviteLinkType.PUBLIC,
|
||||
status=domain.PurchaseStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_purchase(purchase1)
|
||||
await mock_database.create_purchase(purchase2)
|
||||
|
||||
result = await usecases.get_purchases(dto.GetPurchasesInput(user_id=user.id, target_channel_id=target1.id))
|
||||
|
||||
assert len(result.purchases) == 1
|
||||
assert result.purchases[0].target_channel_id == target1.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_purchases_filtered_by_creative(usecases: usecase.Usecase, mock_database: MockDatabase) -> None:
|
||||
"""Получение закупов, отфильтрованных по креативу - для оценки эффективности креатива."""
|
||||
user = domain.User(telegram_id=123456, username='testuser')
|
||||
await mock_database.create_user(user)
|
||||
|
||||
target_channel = domain.TargetChannel(
|
||||
telegram_id=-1001234567890,
|
||||
title='Target Channel',
|
||||
username='targetchannel',
|
||||
user_id=user.id,
|
||||
is_active=True,
|
||||
)
|
||||
await mock_database.upsert_target_channel(target_channel)
|
||||
|
||||
external_channel = domain.ExternalChannel(
|
||||
telegram_id=-1009876543210,
|
||||
title='External Channel',
|
||||
username='externalchannel',
|
||||
description=None,
|
||||
subscribers_count=25000,
|
||||
user_id=user.id,
|
||||
)
|
||||
await mock_database.create_external_channel(external_channel)
|
||||
|
||||
creative1 = domain.Creative(
|
||||
name='Creative A',
|
||||
text='Вариант A',
|
||||
target_channel_id=target_channel.id,
|
||||
user_id=user.id,
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
)
|
||||
creative2 = domain.Creative(
|
||||
name='Creative B',
|
||||
text='Вариант B',
|
||||
target_channel_id=target_channel.id,
|
||||
user_id=user.id,
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_creative(creative1)
|
||||
await mock_database.create_creative(creative2)
|
||||
|
||||
# Создаём закупы с разными креативами
|
||||
purchase1 = domain.Purchase(
|
||||
target_channel_id=target_channel.id,
|
||||
external_channel_id=external_channel.id,
|
||||
creative_id=creative1.id,
|
||||
user_id=user.id,
|
||||
placement_date=datetime.datetime.now(datetime.UTC),
|
||||
invite_link='https://t.me/+link1',
|
||||
invite_link_type=domain.InviteLinkType.PUBLIC,
|
||||
status=domain.PurchaseStatus.ACTIVE,
|
||||
)
|
||||
purchase2 = domain.Purchase(
|
||||
target_channel_id=target_channel.id,
|
||||
external_channel_id=external_channel.id,
|
||||
creative_id=creative2.id,
|
||||
user_id=user.id,
|
||||
placement_date=datetime.datetime.now(datetime.UTC),
|
||||
invite_link='https://t.me/+link2',
|
||||
invite_link_type=domain.InviteLinkType.PUBLIC,
|
||||
status=domain.PurchaseStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_purchase(purchase1)
|
||||
await mock_database.create_purchase(purchase2)
|
||||
|
||||
result = await usecases.get_purchases(dto.GetPurchasesInput(user_id=user.id, creative_id=creative1.id))
|
||||
|
||||
assert len(result.purchases) == 1
|
||||
assert result.purchases[0].creative_id == creative1.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_archive_purchase(usecases: usecase.Usecase, mock_database: MockDatabase) -> None:
|
||||
"""Архивирование закупа - убирает его из активной статистики."""
|
||||
user = domain.User(telegram_id=123456, username='testuser')
|
||||
await mock_database.create_user(user)
|
||||
|
||||
target_channel = domain.TargetChannel(
|
||||
telegram_id=-1001234567890,
|
||||
title='Target Channel',
|
||||
username='targetchannel',
|
||||
user_id=user.id,
|
||||
is_active=True,
|
||||
)
|
||||
await mock_database.upsert_target_channel(target_channel)
|
||||
|
||||
external_channel = domain.ExternalChannel(
|
||||
telegram_id=-1009876543210,
|
||||
title='External Channel',
|
||||
username='externalchannel',
|
||||
description=None,
|
||||
subscribers_count=18000,
|
||||
user_id=user.id,
|
||||
)
|
||||
await mock_database.create_external_channel(external_channel)
|
||||
|
||||
creative = domain.Creative(
|
||||
name='Test Creative',
|
||||
text='Test text',
|
||||
target_channel_id=target_channel.id,
|
||||
user_id=user.id,
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_creative(creative)
|
||||
|
||||
purchase = domain.Purchase(
|
||||
target_channel_id=target_channel.id,
|
||||
external_channel_id=external_channel.id,
|
||||
creative_id=creative.id,
|
||||
user_id=user.id,
|
||||
placement_date=datetime.datetime.now(datetime.UTC),
|
||||
invite_link='https://t.me/+link1',
|
||||
invite_link_type=domain.InviteLinkType.PUBLIC,
|
||||
status=domain.PurchaseStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_purchase(purchase)
|
||||
|
||||
result = await usecases.archive_purchase(purchase.id, user.id)
|
||||
|
||||
assert result.status == domain.PurchaseStatus.ARCHIVED
|
||||
|
||||
# Проверяем, что архивированный закуп не возвращается по умолчанию
|
||||
purchases_list = await usecases.get_purchases(dto.GetPurchasesInput(user_id=user.id))
|
||||
assert len(purchases_list.purchases) == 0
|
||||
|
||||
# Проверяем, что архивированный закуп возвращается с флагом include_archived
|
||||
purchases_with_archived = await usecases.get_purchases(
|
||||
dto.GetPurchasesInput(user_id=user.id, include_archived=True)
|
||||
)
|
||||
assert len(purchases_with_archived.purchases) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_purchase(usecases: usecase.Usecase, mock_database: MockDatabase) -> None:
|
||||
"""Редактирование закупа - обновление стоимости, комментария, даты."""
|
||||
user = domain.User(telegram_id=123456, username='testuser')
|
||||
await mock_database.create_user(user)
|
||||
|
||||
target_channel = domain.TargetChannel(
|
||||
telegram_id=-1001234567890,
|
||||
title='Target Channel',
|
||||
username='targetchannel',
|
||||
user_id=user.id,
|
||||
is_active=True,
|
||||
)
|
||||
await mock_database.upsert_target_channel(target_channel)
|
||||
|
||||
external_channel = domain.ExternalChannel(
|
||||
telegram_id=-1009876543210,
|
||||
title='External Channel',
|
||||
username='externalchannel',
|
||||
description=None,
|
||||
subscribers_count=22000,
|
||||
user_id=user.id,
|
||||
)
|
||||
await mock_database.create_external_channel(external_channel)
|
||||
|
||||
creative = domain.Creative(
|
||||
name='Test Creative',
|
||||
text='Test text',
|
||||
target_channel_id=target_channel.id,
|
||||
user_id=user.id,
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_creative(creative)
|
||||
|
||||
old_date = datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=1)
|
||||
purchase = domain.Purchase(
|
||||
target_channel_id=target_channel.id,
|
||||
external_channel_id=external_channel.id,
|
||||
creative_id=creative.id,
|
||||
user_id=user.id,
|
||||
placement_date=old_date,
|
||||
cost=3000.0,
|
||||
comment='Старый комментарий',
|
||||
invite_link='https://t.me/+link1',
|
||||
invite_link_type=domain.InviteLinkType.PUBLIC,
|
||||
status=domain.PurchaseStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_purchase(purchase)
|
||||
|
||||
new_date = datetime.datetime.now(datetime.UTC)
|
||||
update_input = dto.UpdatePurchaseInput(
|
||||
placement_date=new_date,
|
||||
cost=3500.0,
|
||||
comment='Обновлённый комментарий',
|
||||
ad_post_url='https://t.me/externalchannel/456',
|
||||
)
|
||||
result = await usecases.update_purchase(purchase.id, update_input, user.id)
|
||||
|
||||
assert result.placement_date == new_date
|
||||
assert result.cost == 3500.0
|
||||
assert result.comment == 'Обновлённый комментарий'
|
||||
assert result.ad_post_url == 'https://t.me/externalchannel/456'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_purchase(usecases: usecase.Usecase, mock_database: MockDatabase) -> None:
|
||||
"""Удаление закупа - уменьшает счётчик закупов у креатива."""
|
||||
user = domain.User(telegram_id=123456, username='testuser')
|
||||
await mock_database.create_user(user)
|
||||
|
||||
target_channel = domain.TargetChannel(
|
||||
telegram_id=-1001234567890,
|
||||
title='Target Channel',
|
||||
username='targetchannel',
|
||||
user_id=user.id,
|
||||
is_active=True,
|
||||
)
|
||||
await mock_database.upsert_target_channel(target_channel)
|
||||
|
||||
external_channel = domain.ExternalChannel(
|
||||
telegram_id=-1009876543210,
|
||||
title='External Channel',
|
||||
username='externalchannel',
|
||||
description=None,
|
||||
subscribers_count=28000,
|
||||
user_id=user.id,
|
||||
)
|
||||
await mock_database.create_external_channel(external_channel)
|
||||
|
||||
creative = domain.Creative(
|
||||
name='Test Creative',
|
||||
text='Test text',
|
||||
target_channel_id=target_channel.id,
|
||||
user_id=user.id,
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
purchases_count=1,
|
||||
)
|
||||
await mock_database.create_creative(creative)
|
||||
|
||||
purchase = domain.Purchase(
|
||||
target_channel_id=target_channel.id,
|
||||
external_channel_id=external_channel.id,
|
||||
creative_id=creative.id,
|
||||
user_id=user.id,
|
||||
placement_date=datetime.datetime.now(datetime.UTC),
|
||||
invite_link='https://t.me/+link1',
|
||||
invite_link_type=domain.InviteLinkType.PUBLIC,
|
||||
status=domain.PurchaseStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_purchase(purchase)
|
||||
|
||||
await usecases.delete_purchase(dto.DeletePurchaseInput(purchase_id=purchase.id, user_id=user.id))
|
||||
|
||||
# Проверяем, что закуп удалён
|
||||
purchases_list = await usecases.get_purchases(dto.GetPurchasesInput(user_id=user.id, include_archived=True))
|
||||
assert len(purchases_list.purchases) == 0
|
||||
|
||||
# Проверяем, что счётчик закупов у креатива уменьшился
|
||||
updated_creative = await mock_database.get_creative(user.id, creative.id)
|
||||
assert updated_creative is not None
|
||||
assert updated_creative.purchases_count == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_purchases_same_creative(usecases: usecase.Usecase, mock_database: MockDatabase) -> None:
|
||||
"""Создание нескольких закупов с одним креативом - для A/B тестирования в разных каналах."""
|
||||
user = domain.User(telegram_id=123456, username='testuser')
|
||||
await mock_database.create_user(user)
|
||||
|
||||
target_channel = domain.TargetChannel(
|
||||
telegram_id=-1001234567890,
|
||||
title='Target Channel',
|
||||
username='targetchannel',
|
||||
user_id=user.id,
|
||||
is_active=True,
|
||||
)
|
||||
await mock_database.upsert_target_channel(target_channel)
|
||||
|
||||
external1 = domain.ExternalChannel(
|
||||
telegram_id=-1009876543210,
|
||||
title='External 1',
|
||||
username='external1',
|
||||
description=None,
|
||||
subscribers_count=10000,
|
||||
user_id=user.id,
|
||||
)
|
||||
external2 = domain.ExternalChannel(
|
||||
telegram_id=-1009876543211,
|
||||
title='External 2',
|
||||
username='external2',
|
||||
description=None,
|
||||
subscribers_count=20000,
|
||||
user_id=user.id,
|
||||
)
|
||||
await mock_database.create_external_channel(external1)
|
||||
await mock_database.create_external_channel(external2)
|
||||
|
||||
creative = domain.Creative(
|
||||
name='Universal Creative',
|
||||
text='Универсальный текст',
|
||||
target_channel_id=target_channel.id,
|
||||
user_id=user.id,
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
)
|
||||
await mock_database.create_creative(creative)
|
||||
|
||||
# Создаём два закупа с одним креативом в разных внешних каналах
|
||||
input1 = dto.CreatePurchaseInput(
|
||||
target_channel_id=target_channel.id,
|
||||
external_channel_id=external1.id,
|
||||
creative_id=creative.id,
|
||||
placement_date=datetime.datetime.now(datetime.UTC),
|
||||
cost=2000.0,
|
||||
)
|
||||
input2 = dto.CreatePurchaseInput(
|
||||
target_channel_id=target_channel.id,
|
||||
external_channel_id=external2.id,
|
||||
creative_id=creative.id,
|
||||
placement_date=datetime.datetime.now(datetime.UTC),
|
||||
cost=4000.0,
|
||||
)
|
||||
|
||||
result1 = await usecases.create_purchase(input1, user.id)
|
||||
result2 = await usecases.create_purchase(input2, user.id)
|
||||
|
||||
# Проверяем, что созданы два разных закупа
|
||||
assert result1.id != result2.id
|
||||
assert result1.invite_link != result2.invite_link
|
||||
|
||||
# Проверяем, что счётчик креатива увеличился на 2
|
||||
updated_creative = await mock_database.get_creative(user.id, creative.id)
|
||||
assert updated_creative is not None
|
||||
assert updated_creative.purchases_count == 2
|
||||
|
||||
# Проверяем фильтрацию по креативу
|
||||
purchases_for_creative = await usecases.get_purchases(
|
||||
dto.GetPurchasesInput(user_id=user.id, creative_id=creative.id)
|
||||
)
|
||||
assert len(purchases_for_creative.purchases) == 2
|
||||
Reference in New Issue
Block a user