RemnaWave-bot Update
This commit is contained in:
@@ -19,11 +19,14 @@ def _make_settings() -> Settings:
|
||||
referral_discount_percent=5,
|
||||
referral_bonus_days=7,
|
||||
payment_plans_raw="30:250,180:600",
|
||||
payment_product_plans_raw="",
|
||||
payment_transfer_text="Реквизиты",
|
||||
payment_support_text="Напишите в поддержку",
|
||||
payment_plan_traffic_limit_gb=0,
|
||||
payment_plan_traffic_reset_period="NO_RESET",
|
||||
payment_internal_squad_uuids_raw="uuid-1",
|
||||
payment_vpn_squad_uuids_raw="vpn-uuid",
|
||||
payment_white_squad_uuids_raw="white-uuid",
|
||||
payment_external_squad_uuid="",
|
||||
payment_username_prefix="Oreol",
|
||||
payment_user_tag="BOT",
|
||||
@@ -46,6 +49,11 @@ async def test_bot_config_service_updates_runtime_snapshot(session_factory) -> N
|
||||
raw_value="30:300,90:700",
|
||||
updated_by_telegram_id=1,
|
||||
)
|
||||
await service.update_setting(
|
||||
key="payment_product_plans_raw",
|
||||
raw_value="vpn_white:30:450,white:30:250,vpn:30:200",
|
||||
updated_by_telegram_id=1,
|
||||
)
|
||||
await service.update_setting(
|
||||
key="referral_enabled",
|
||||
raw_value="off",
|
||||
@@ -56,9 +64,10 @@ async def test_bot_config_service_updates_runtime_snapshot(session_factory) -> N
|
||||
|
||||
assert snapshot.bot_brand_name == "NEW BRAND"
|
||||
assert snapshot.referral_enabled is False
|
||||
assert [(plan.days, plan.amount_rub) for plan in snapshot.payment_plans] == [
|
||||
(30, 300),
|
||||
(90, 700),
|
||||
assert [(plan.code, plan.days, plan.amount_rub) for plan in snapshot.payment_plans] == [
|
||||
("vpn_white", 30, 450),
|
||||
("white", 30, 250),
|
||||
("vpn", 30, 200),
|
||||
]
|
||||
|
||||
|
||||
@@ -72,3 +81,4 @@ async def test_bot_config_service_formats_values_for_admin_ui(session_factory) -
|
||||
assert BotConfigService.format_value(snapshot=snapshot, key="bot_public_username") == "@oreol_vpn_bot"
|
||||
assert BotConfigService.format_value(snapshot=snapshot, key="referral_enabled") == "on"
|
||||
assert BotConfigService.format_value(snapshot=snapshot, key="payment_plan_traffic_limit_gb") == "0 GB (unlimited)"
|
||||
assert BotConfigService.format_value(snapshot=snapshot, key="payment_vpn_squad_uuids_raw") == "vpn-uuid"
|
||||
|
||||
@@ -79,3 +79,22 @@ def test_payment_plans_are_parsed() -> None:
|
||||
assert [plan.code for plan in plans] == ["30d", "180d", "365d"]
|
||||
assert [plan.days for plan in plans] == [30, 180, 365]
|
||||
assert [plan.amount_rub for plan in plans] == [250, 600, 1000]
|
||||
|
||||
|
||||
def test_payment_product_plans_override_legacy_plans() -> None:
|
||||
settings = Settings.model_construct(
|
||||
payment_product_plans_raw="vpn_white:30:450,white:30:250,vpn:30:200",
|
||||
payment_plans_raw="30:999",
|
||||
payment_plan_duration_days=30,
|
||||
)
|
||||
|
||||
plans = settings.payment_plans
|
||||
|
||||
assert [plan.code for plan in plans] == ["vpn_white", "white", "vpn"]
|
||||
assert [plan.title for plan in plans] == ["VPN + Белые списки", "Белые списки", "VPN"]
|
||||
assert [plan.amount_rub for plan in plans] == [450, 250, 200]
|
||||
assert [plan.squad_groups for plan in plans] == [
|
||||
("vpn", "white"),
|
||||
("white",),
|
||||
("vpn",),
|
||||
]
|
||||
|
||||
@@ -158,7 +158,8 @@ def test_admin_panel_keyboard_has_expected_buttons() -> None:
|
||||
assert "👤 Пользователи" in button_texts
|
||||
assert "🔄 Полная синхронизация" in button_texts
|
||||
assert "💸 Очередь оплат" in button_texts
|
||||
assert "🎫 Тикеты" in button_texts
|
||||
assert "🗂 История тикетов" in button_texts
|
||||
assert "🎫 Чат тикетов" in button_texts
|
||||
assert "↻ Обновить" in button_texts
|
||||
assert "🏠 Главное меню" in button_texts
|
||||
|
||||
|
||||
@@ -21,9 +21,15 @@ from tests.helpers import make_remote_user
|
||||
|
||||
|
||||
class _StubSyncService:
|
||||
def __init__(self, *, cached_users_by_telegram: dict[int, list] | None = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
cached_users_by_telegram: dict[int, list] | None = None,
|
||||
active_promo_by_telegram: dict[int, object] | None = None,
|
||||
) -> None:
|
||||
self.refreshed: list[int] = []
|
||||
self.cached_users_by_telegram = cached_users_by_telegram or {}
|
||||
self.active_promo_by_telegram = active_promo_by_telegram or {}
|
||||
|
||||
async def get_referral_summary(self, telegram_id: int):
|
||||
return type(
|
||||
@@ -44,6 +50,9 @@ class _StubSyncService:
|
||||
async def get_cached_users_for_telegram(self, telegram_id: int):
|
||||
return list(self.cached_users_by_telegram.get(telegram_id, []))
|
||||
|
||||
async def get_active_promo_for_user(self, telegram_id: int):
|
||||
return self.active_promo_by_telegram.get(telegram_id)
|
||||
|
||||
|
||||
class _StubRemnawaveClient:
|
||||
def __init__(self, *, created_user, users_by_telegram_id: dict[int, list] | None = None) -> None:
|
||||
@@ -98,6 +107,9 @@ async def _seed_order(
|
||||
telegram_id: int,
|
||||
status: str,
|
||||
order_uuid: str | None = None,
|
||||
plan_code: str = "30d",
|
||||
plan_title: str = "OREOL VPN на 30 дней",
|
||||
amount_stars: int = 250,
|
||||
) -> str:
|
||||
resolved_order_uuid = order_uuid or str(uuid4())
|
||||
|
||||
@@ -116,12 +128,12 @@ async def _seed_order(
|
||||
order_uuid=resolved_order_uuid,
|
||||
telegram_user_id=user.id,
|
||||
telegram_id=telegram_id,
|
||||
plan_code="30d",
|
||||
plan_title="OREOL VPN на 30 дней",
|
||||
plan_code=plan_code,
|
||||
plan_title=plan_title,
|
||||
plan_duration_days=30,
|
||||
traffic_limit_bytes=0,
|
||||
traffic_limit_strategy="NO_RESET",
|
||||
amount_stars=250,
|
||||
amount_stars=amount_stars,
|
||||
currency="RUB",
|
||||
status=status,
|
||||
invoice_payload=f"payload-{resolved_order_uuid}",
|
||||
@@ -155,7 +167,29 @@ async def _seed_referral_invite(
|
||||
def _make_settings() -> Settings:
|
||||
return Settings.model_construct(
|
||||
bot_admin_ids_raw="",
|
||||
payment_product_plans_raw="",
|
||||
payment_internal_squad_uuids_raw="11111111-1111-1111-1111-111111111111",
|
||||
payment_vpn_squad_uuids_raw="",
|
||||
payment_white_squad_uuids_raw="",
|
||||
payment_user_tag="BOT",
|
||||
payment_external_squad_uuid="",
|
||||
payment_username_prefix="Oreol",
|
||||
payment_plan_traffic_limit_gb=0,
|
||||
payment_plan_traffic_reset_period="NO_RESET",
|
||||
referral_discount_percent=5,
|
||||
referral_bonus_days=7,
|
||||
)
|
||||
|
||||
|
||||
def _make_product_settings() -> Settings:
|
||||
return Settings.model_construct(
|
||||
bot_admin_ids_raw="",
|
||||
payment_plans_raw="30:999",
|
||||
payment_product_plans_raw="vpn_white:30:450,white:30:250,vpn:30:200",
|
||||
payment_transfer_text="Реквизиты",
|
||||
payment_internal_squad_uuids_raw="",
|
||||
payment_vpn_squad_uuids_raw="11111111-1111-1111-1111-111111111111",
|
||||
payment_white_squad_uuids_raw="22222222-2222-2222-2222-222222222222",
|
||||
payment_user_tag="BOT",
|
||||
payment_external_squad_uuid="",
|
||||
payment_username_prefix="Oreol",
|
||||
@@ -258,6 +292,37 @@ async def test_approve_order_fulfills_and_refreshes_cache(session_factory) -> No
|
||||
assert order.remnawave_user_uuid == str(remote_user.uuid)
|
||||
|
||||
|
||||
async def test_approve_product_order_sends_product_squads_to_remnawave(session_factory) -> None:
|
||||
remote_user = make_remote_user(
|
||||
user_uuid="99999999-9999-9999-9999-999999999999",
|
||||
user_id=9,
|
||||
short_uuid="product999",
|
||||
username="oreol-product",
|
||||
telegram_id=1001,
|
||||
)
|
||||
remnawave_client = _StubRemnawaveClient(created_user=remote_user)
|
||||
service = PaymentService(
|
||||
settings=_make_product_settings(),
|
||||
session_factory=session_factory,
|
||||
remnawave_client=remnawave_client,
|
||||
sync_service=_StubSyncService(),
|
||||
)
|
||||
order_uuid = await _seed_order(
|
||||
session_factory,
|
||||
telegram_id=1001,
|
||||
status=REVIEW_PAYMENT_STATUS,
|
||||
plan_code="white",
|
||||
plan_title="OREOL VPN · Белые списки",
|
||||
amount_stars=250,
|
||||
)
|
||||
|
||||
await service.approve_order(order_uuid=order_uuid)
|
||||
|
||||
assert remnawave_client.created_bodies[0]["activeInternalSquads"] == [
|
||||
"22222222-2222-2222-2222-222222222222",
|
||||
]
|
||||
|
||||
|
||||
async def test_approve_order_grants_and_applies_referral_bonus(session_factory) -> None:
|
||||
buyer_remote_user = make_remote_user(
|
||||
user_uuid="44444444-4444-4444-4444-444444444444",
|
||||
@@ -410,6 +475,95 @@ async def test_create_order_generates_unique_provision_username(session_factory)
|
||||
assert second_order.extends_existing_access is False
|
||||
|
||||
|
||||
async def test_product_plan_uses_matching_squads_and_price(session_factory) -> None:
|
||||
service = PaymentService(
|
||||
settings=_make_product_settings(),
|
||||
session_factory=session_factory,
|
||||
remnawave_client=_StubRemnawaveClient(
|
||||
created_user=make_remote_user(
|
||||
user_uuid="99999999-9999-9999-9999-999999999999",
|
||||
user_id=9,
|
||||
short_uuid="buyer999",
|
||||
username="oreol-product",
|
||||
telegram_id=1001,
|
||||
)
|
||||
),
|
||||
sync_service=_StubSyncService(),
|
||||
)
|
||||
|
||||
order = await service.create_order(
|
||||
telegram_id=1001,
|
||||
username="tester",
|
||||
first_name="Test",
|
||||
last_name=None,
|
||||
language_code="ru",
|
||||
plan_code="vpn_white",
|
||||
)
|
||||
|
||||
assert order.plan.label == "VPN + Белые списки"
|
||||
assert order.plan.amount_rub == 450
|
||||
assert order.plan.internal_squad_uuids == [
|
||||
"11111111-1111-1111-1111-111111111111",
|
||||
"22222222-2222-2222-2222-222222222222",
|
||||
]
|
||||
|
||||
|
||||
async def test_promo_code_discounts_only_matching_plan(session_factory) -> None:
|
||||
promo = type(
|
||||
"PromoStub",
|
||||
(),
|
||||
{
|
||||
"code": "WHITE50",
|
||||
"discount_percent": 50,
|
||||
"plan_code": "white",
|
||||
"expires_at": None,
|
||||
},
|
||||
)()
|
||||
service = PaymentService(
|
||||
settings=_make_product_settings(),
|
||||
session_factory=session_factory,
|
||||
remnawave_client=_StubRemnawaveClient(
|
||||
created_user=make_remote_user(
|
||||
user_uuid="10101010-1010-1010-1010-101010101010",
|
||||
user_id=10,
|
||||
short_uuid="buyer101",
|
||||
username="oreol-promo",
|
||||
telegram_id=1001,
|
||||
)
|
||||
),
|
||||
sync_service=_StubSyncService(active_promo_by_telegram={1001: promo}),
|
||||
)
|
||||
|
||||
plans = await service.get_available_plans(telegram_id=1001)
|
||||
|
||||
assert [(plan.code, plan.amount_rub, plan.discount_code) for plan in plans] == [
|
||||
("vpn_white", 450, ""),
|
||||
("white", 125, "WHITE50"),
|
||||
("vpn", 200, ""),
|
||||
]
|
||||
|
||||
order = await service.create_order(
|
||||
telegram_id=1001,
|
||||
username="tester",
|
||||
first_name="Test",
|
||||
last_name=None,
|
||||
language_code="ru",
|
||||
plan_code="white",
|
||||
)
|
||||
|
||||
assert order.plan.amount_rub == 125
|
||||
assert order.plan.discount_source == "promo"
|
||||
|
||||
async with session_factory() as session:
|
||||
stored_order = await session.scalar(
|
||||
select(PaymentOrder).where(PaymentOrder.order_uuid == order.order_uuid)
|
||||
)
|
||||
|
||||
assert stored_order is not None
|
||||
assert stored_order.amount_stars == 125
|
||||
assert stored_order.error_message == "WHITE50"
|
||||
|
||||
|
||||
async def test_create_order_marks_existing_access_as_renewal(session_factory) -> None:
|
||||
service = PaymentService(
|
||||
settings=Settings.model_construct(
|
||||
|
||||
104
tests/test_support_ticket_service.py
Normal file
104
tests/test_support_ticket_service.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from app.services.support_ticket_service import (
|
||||
ANSWERED_SUPPORT_TICKET_STATUS,
|
||||
CLOSED_SUPPORT_TICKET_STATUS,
|
||||
OPEN_SUPPORT_TICKET_STATUS,
|
||||
SupportTicketService,
|
||||
)
|
||||
|
||||
|
||||
async def test_support_ticket_lifecycle(session_factory) -> None:
|
||||
service = SupportTicketService(session_factory=session_factory)
|
||||
|
||||
ticket = await service.create_ticket(
|
||||
public_id="ABCD1234",
|
||||
telegram_id=1001,
|
||||
username="tester",
|
||||
display_name="Test User",
|
||||
user_message="Need help",
|
||||
support_chat_id=-1001234567890,
|
||||
support_thread_id=12,
|
||||
support_message_id=345,
|
||||
)
|
||||
|
||||
assert ticket.status == OPEN_SUPPORT_TICKET_STATUS
|
||||
assert await service.get_open_tickets_count() == 1
|
||||
|
||||
answered_ticket = await service.mark_answered(ticket_id=ticket.id)
|
||||
assert answered_ticket is not None
|
||||
assert answered_ticket.status == ANSWERED_SUPPORT_TICKET_STATUS
|
||||
assert await service.get_open_tickets_count() == 1
|
||||
|
||||
noted_ticket = await service.append_note(
|
||||
ticket_id=ticket.id,
|
||||
note_text="First note",
|
||||
status=OPEN_SUPPORT_TICKET_STATUS,
|
||||
)
|
||||
assert noted_ticket is not None
|
||||
assert noted_ticket.support_note == "First note"
|
||||
assert noted_ticket.status == OPEN_SUPPORT_TICKET_STATUS
|
||||
|
||||
appended_ticket = await service.append_note(ticket_id=ticket.id, note_text="Second note")
|
||||
assert appended_ticket is not None
|
||||
assert appended_ticket.support_note == "First note\n\nSecond note"
|
||||
|
||||
closed_ticket = await service.close_ticket(
|
||||
ticket_id=ticket.id,
|
||||
closed_by_telegram_id=9999,
|
||||
)
|
||||
assert closed_ticket is not None
|
||||
assert closed_ticket.status == CLOSED_SUPPORT_TICKET_STATUS
|
||||
assert closed_ticket.closed_by_telegram_id == 9999
|
||||
assert closed_ticket.closed_at is not None
|
||||
assert await service.get_open_tickets_count() == 0
|
||||
|
||||
still_closed_ticket = await service.mark_answered(ticket_id=ticket.id)
|
||||
assert still_closed_ticket is not None
|
||||
assert still_closed_ticket.status == CLOSED_SUPPORT_TICKET_STATUS
|
||||
|
||||
|
||||
async def test_get_ticket_by_support_message_returns_ticket(session_factory) -> None:
|
||||
service = SupportTicketService(session_factory=session_factory)
|
||||
|
||||
created_ticket = await service.create_ticket(
|
||||
public_id="EFGH5678",
|
||||
telegram_id=2002,
|
||||
username=None,
|
||||
display_name="Another User",
|
||||
user_message="Another issue",
|
||||
support_chat_id=-1009876543210,
|
||||
support_thread_id=None,
|
||||
support_message_id=777,
|
||||
)
|
||||
|
||||
loaded_ticket = await service.get_ticket_by_support_message(
|
||||
support_chat_id=-1009876543210,
|
||||
support_message_id=777,
|
||||
)
|
||||
|
||||
assert loaded_ticket is not None
|
||||
assert loaded_ticket.id == created_ticket.id
|
||||
assert loaded_ticket.public_id == "EFGH5678"
|
||||
|
||||
|
||||
async def test_get_admin_tickets_page_returns_latest_first(session_factory) -> None:
|
||||
service = SupportTicketService(session_factory=session_factory)
|
||||
|
||||
for index in range(3):
|
||||
await service.create_ticket(
|
||||
public_id=f"TICKET{index}",
|
||||
telegram_id=3000 + index,
|
||||
username=f"user{index}",
|
||||
display_name=f"User {index}",
|
||||
user_message=f"Message {index}",
|
||||
support_chat_id=-100500,
|
||||
support_thread_id=None,
|
||||
support_message_id=900 + index,
|
||||
)
|
||||
|
||||
first_page = await service.get_admin_tickets_page(page=1, page_size=2)
|
||||
second_page = await service.get_admin_tickets_page(page=2, page_size=2)
|
||||
|
||||
assert first_page.total_items == 3
|
||||
assert first_page.total_pages == 2
|
||||
assert [item.public_id for item in first_page.items] == ["TICKET2", "TICKET1"]
|
||||
assert [item.public_id for item in second_page.items] == ["TICKET0"]
|
||||
@@ -1,11 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.config import Settings
|
||||
from app.db.models import ReferralCode, ReferralInvite, RemnawaveUser, TelegramUser
|
||||
from app.db.base import utcnow
|
||||
from app.db.models import (
|
||||
PromoCode,
|
||||
PromoCodeApplication,
|
||||
ReferralCode,
|
||||
ReferralInvite,
|
||||
RemnawaveUser,
|
||||
TelegramUser,
|
||||
)
|
||||
from app.schemas.remnawave import PaginatedUsers
|
||||
from app.services.sync_service import SyncService
|
||||
from tests.helpers import make_remote_user
|
||||
|
||||
@@ -17,6 +26,10 @@ class _StubRemnawaveClient:
|
||||
async def get_users_by_telegram_id(self, telegram_id: int):
|
||||
return list(self.users)
|
||||
|
||||
async def get_all_users(self, *, start: int = 0, size: int = 100):
|
||||
items = list(self.users)[start : start + size]
|
||||
return PaginatedUsers(users=items, total=len(self.users))
|
||||
|
||||
|
||||
async def test_refresh_cached_users_clears_stale_links_and_keeps_profile(
|
||||
session_factory,
|
||||
@@ -206,3 +219,103 @@ async def test_get_admin_telegram_user_profile_returns_referral_and_access_data(
|
||||
assert profile.recent_invited_names == ["Child User"]
|
||||
assert len(profile.accesses) == 1
|
||||
assert profile.accesses[0].record.username == "oreol-profile"
|
||||
|
||||
|
||||
async def test_promo_code_can_be_applied_through_referral_input(session_factory) -> None:
|
||||
service = SyncService(
|
||||
settings=Settings.model_construct(
|
||||
bot_admin_ids_raw="",
|
||||
sync_subscription_history=False,
|
||||
),
|
||||
session_factory=session_factory,
|
||||
remnawave_client=_StubRemnawaveClient([]),
|
||||
)
|
||||
|
||||
promo = await service.create_promo_code(
|
||||
code="white50",
|
||||
plan_code="white",
|
||||
discount_percent=50,
|
||||
expires_at=utcnow() + timedelta(days=7),
|
||||
created_by_telegram_id=1001,
|
||||
)
|
||||
applied_code = await service.apply_referral_code(
|
||||
telegram_id=2001,
|
||||
username="buyer",
|
||||
first_name="Buyer",
|
||||
last_name=None,
|
||||
language_code="ru",
|
||||
referral_code="white50",
|
||||
)
|
||||
summary = await service.get_referral_summary(2001)
|
||||
active_promo = await service.get_active_promo_for_user(2001)
|
||||
|
||||
assert promo.code == "WHITE50"
|
||||
assert applied_code == "WHITE50"
|
||||
assert summary.applied_referral_code == ""
|
||||
assert summary.applied_promo_code == "WHITE50"
|
||||
assert summary.applied_promo_discount_percent == 50
|
||||
assert summary.applied_promo_plan_code == "white"
|
||||
assert active_promo is not None
|
||||
assert active_promo.code == "WHITE50"
|
||||
|
||||
async with session_factory() as session:
|
||||
promo_record = await session.scalar(
|
||||
select(PromoCode).where(PromoCode.code == "WHITE50")
|
||||
)
|
||||
application = await session.scalar(
|
||||
select(PromoCodeApplication).where(PromoCodeApplication.telegram_id == 2001)
|
||||
)
|
||||
|
||||
assert promo_record is not None
|
||||
assert application is not None
|
||||
assert application.promo_code_id == promo_record.id
|
||||
|
||||
|
||||
async def test_sync_all_users_does_not_autoflush_half_built_user(
|
||||
session_factory,
|
||||
) -> None:
|
||||
remote_user = make_remote_user(
|
||||
user_uuid="77777777-7777-7777-7777-777777777777",
|
||||
user_id=7,
|
||||
short_uuid="sync-all-user",
|
||||
username="oreol-sync-all",
|
||||
telegram_id=4001,
|
||||
)
|
||||
service = SyncService(
|
||||
settings=Settings.model_construct(
|
||||
bot_admin_ids_raw="",
|
||||
sync_subscription_history=False,
|
||||
),
|
||||
session_factory=session_factory,
|
||||
remnawave_client=_StubRemnawaveClient([remote_user]),
|
||||
)
|
||||
|
||||
async with session_factory() as session:
|
||||
session.add(
|
||||
TelegramUser(
|
||||
telegram_id=4001,
|
||||
username="owner",
|
||||
first_name="Sync",
|
||||
last_name="Owner",
|
||||
language_code="ru",
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
total = await service.sync_all_users(batch_size=50)
|
||||
|
||||
assert total == 1
|
||||
|
||||
async with session_factory() as session:
|
||||
owner = await session.scalar(
|
||||
select(TelegramUser).where(TelegramUser.telegram_id == 4001)
|
||||
)
|
||||
cached_user = await session.scalar(
|
||||
select(RemnawaveUser).where(RemnawaveUser.rw_uuid == str(remote_user.uuid))
|
||||
)
|
||||
|
||||
assert owner is not None
|
||||
assert cached_user is not None
|
||||
assert cached_user.rw_id == 7
|
||||
assert cached_user.owner_telegram_user_id == owner.id
|
||||
assert cached_user.username == "oreol-sync-all"
|
||||
|
||||
Reference in New Issue
Block a user