Keep one-off device top-ups scoped to the active subscription term and move device renewal into subscription checkout. Carry HWID renewal metadata through provider callbacks and webhooks, including YooKassa saved-card flows. Add admin extension controls, docs, demo data, and regression coverage.
285 lines
11 KiB
Python
285 lines
11 KiB
Python
from datetime import datetime, timezone
|
|
from types import SimpleNamespace
|
|
from unittest import IsolatedAsyncioTestCase
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from bot.payment_providers import yookassa
|
|
|
|
|
|
class _I18n:
|
|
def gettext(self, _lang, key, **kwargs):
|
|
if key == "payment_successful_hwid_devices_full":
|
|
return f"HWID +{kwargs['count']}"
|
|
if key == "config_link_not_available":
|
|
return "n/a"
|
|
return key
|
|
|
|
|
|
class YooKassaHwidWebhookTests(IsolatedAsyncioTestCase):
|
|
async def test_telegram_subscription_hwid_quote_is_stored_in_yookassa_metadata(self):
|
|
valid_from = datetime(2099, 2, 1, tzinfo=timezone.utc)
|
|
valid_until = datetime(2099, 3, 1, tzinfo=timezone.utc)
|
|
session = AsyncMock()
|
|
callback = SimpleNamespace(
|
|
from_user=SimpleNamespace(id=42),
|
|
message=SimpleNamespace(edit_text=AsyncMock()),
|
|
)
|
|
service = SimpleNamespace(
|
|
config=SimpleNamespace(DEFAULT_RECEIPT_EMAIL="receipt@example.test"),
|
|
create_payment=AsyncMock(
|
|
return_value={
|
|
"id": "yk-pay-1",
|
|
"status": "pending",
|
|
"confirmation_url": "https://pay.example.test/1",
|
|
}
|
|
),
|
|
)
|
|
hwid_quote = {
|
|
"device_count": 2,
|
|
"valid_from": valid_from,
|
|
"valid_until": valid_until,
|
|
"pricing_period_months": 1,
|
|
"proration_ratio": 1.0,
|
|
"full_price": 50.0,
|
|
}
|
|
payment = SimpleNamespace(payment_id=123)
|
|
|
|
with (
|
|
patch.object(
|
|
yookassa.payment_dal,
|
|
"create_payment_record",
|
|
AsyncMock(return_value=payment),
|
|
) as create_record,
|
|
patch.object(
|
|
yookassa.payment_dal,
|
|
"update_payment_status_by_db_id",
|
|
AsyncMock(),
|
|
),
|
|
):
|
|
result = await yookassa._initiate_yk_payment(
|
|
callback,
|
|
settings=SimpleNamespace(),
|
|
session=session,
|
|
yookassa_service=service,
|
|
i18n=_I18n(),
|
|
current_lang="en",
|
|
get_text=lambda key, **kwargs: key,
|
|
user_id=42,
|
|
months=1,
|
|
price_rub=150,
|
|
currency_code_for_yk="RUB",
|
|
save_payment_method=False,
|
|
back_callback="tariff:period:standard:1",
|
|
sale_mode="subscription@standard|hwid_renewal",
|
|
hwid_quote=hwid_quote,
|
|
)
|
|
|
|
assert result is True
|
|
record_payload = create_record.await_args.args[1]
|
|
assert record_payload["sale_mode"] == "subscription@standard|hwid_renewal"
|
|
assert record_payload["tariff_key"] == "standard"
|
|
assert record_payload["purchased_hwid_devices"] == 2
|
|
assert record_payload["hwid_valid_from"] == valid_from
|
|
assert record_payload["hwid_valid_until"] == valid_until
|
|
metadata = service.create_payment.await_args.kwargs["metadata"]
|
|
assert metadata["sale_mode"] == "subscription@standard|hwid_renewal"
|
|
assert metadata["hwid_devices"] == "2"
|
|
assert metadata["hwid_valid_from"] == valid_from.isoformat()
|
|
assert metadata["hwid_valid_until"] == valid_until.isoformat()
|
|
assert metadata["hwid_pricing_period_months"] == "1"
|
|
assert metadata["hwid_proration_ratio"] == "1.0"
|
|
assert metadata["hwid_full_price"] == "50.0"
|
|
assert service.create_payment.await_args.kwargs["amount"] == 150
|
|
|
|
async def test_webapp_hwid_metadata_activates_device_count_without_end_date(self):
|
|
payment = SimpleNamespace(payment_id=5, status="pending_yookassa", tariff_key="standard")
|
|
updated_payment = SimpleNamespace(payment_id=5, status="succeeded", tariff_key="standard")
|
|
db_user = SimpleNamespace(
|
|
user_id=42,
|
|
username="alice",
|
|
language_code="en",
|
|
referred_by_id=None,
|
|
)
|
|
subscription_service = SimpleNamespace(
|
|
activate_subscription=AsyncMock(
|
|
return_value={
|
|
"subscription_id": 11,
|
|
"purchased_hwid_devices": 2,
|
|
}
|
|
)
|
|
)
|
|
settings = SimpleNamespace(
|
|
traffic_sale_mode=False,
|
|
yookassa_autopayments_active=False,
|
|
DEFAULT_LANGUAGE="en",
|
|
DEFAULT_CURRENCY_SYMBOL="RUB",
|
|
LKNPD_RECEIPT_NAME_TRAFFIC="{gb} GB",
|
|
LKNPD_RECEIPT_NAME_SUBSCRIPTION="{months} months",
|
|
)
|
|
payment_info = {
|
|
"id": "yk-hwid-1",
|
|
"status": "succeeded",
|
|
"paid": True,
|
|
"amount": {"value": "120.00", "currency": "RUB"},
|
|
"metadata": {
|
|
"user_id": "42",
|
|
"subscription_months": "0",
|
|
"payment_db_id": "5",
|
|
"sale_mode": "hwid_devices@standard",
|
|
"hwid_devices": "2",
|
|
"source": "webapp",
|
|
},
|
|
"description": "Extra HWID devices +2",
|
|
}
|
|
|
|
with (
|
|
patch.object(
|
|
yookassa.payment_dal,
|
|
"get_payment_by_db_id",
|
|
AsyncMock(return_value=payment),
|
|
),
|
|
patch.object(
|
|
yookassa.payment_dal,
|
|
"update_payment_status_by_db_id",
|
|
AsyncMock(return_value=updated_payment),
|
|
) as update_status,
|
|
patch.object(yookassa.user_dal, "get_user_by_id", AsyncMock(return_value=db_user)),
|
|
patch.object(
|
|
yookassa,
|
|
"prepare_config_links",
|
|
AsyncMock(return_value=("link", "https://example.test/sub")),
|
|
),
|
|
patch.object(
|
|
yookassa,
|
|
"ensure_user_install_guide_links",
|
|
AsyncMock(return_value=SimpleNamespace(public_share_url=None)),
|
|
),
|
|
patch.object(yookassa, "send_success_message_to_user", AsyncMock()) as send_success,
|
|
patch.object(yookassa, "notify_admins_payment_received", AsyncMock()),
|
|
):
|
|
await yookassa.process_successful_payment(
|
|
AsyncMock(),
|
|
AsyncMock(),
|
|
payment_info,
|
|
_I18n(),
|
|
settings,
|
|
AsyncMock(),
|
|
subscription_service,
|
|
AsyncMock(),
|
|
)
|
|
|
|
activation_args = subscription_service.activate_subscription.await_args.args
|
|
activation_kwargs = subscription_service.activate_subscription.await_args.kwargs
|
|
assert activation_args[2] == 2
|
|
assert activation_kwargs["sale_mode"] == "hwid_devices@standard"
|
|
assert activation_kwargs["traffic_gb"] is None
|
|
update_status.assert_awaited_once()
|
|
send_success.assert_awaited_once()
|
|
|
|
async def test_auto_renew_hwid_metadata_is_persisted_for_activation(self):
|
|
valid_from = datetime(2099, 2, 1, tzinfo=timezone.utc)
|
|
valid_until = datetime(2099, 3, 1, tzinfo=timezone.utc)
|
|
payment = SimpleNamespace(payment_id=5, status="pending_yookassa", tariff_key="standard")
|
|
updated_payment = SimpleNamespace(payment_id=5, status="succeeded", tariff_key="standard")
|
|
db_user = SimpleNamespace(
|
|
user_id=42,
|
|
username="alice",
|
|
language_code="en",
|
|
referred_by_id=None,
|
|
)
|
|
subscription_service = SimpleNamespace(
|
|
activate_subscription=AsyncMock(
|
|
return_value={
|
|
"subscription_id": 11,
|
|
"end_date": valid_until,
|
|
"hwid_devices_renewed_count": 2,
|
|
"hwid_devices_valid_until": valid_until,
|
|
}
|
|
)
|
|
)
|
|
referral_service = SimpleNamespace(
|
|
apply_referral_bonuses_for_payment=AsyncMock(return_value={})
|
|
)
|
|
settings = SimpleNamespace(
|
|
traffic_sale_mode=False,
|
|
yookassa_autopayments_active=False,
|
|
DEFAULT_LANGUAGE="en",
|
|
DEFAULT_CURRENCY_SYMBOL="RUB",
|
|
LKNPD_RECEIPT_NAME_TRAFFIC="{gb} GB",
|
|
LKNPD_RECEIPT_NAME_SUBSCRIPTION="{months} months",
|
|
)
|
|
payment_info = {
|
|
"id": "yk-auto-hwid-1",
|
|
"status": "succeeded",
|
|
"paid": True,
|
|
"amount": {"value": "449.00", "currency": "RUB"},
|
|
"metadata": {
|
|
"user_id": "42",
|
|
"subscription_months": "1",
|
|
"auto_renew_for_subscription_id": "555",
|
|
"sale_mode": "subscription@standard",
|
|
"hwid_devices": "2",
|
|
"hwid_valid_from": valid_from.isoformat(),
|
|
"hwid_valid_until": valid_until.isoformat(),
|
|
"hwid_pricing_period_months": "1",
|
|
"hwid_proration_ratio": "1.0",
|
|
"hwid_full_price": "50.0",
|
|
},
|
|
"description": "Auto-renewal for 1 months",
|
|
}
|
|
|
|
with (
|
|
patch.object(
|
|
yookassa.payment_dal,
|
|
"get_payment_by_provider_payment_id",
|
|
AsyncMock(return_value=None),
|
|
),
|
|
patch.object(
|
|
yookassa.payment_dal,
|
|
"ensure_payment_with_provider_id",
|
|
AsyncMock(return_value=payment),
|
|
) as ensure_payment,
|
|
patch.object(
|
|
yookassa.payment_dal,
|
|
"get_payment_by_db_id",
|
|
AsyncMock(return_value=payment),
|
|
),
|
|
patch.object(
|
|
yookassa.payment_dal,
|
|
"update_payment_status_by_db_id",
|
|
AsyncMock(return_value=updated_payment),
|
|
),
|
|
patch.object(yookassa.user_dal, "get_user_by_id", AsyncMock(return_value=db_user)),
|
|
patch.object(
|
|
yookassa,
|
|
"prepare_config_links",
|
|
AsyncMock(return_value=("link", "https://example.test/sub")),
|
|
),
|
|
patch.object(yookassa, "send_success_message_to_user", AsyncMock()) as send_success,
|
|
patch.object(yookassa, "notify_admins_payment_received", AsyncMock()),
|
|
):
|
|
await yookassa.process_successful_payment(
|
|
AsyncMock(),
|
|
AsyncMock(),
|
|
payment_info,
|
|
_I18n(),
|
|
settings,
|
|
AsyncMock(),
|
|
subscription_service,
|
|
referral_service,
|
|
)
|
|
|
|
ensure_payment.assert_awaited_once()
|
|
ensure_kwargs = ensure_payment.await_args.kwargs
|
|
assert ensure_kwargs["sale_mode"] == "subscription@standard"
|
|
assert ensure_kwargs["tariff_key"] == "standard"
|
|
assert ensure_kwargs["purchased_hwid_devices"] == 2
|
|
assert ensure_kwargs["hwid_valid_from"] == valid_from
|
|
assert ensure_kwargs["hwid_valid_until"] == valid_until
|
|
assert ensure_kwargs["hwid_pricing_period_months"] == 1
|
|
assert ensure_kwargs["hwid_proration_ratio"] == 1.0
|
|
assert ensure_kwargs["hwid_full_price"] == 50.0
|
|
activation_kwargs = subscription_service.activate_subscription.await_args.kwargs
|
|
assert activation_kwargs["sale_mode"] == "subscription@standard"
|
|
send_success.assert_awaited_once()
|