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:
|
async def me_route(request: web.Request) -> web.Response:
|
||||||
user_id = _require_user_id(request)
|
user_id = _require_user_id(request)
|
||||||
settings: Settings = request.app["settings"]
|
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(
|
data = await webapp_cached_user_payload(
|
||||||
settings,
|
settings,
|
||||||
"me",
|
"me",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# ruff: noqa: F401,F403,F405,I001
|
# ruff: noqa: F401,F403,F405,I001
|
||||||
from ._runtime import * # noqa: F403,F405
|
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:
|
async def apply_promo_route(request: web.Request) -> web.Response:
|
||||||
user_id = _require_user_id(request)
|
user_id = _require_user_id(request)
|
||||||
@@ -329,6 +331,8 @@ async def activate_trial_route(request: web.Request) -> web.Response:
|
|||||||
await session.rollback()
|
await session.rollback()
|
||||||
logger.exception("Failed to mark WebApp trial activation for ad attribution")
|
logger.exception("Failed to mark WebApp trial activation for ad attribution")
|
||||||
|
|
||||||
|
await invalidate_webapp_user_caches(settings, user_id)
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
"ok": True,
|
"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:
|
if not payment or payment.user_id != user_id:
|
||||||
return _json_error(404, "not_found", "Payment not found")
|
return _json_error(404, "not_found", "Payment not found")
|
||||||
payment = await _refresh_yookassa_payment_status(request, session, payment)
|
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(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
"ok": True,
|
"ok": True,
|
||||||
|
|||||||
@@ -641,7 +641,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (shouldRefreshProfile) {
|
if (shouldRefreshProfile) {
|
||||||
await loadData();
|
await loadData({ fresh: true });
|
||||||
const shown = await maybeShowActivationSuccessDialog({
|
const shown = await maybeShowActivationSuccessDialog({
|
||||||
source: "watch",
|
source: "watch",
|
||||||
paymentId: pending?.paymentId,
|
paymentId: pending?.paymentId,
|
||||||
@@ -685,7 +685,7 @@
|
|||||||
activationResumeLastCheckAt = now;
|
activationResumeLastCheckAt = now;
|
||||||
activationResumeRefreshBusy = true;
|
activationResumeRefreshBusy = true;
|
||||||
try {
|
try {
|
||||||
await loadData();
|
await loadData({ fresh: true });
|
||||||
const shown = await maybeShowActivationSuccessDialog({ source: "resume" });
|
const shown = await maybeShowActivationSuccessDialog({ source: "resume" });
|
||||||
if (!shown) startPendingActivationWatch();
|
if (!shown) startPendingActivationWatch();
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
@@ -1072,6 +1072,7 @@
|
|||||||
getCsrfToken: () => csrfToken,
|
getCsrfToken: () => csrfToken,
|
||||||
});
|
});
|
||||||
if (mode === "app" && screen !== "admin") {
|
if (mode === "app" && screen !== "admin") {
|
||||||
|
if (hasPendingActivationHandoff()) await loadData({ fresh: true });
|
||||||
const shown = await maybeShowActivationSuccessDialog({ source: "boot" });
|
const shown = await maybeShowActivationSuccessDialog({ source: "boot" });
|
||||||
if (!shown) startPendingActivationWatch();
|
if (!shown) startPendingActivationWatch();
|
||||||
}
|
}
|
||||||
@@ -1115,8 +1116,8 @@
|
|||||||
syncPasswordLoginPath(nextEnabled, replace);
|
syncPasswordLoginPath(nextEnabled, replace);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData(options = {}) {
|
||||||
const payload = await api("/me");
|
const payload = await api(options?.fresh ? "/me?fresh=1" : "/me");
|
||||||
if (!payload.ok) throw new Error(payload.error || "load_failed");
|
if (!payload.ok) throw new Error(payload.error || "load_failed");
|
||||||
data = payload;
|
data = payload;
|
||||||
billingStore.update((s) => ({
|
billingStore.update((s) => ({
|
||||||
@@ -1458,7 +1459,7 @@
|
|||||||
? t("wa_promo_activated_until", { date: response.end_date_text })
|
? t("wa_promo_activated_until", { date: response.end_date_text })
|
||||||
: t("wa_promo_activated");
|
: t("wa_promo_activated");
|
||||||
promoIsError = false;
|
promoIsError = false;
|
||||||
await loadData();
|
await loadData({ fresh: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
promoStatus = error?.message || t("wa_promo_activation_failed");
|
promoStatus = error?.message || t("wa_promo_activation_failed");
|
||||||
promoIsError = true;
|
promoIsError = true;
|
||||||
@@ -1481,7 +1482,7 @@
|
|||||||
if (!response.ok) throw response;
|
if (!response.ok) throw response;
|
||||||
trialActivationResult = response;
|
trialActivationResult = response;
|
||||||
showToast(t("wa_trial_activated"));
|
showToast(t("wa_trial_activated"));
|
||||||
await loadData();
|
await loadData({ fresh: true });
|
||||||
await maybeShowActivationSuccessDialog({ source: "trial", force: true });
|
await maybeShowActivationSuccessDialog({ source: "trial", force: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error?.message || t("wa_trial_activation_failed");
|
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] || []) };
|
return { ok: true, ticket, messages: clone(supportMessages[ticket.ticket_id] || []) };
|
||||||
}
|
}
|
||||||
if (cleanPath === "/support/unread") return { ok: true, unread: 1 };
|
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 (path === "/subscription-guides") return clone(DEV_MOCK.data.subscription_guides);
|
||||||
if (cleanPath.startsWith("/subscription-guides/public/")) {
|
if (cleanPath.startsWith("/subscription-guides/public/")) {
|
||||||
const shareToken = decodeURIComponent(cleanPath.split("/").pop() || "");
|
const shareToken = decodeURIComponent(cleanPath.split("/").pop() || "");
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function createBillingStore({
|
|||||||
paymentPollToken += 1;
|
paymentPollToken += 1;
|
||||||
}
|
}
|
||||||
showToast(t("wa_payment_success", {}, "Payment successful"));
|
showToast(t("wa_payment_success", {}, "Payment successful"));
|
||||||
await loadData();
|
await loadData({ fresh: true });
|
||||||
if (
|
if (
|
||||||
successContext.initialSubscriptionPayment &&
|
successContext.initialSubscriptionPayment &&
|
||||||
typeof onSubscriptionActivated === "function"
|
typeof onSubscriptionActivated === "function"
|
||||||
|
|||||||
@@ -2,11 +2,23 @@ from types import SimpleNamespace
|
|||||||
from unittest import IsolatedAsyncioTestCase
|
from unittest import IsolatedAsyncioTestCase
|
||||||
from unittest.mock import AsyncMock, patch
|
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.app.web.webapp import billing as billing_module
|
||||||
from bot.payment_providers.base import WebAppPaymentContext
|
from bot.payment_providers.base import WebAppPaymentContext
|
||||||
from bot.payment_providers.yookassa import create_webapp_payment
|
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):
|
class WebAppPaymentStatusTests(IsolatedAsyncioTestCase):
|
||||||
async def test_yookassa_pending_payment_refresh_processes_succeeded_provider_status(self):
|
async def test_yookassa_pending_payment_refresh_processes_succeeded_provider_status(self):
|
||||||
payment = SimpleNamespace(
|
payment = SimpleNamespace(
|
||||||
@@ -123,3 +135,34 @@ class WebAppPaymentStatusTests(IsolatedAsyncioTestCase):
|
|||||||
yookassa_service.create_payment.await_args.kwargs["save_payment_method"],
|
yookassa_service.create_payment.await_args.kwargs["save_payment_method"],
|
||||||
False,
|
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