fix: refresh webapp profile after activation
This commit is contained in:
@@ -430,6 +430,17 @@ async def account_telegram_link_route(request: web.Request) -> web.Response:
|
||||
async def me_route(request: web.Request) -> web.Response:
|
||||
user_id = _require_user_id(request)
|
||||
settings: Settings = request.app["settings"]
|
||||
fresh = str(request.query.get("fresh") or "").strip().lower() in {
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
}
|
||||
if fresh:
|
||||
await _invalidate_webapp_user_caches(settings, user_id)
|
||||
data = await _build_user_payload(request, user_id)
|
||||
return web.json_response({"ok": True, **data})
|
||||
|
||||
data = await webapp_cached_user_payload(
|
||||
settings,
|
||||
"me",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# ruff: noqa: F401,F403,F405,I001
|
||||
from ._runtime import * # noqa: F403,F405
|
||||
|
||||
from bot.app.web.webapp.cache_helpers import invalidate_webapp_user_caches
|
||||
|
||||
|
||||
async def apply_promo_route(request: web.Request) -> web.Response:
|
||||
user_id = _require_user_id(request)
|
||||
@@ -329,6 +331,8 @@ async def activate_trial_route(request: web.Request) -> web.Response:
|
||||
await session.rollback()
|
||||
logger.exception("Failed to mark WebApp trial activation for ad attribution")
|
||||
|
||||
await invalidate_webapp_user_caches(settings, user_id)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"ok": True,
|
||||
@@ -724,6 +728,8 @@ async def payment_status_route(request: web.Request) -> web.Response:
|
||||
if not payment or payment.user_id != user_id:
|
||||
return _json_error(404, "not_found", "Payment not found")
|
||||
payment = await _refresh_yookassa_payment_status(request, session, payment)
|
||||
if payment.status == "succeeded":
|
||||
await invalidate_webapp_user_caches(request.app["settings"], user_id)
|
||||
return web.json_response(
|
||||
{
|
||||
"ok": True,
|
||||
|
||||
@@ -641,7 +641,7 @@
|
||||
}
|
||||
}
|
||||
if (shouldRefreshProfile) {
|
||||
await loadData();
|
||||
await loadData({ fresh: true });
|
||||
const shown = await maybeShowActivationSuccessDialog({
|
||||
source: "watch",
|
||||
paymentId: pending?.paymentId,
|
||||
@@ -685,7 +685,7 @@
|
||||
activationResumeLastCheckAt = now;
|
||||
activationResumeRefreshBusy = true;
|
||||
try {
|
||||
await loadData();
|
||||
await loadData({ fresh: true });
|
||||
const shown = await maybeShowActivationSuccessDialog({ source: "resume" });
|
||||
if (!shown) startPendingActivationWatch();
|
||||
} catch (_error) {
|
||||
@@ -1072,6 +1072,7 @@
|
||||
getCsrfToken: () => csrfToken,
|
||||
});
|
||||
if (mode === "app" && screen !== "admin") {
|
||||
if (hasPendingActivationHandoff()) await loadData({ fresh: true });
|
||||
const shown = await maybeShowActivationSuccessDialog({ source: "boot" });
|
||||
if (!shown) startPendingActivationWatch();
|
||||
}
|
||||
@@ -1115,8 +1116,8 @@
|
||||
syncPasswordLoginPath(nextEnabled, replace);
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const payload = await api("/me");
|
||||
async function loadData(options = {}) {
|
||||
const payload = await api(options?.fresh ? "/me?fresh=1" : "/me");
|
||||
if (!payload.ok) throw new Error(payload.error || "load_failed");
|
||||
data = payload;
|
||||
billingStore.update((s) => ({
|
||||
@@ -1458,7 +1459,7 @@
|
||||
? t("wa_promo_activated_until", { date: response.end_date_text })
|
||||
: t("wa_promo_activated");
|
||||
promoIsError = false;
|
||||
await loadData();
|
||||
await loadData({ fresh: true });
|
||||
} catch (error) {
|
||||
promoStatus = error?.message || t("wa_promo_activation_failed");
|
||||
promoIsError = true;
|
||||
@@ -1481,7 +1482,7 @@
|
||||
if (!response.ok) throw response;
|
||||
trialActivationResult = response;
|
||||
showToast(t("wa_trial_activated"));
|
||||
await loadData();
|
||||
await loadData({ fresh: true });
|
||||
await maybeShowActivationSuccessDialog({ source: "trial", force: true });
|
||||
} catch (error) {
|
||||
const message = error?.message || t("wa_trial_activation_failed");
|
||||
|
||||
@@ -751,7 +751,7 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
return { ok: true, ticket, messages: clone(supportMessages[ticket.ticket_id] || []) };
|
||||
}
|
||||
if (cleanPath === "/support/unread") return { ok: true, unread: 1 };
|
||||
if (path === "/me") return clone(DEV_MOCK.data);
|
||||
if (cleanPath === "/me") return clone(DEV_MOCK.data);
|
||||
if (path === "/subscription-guides") return clone(DEV_MOCK.data.subscription_guides);
|
||||
if (cleanPath.startsWith("/subscription-guides/public/")) {
|
||||
const shareToken = decodeURIComponent(cleanPath.split("/").pop() || "");
|
||||
|
||||
@@ -64,7 +64,7 @@ export function createBillingStore({
|
||||
paymentPollToken += 1;
|
||||
}
|
||||
showToast(t("wa_payment_success", {}, "Payment successful"));
|
||||
await loadData();
|
||||
await loadData({ fresh: true });
|
||||
if (
|
||||
successContext.initialSubscriptionPayment &&
|
||||
typeof onSubscriptionActivated === "function"
|
||||
|
||||
@@ -2,11 +2,23 @@ from types import SimpleNamespace
|
||||
from unittest import IsolatedAsyncioTestCase
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import bot.app.web.subscription_webapp # noqa: F401
|
||||
from bot.app.web.webapp import billing as billing_module
|
||||
from bot.payment_providers.base import WebAppPaymentContext
|
||||
from bot.payment_providers.yookassa import create_webapp_payment
|
||||
|
||||
|
||||
class _SessionFactory:
|
||||
def __call__(self):
|
||||
return self
|
||||
|
||||
async def __aenter__(self):
|
||||
return SimpleNamespace()
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
|
||||
class WebAppPaymentStatusTests(IsolatedAsyncioTestCase):
|
||||
async def test_yookassa_pending_payment_refresh_processes_succeeded_provider_status(self):
|
||||
payment = SimpleNamespace(
|
||||
@@ -123,3 +135,34 @@ class WebAppPaymentStatusTests(IsolatedAsyncioTestCase):
|
||||
yookassa_service.create_payment.await_args.kwargs["save_payment_method"],
|
||||
False,
|
||||
)
|
||||
|
||||
async def test_payment_status_invalidates_profile_cache_for_succeeded_payment(self):
|
||||
settings = SimpleNamespace()
|
||||
payment = SimpleNamespace(payment_id=42, user_id=1001, status="succeeded")
|
||||
request = SimpleNamespace(
|
||||
app={"settings": settings, "async_session_factory": _SessionFactory()},
|
||||
match_info={"payment_id": "42"},
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(billing_module, "_require_user_id", return_value=1001),
|
||||
patch.object(
|
||||
billing_module.payment_dal,
|
||||
"get_payment_by_db_id",
|
||||
AsyncMock(return_value=payment),
|
||||
),
|
||||
patch.object(
|
||||
billing_module,
|
||||
"_refresh_yookassa_payment_status",
|
||||
AsyncMock(return_value=payment),
|
||||
),
|
||||
patch.object(
|
||||
billing_module,
|
||||
"invalidate_webapp_user_caches",
|
||||
AsyncMock(),
|
||||
) as invalidate_cache,
|
||||
):
|
||||
response = await billing_module.payment_status_route(request)
|
||||
|
||||
invalidate_cache.assert_awaited_once_with(settings, 1001)
|
||||
self.assertEqual(response.status, 200)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest import IsolatedAsyncioTestCase
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import bot.app.web.subscription_webapp # noqa: F401
|
||||
from bot.app.web.webapp import account as account_module
|
||||
|
||||
|
||||
class WebAppProfileCacheTests(IsolatedAsyncioTestCase):
|
||||
async def test_me_route_fresh_bypasses_cache_and_invalidates_payload(self):
|
||||
settings = SimpleNamespace(WEBAPP_ME_CACHE_TTL_SECONDS=60)
|
||||
request = SimpleNamespace(app={"settings": settings}, query={"fresh": "1"})
|
||||
|
||||
with (
|
||||
patch.object(account_module, "_require_user_id", return_value=42),
|
||||
patch.object(
|
||||
account_module,
|
||||
"_invalidate_webapp_user_caches",
|
||||
AsyncMock(),
|
||||
) as invalidate_cache,
|
||||
patch.object(
|
||||
account_module,
|
||||
"_build_user_payload",
|
||||
AsyncMock(return_value={"user": {"id": 42}, "subscription": {"active": True}}),
|
||||
) as build_payload,
|
||||
patch.object(
|
||||
account_module,
|
||||
"webapp_cached_user_payload",
|
||||
AsyncMock(),
|
||||
) as cached_payload,
|
||||
):
|
||||
response = await account_module.me_route(request)
|
||||
|
||||
invalidate_cache.assert_awaited_once_with(settings, 42)
|
||||
build_payload.assert_awaited_once_with(request, 42)
|
||||
cached_payload.assert_not_awaited()
|
||||
self.assertEqual(
|
||||
json.loads(response.text),
|
||||
{"ok": True, "user": {"id": 42}, "subscription": {"active": True}},
|
||||
)
|
||||
Reference in New Issue
Block a user