Files
RemnaWaveBOT/tests/test_payment_service.py
2026-07-09 16:45:17 +03:00

595 lines
20 KiB
Python

from __future__ import annotations
from datetime import datetime
from uuid import uuid4
import pytest
from sqlalchemy import select
from app.config import Settings
from app.db.models import PaymentOrder, ReferralBonus, ReferralInvite, TelegramUser
from app.services.payment_service import (
FULFILLED_PAYMENT_STATUS,
PENDING_PAYMENT_STATUS,
REFERRAL_BONUS_APPLIED_STATUS,
REFERRAL_BONUS_PENDING_STATUS,
REVIEW_PAYMENT_STATUS,
PaymentService,
)
from app.services.remnawave_client import RemnawaveApiError
from tests.helpers import make_remote_user
class _StubSyncService:
def __init__(self, *, cached_users_by_telegram: dict[int, list] | None = None) -> None:
self.refreshed: list[int] = []
self.cached_users_by_telegram = cached_users_by_telegram or {}
async def get_referral_summary(self, telegram_id: int):
return type(
"ReferralSummaryStub",
(),
{
"applied_referral_code": "",
"referral_code": "",
"total_invited": 0,
"recent_names": [],
},
)()
async def refresh_cached_users_for_telegram(self, *, telegram_id: int):
self.refreshed.append(telegram_id)
return []
async def get_cached_users_for_telegram(self, telegram_id: int):
return list(self.cached_users_by_telegram.get(telegram_id, []))
class _StubRemnawaveClient:
def __init__(self, *, created_user, users_by_telegram_id: dict[int, list] | None = None) -> None:
self.created_user = created_user
self.created_bodies: list[dict[str, object]] = []
self.updated_bodies: list[dict[str, object]] = []
self.users_by_telegram_id = users_by_telegram_id or {}
async def get_user_by_uuid(self, user_uuid: str):
for users in self.users_by_telegram_id.values():
for user in users:
if str(user.uuid) == user_uuid:
return user
raise RemnawaveApiError(status_code=404, message="not found")
async def get_user_by_username(self, username: str):
raise RemnawaveApiError(status_code=404, message="not found")
async def get_users_by_telegram_id(self, telegram_id: int):
return list(self.users_by_telegram_id.get(telegram_id, []))
async def create_user(self, body: dict[str, object]):
self.created_bodies.append(body)
return self.created_user
async def update_user(self, body: dict[str, object]):
self.updated_bodies.append(body)
raw_uuid = str(body.get("uuid") or "")
raw_expire_at = str(body.get("expireAt") or "")
if raw_expire_at.endswith("Z"):
raw_expire_at = raw_expire_at.replace("Z", "+00:00")
next_expire_at = datetime.fromisoformat(raw_expire_at) if raw_expire_at else None
for telegram_id, users in self.users_by_telegram_id.items():
for index, user in enumerate(users):
if str(user.uuid) != raw_uuid:
continue
updated_user = make_remote_user(
user_uuid=str(user.uuid),
user_id=user.id,
short_uuid=user.short_uuid,
username=user.username,
telegram_id=telegram_id,
expire_at=next_expire_at or user.expire_at,
traffic_limit_bytes=user.traffic_limit_bytes,
)
users[index] = updated_user
return updated_user
raise AssertionError("update_user should not be called in this scenario")
async def _seed_order(
session_factory,
*,
telegram_id: int,
status: str,
order_uuid: str | None = None,
) -> str:
resolved_order_uuid = order_uuid or str(uuid4())
async with session_factory() as session:
user = TelegramUser(
telegram_id=telegram_id,
username="tester",
first_name="Test",
language_code="ru",
)
session.add(user)
await session.flush()
session.add(
PaymentOrder(
order_uuid=resolved_order_uuid,
telegram_user_id=user.id,
telegram_id=telegram_id,
plan_code="30d",
plan_title="OREOL VPN на 30 дней",
plan_duration_days=30,
traffic_limit_bytes=0,
traffic_limit_strategy="NO_RESET",
amount_stars=250,
currency="RUB",
status=status,
invoice_payload=f"payload-{resolved_order_uuid}",
provision_username="Oreol-1001-tester",
error_message=None,
)
)
await session.commit()
return resolved_order_uuid
async def _seed_referral_invite(
session_factory,
*,
inviter_telegram_id: int,
invited_telegram_id: int,
) -> None:
async with session_factory() as session:
session.add(
ReferralInvite(
inviter_telegram_id=inviter_telegram_id,
invited_telegram_id=invited_telegram_id,
invited_username="buyer",
invited_display_name="Buyer",
)
)
await session.commit()
def _make_settings() -> Settings:
return Settings.model_construct(
bot_admin_ids_raw="",
payment_internal_squad_uuids_raw="11111111-1111-1111-1111-111111111111",
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,
)
async def test_mark_order_under_review_only_transitions_once(session_factory) -> None:
service = PaymentService(
settings=_make_settings(),
session_factory=session_factory,
remnawave_client=_StubRemnawaveClient(
created_user=make_remote_user(
user_uuid="11111111-1111-1111-1111-111111111111",
user_id=1,
short_uuid="abc123",
username="oreol-user",
telegram_id=1001,
)
),
sync_service=_StubSyncService(),
)
order_uuid = await _seed_order(
session_factory,
telegram_id=1001,
status=PENDING_PAYMENT_STATUS,
)
order = await service.mark_order_under_review(order_uuid=order_uuid, telegram_id=1001)
assert order.status == REVIEW_PAYMENT_STATUS
with pytest.raises(ValueError):
await service.mark_order_under_review(order_uuid=order_uuid, telegram_id=1001)
async def test_approve_order_requires_review_status(session_factory) -> None:
remote_user = make_remote_user(
user_uuid="22222222-2222-2222-2222-222222222222",
user_id=2,
short_uuid="def456",
username="oreol-user",
telegram_id=1001,
)
service = PaymentService(
settings=_make_settings(),
session_factory=session_factory,
remnawave_client=_StubRemnawaveClient(created_user=remote_user),
sync_service=_StubSyncService(),
)
order_uuid = await _seed_order(
session_factory,
telegram_id=1001,
status=PENDING_PAYMENT_STATUS,
)
with pytest.raises(ValueError):
await service.approve_order(order_uuid=order_uuid)
async def test_approve_order_fulfills_and_refreshes_cache(session_factory) -> None:
remote_user = make_remote_user(
user_uuid="33333333-3333-3333-3333-333333333333",
user_id=3,
short_uuid="ghi789",
username="oreol-user",
telegram_id=1001,
)
sync_service = _StubSyncService()
remnawave_client = _StubRemnawaveClient(created_user=remote_user)
service = PaymentService(
settings=_make_settings(),
session_factory=session_factory,
remnawave_client=remnawave_client,
sync_service=sync_service,
)
order_uuid = await _seed_order(
session_factory,
telegram_id=1001,
status=REVIEW_PAYMENT_STATUS,
)
access = await service.approve_order(order_uuid=order_uuid)
assert access.remnawave_user_uuid == str(remote_user.uuid)
assert access.is_renewal is False
assert sync_service.refreshed == [1001]
assert remnawave_client.created_bodies
async with session_factory() as session:
order = await session.scalar(
select(PaymentOrder).where(PaymentOrder.order_uuid == order_uuid)
)
assert order is not None
assert order.status == FULFILLED_PAYMENT_STATUS
assert order.remnawave_user_uuid == str(remote_user.uuid)
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",
user_id=4,
short_uuid="buyer444",
username="oreol-buyer",
telegram_id=1001,
)
inviter_remote_user = make_remote_user(
user_uuid="55555555-5555-5555-5555-555555555555",
user_id=5,
short_uuid="inviter555",
username="oreol-inviter",
telegram_id=2002,
)
sync_service = _StubSyncService()
remnawave_client = _StubRemnawaveClient(
created_user=buyer_remote_user,
users_by_telegram_id={2002: [inviter_remote_user]},
)
service = PaymentService(
settings=_make_settings(),
session_factory=session_factory,
remnawave_client=remnawave_client,
sync_service=sync_service,
)
order_uuid = await _seed_order(
session_factory,
telegram_id=1001,
status=REVIEW_PAYMENT_STATUS,
)
await _seed_referral_invite(
session_factory,
inviter_telegram_id=2002,
invited_telegram_id=1001,
)
access = await service.approve_order(order_uuid=order_uuid)
assert access.referral_bonus is not None
assert access.referral_bonus.inviter_telegram_id == 2002
assert access.referral_bonus.bonus_days == 7
assert access.referral_bonus.status == REFERRAL_BONUS_APPLIED_STATUS
assert sync_service.refreshed == [1001, 2002]
assert any(
body.get("uuid") == str(inviter_remote_user.uuid)
for body in remnawave_client.updated_bodies
)
async with session_factory() as session:
bonus = await session.scalar(
select(ReferralBonus).where(ReferralBonus.source_order_uuid == order_uuid)
)
assert bonus is not None
assert bonus.status == REFERRAL_BONUS_APPLIED_STATUS
async def test_approve_order_keeps_referral_bonus_pending_without_inviter_access(
session_factory,
) -> None:
buyer_remote_user = make_remote_user(
user_uuid="66666666-6666-6666-6666-666666666666",
user_id=6,
short_uuid="buyer666",
username="oreol-buyer-2",
telegram_id=1001,
)
sync_service = _StubSyncService()
remnawave_client = _StubRemnawaveClient(created_user=buyer_remote_user)
service = PaymentService(
settings=_make_settings(),
session_factory=session_factory,
remnawave_client=remnawave_client,
sync_service=sync_service,
)
order_uuid = await _seed_order(
session_factory,
telegram_id=1001,
status=REVIEW_PAYMENT_STATUS,
)
await _seed_referral_invite(
session_factory,
inviter_telegram_id=2002,
invited_telegram_id=1001,
)
access = await service.approve_order(order_uuid=order_uuid)
assert access.referral_bonus is not None
assert access.referral_bonus.status == REFERRAL_BONUS_PENDING_STATUS
assert sync_service.refreshed == [1001]
async with session_factory() as session:
bonus = await session.scalar(
select(ReferralBonus).where(ReferralBonus.source_order_uuid == order_uuid)
)
assert bonus is not None
assert bonus.status == REFERRAL_BONUS_PENDING_STATUS
async def test_create_order_generates_unique_provision_username(session_factory) -> None:
service = PaymentService(
settings=Settings.model_construct(
bot_admin_ids_raw="",
payment_plans_raw="30:250",
payment_transfer_text="Реквизиты",
payment_internal_squad_uuids_raw="11111111-1111-1111-1111-111111111111",
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,
),
session_factory=session_factory,
remnawave_client=_StubRemnawaveClient(
created_user=make_remote_user(
user_uuid="77777777-7777-7777-7777-777777777777",
user_id=7,
short_uuid="buyer777",
username="oreol-user",
telegram_id=1001,
)
),
sync_service=_StubSyncService(),
)
first_order = await service.create_order(
telegram_id=1001,
username="tester",
first_name="Test",
last_name=None,
language_code="ru",
plan_code="30d",
)
second_order = await service.create_order(
telegram_id=1001,
username="tester",
first_name="Test",
last_name=None,
language_code="ru",
plan_code="30d",
)
assert first_order.provision_username != second_order.provision_username
assert first_order.extends_existing_access is False
assert second_order.extends_existing_access is False
async def test_create_order_transliterates_russian_first_name_without_username(
session_factory,
) -> None:
service = PaymentService(
settings=Settings.model_construct(
bot_admin_ids_raw="",
payment_plans_raw="30:250",
payment_transfer_text="Реквизиты",
payment_internal_squad_uuids_raw="11111111-1111-1111-1111-111111111111",
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,
),
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-user",
telegram_id=1001,
)
),
sync_service=_StubSyncService(),
)
order = await service.create_order(
telegram_id=1001,
username=None,
first_name="\u0410\u043b\u0435\u043a\u0441\u0435\u0439",
last_name=None,
language_code="ru",
plan_code="30d",
)
assert order.provision_username.startswith("Oreol-1001-")
assert order.provision_username.endswith("-Aleksey")
assert order.provision_username.isascii()
async def test_create_order_marks_existing_access_as_renewal(session_factory) -> None:
service = PaymentService(
settings=Settings.model_construct(
bot_admin_ids_raw="",
payment_plans_raw="30:250",
payment_transfer_text="Реквизиты",
payment_internal_squad_uuids_raw="11111111-1111-1111-1111-111111111111",
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,
),
session_factory=session_factory,
remnawave_client=_StubRemnawaveClient(
created_user=make_remote_user(
user_uuid="77777777-7777-7777-7777-777777777777",
user_id=7,
short_uuid="buyer777",
username="oreol-user",
telegram_id=1001,
)
),
sync_service=_StubSyncService(cached_users_by_telegram={1001: [object()]}),
)
order = await service.create_order(
telegram_id=1001,
username="tester",
first_name="Test",
last_name=None,
language_code="ru",
plan_code="30d",
)
assert order.extends_existing_access is True
assert "продлит текущую подписку" in service.build_transfer_text(order=order)
async def test_create_order_targets_external_subscription_linked_by_telegram_id(
session_factory,
) -> None:
existing_remote_user = make_remote_user(
user_uuid="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
user_id=10,
short_uuid="external10",
username="custom-external",
telegram_id=1001,
)
remnawave_client = _StubRemnawaveClient(
created_user=existing_remote_user,
users_by_telegram_id={1001: [existing_remote_user]},
)
service = PaymentService(
settings=Settings.model_construct(
bot_admin_ids_raw="",
payment_plans_raw="30:250",
payment_transfer_text="Реквизиты",
payment_internal_squad_uuids_raw="11111111-1111-1111-1111-111111111111",
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,
),
session_factory=session_factory,
remnawave_client=remnawave_client,
sync_service=_StubSyncService(),
)
order = await service.create_order(
telegram_id=1001,
username="buyer",
first_name="Buyer",
last_name=None,
language_code="ru",
plan_code="30d",
)
assert order.extends_existing_access is True
assert order.provision_username == "custom-external"
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.remnawave_user_uuid == str(existing_remote_user.uuid)
await service.mark_order_under_review(order_uuid=order.order_uuid, telegram_id=1001)
access = await service.approve_order(order_uuid=order.order_uuid)
assert access.is_renewal is True
assert access.username == "custom-external"
assert remnawave_client.created_bodies == []
assert remnawave_client.updated_bodies[-1]["username"] == "custom-external"
async def test_approve_order_extends_existing_user_found_by_telegram_id(session_factory) -> None:
existing_remote_user = make_remote_user(
user_uuid="88888888-8888-8888-8888-888888888888",
user_id=8,
short_uuid="existing888",
username="oreol-existing",
telegram_id=1001,
)
sync_service = _StubSyncService()
remnawave_client = _StubRemnawaveClient(
created_user=existing_remote_user,
users_by_telegram_id={1001: [existing_remote_user]},
)
service = PaymentService(
settings=_make_settings(),
session_factory=session_factory,
remnawave_client=remnawave_client,
sync_service=sync_service,
)
order_uuid = await _seed_order(
session_factory,
telegram_id=1001,
status=REVIEW_PAYMENT_STATUS,
)
access = await service.approve_order(order_uuid=order_uuid)
assert access.remnawave_user_uuid == str(existing_remote_user.uuid)
assert access.is_renewal is True
assert remnawave_client.created_bodies == []
assert remnawave_client.updated_bodies