fix: separate HWID device renewal flows

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.
This commit is contained in:
3252a8
2026-06-03 23:51:46 +03:00
parent a06884d816
commit fbb89793cb
48 changed files with 2410 additions and 224 deletions
+183
View File
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from types import SimpleNamespace
from unittest import IsolatedAsyncioTestCase
from unittest.mock import AsyncMock, patch
@@ -15,6 +16,81 @@ class _I18n:
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")
@@ -99,3 +175,110 @@ class YooKassaHwidWebhookTests(IsolatedAsyncioTestCase):
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()