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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user