From cd469ae2bb4e3bd34cd2031f97bc29dcd30d388b Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Thu, 28 May 2026 15:54:35 +0300 Subject: [PATCH] feat: add demo device top-up flow --- docs-site/public/demo/demo-shell.js | 5 +- docs-site/src/pages/demo.astro | 2 +- docs/getting-started/demo.md | 2 +- frontend/src/lib/webapp/mockApi.js | 112 +++++++++++++++++++++++++ frontend/src/lib/webapp/previewMock.js | 13 ++- 5 files changed, 129 insertions(+), 5 deletions(-) diff --git a/docs-site/public/demo/demo-shell.js b/docs-site/public/demo/demo-shell.js index f370876..a3bab4e 100644 --- a/docs-site/public/demo/demo-shell.js +++ b/docs-site/public/demo/demo-shell.js @@ -72,8 +72,11 @@ const routeFromParams = () => { return "/home"; }; -const initialMock = normalizeRouteMock(params.get("mock")); let initialRoute = routeFromParams(); +const mockForRoute = (route) => (normalizePath(route) === "/devices" ? "devices" : ""); +const initialMock = params.has("mock") + ? normalizeRouteMock(params.get("mock")) + : normalizeRouteMock(mockForRoute(initialRoute) || defaultMock); if (initialMock === "trial" && initialRoute === "/trial") { initialRoute = "/home"; const normalizedUrl = new URL(window.location.href); diff --git a/docs-site/src/pages/demo.astro b/docs-site/src/pages/demo.astro index 3a9e8f1..c89705b 100644 --- a/docs-site/src/pages/demo.astro +++ b/docs-site/src/pages/demo.astro @@ -293,7 +293,7 @@ const docsHref = '/getting-started/demo/'; - +
diff --git a/docs/getting-started/demo.md b/docs/getting-started/demo.md index 28c2a32..906b033 100644 --- a/docs/getting-started/demo.md +++ b/docs/getting-started/demo.md @@ -11,7 +11,7 @@ - [Админка: пользователи](/demo/admin/users) - [Админка: бэкапы](/demo/admin/backups) - [Пробный период](/demo/home?mock=trial) -- [Устройства](/demo/devices?mock=devices) +- [Докупка устройств](/demo/devices?mock=devices) ## Как собирается diff --git a/frontend/src/lib/webapp/mockApi.js b/frontend/src/lib/webapp/mockApi.js index a6b6945..1954eaa 100644 --- a/frontend/src/lib/webapp/mockApi.js +++ b/frontend/src/lib/webapp/mockApi.js @@ -17,7 +17,10 @@ let demoAdsState = null; let demoSupportTicketsState = null; let demoSupportMessagesState = null; let demoTariffsState = null; +let demoPaymentSequence = 20000; const demoSettingsChanges = new Map(); +const demoPaymentStatuses = new Map(); +const deviceTopupSaleModes = new Set(["hwid_device", "hwid_devices", "hwid_devices_renewal"]); function demoPromos() { if (!demoPromosState) demoPromosState = defaultClone(DEMO_DATASET.promos || []); @@ -68,6 +71,79 @@ function jsonBody(options) { } } +function isDeviceTopupSaleMode(value) { + return deviceTopupSaleModes.has(String(value || "").toLowerCase()); +} + +function demoDeviceTopupPlan(body) { + const deviceCount = Number(body.device_count || body.months || 0); + const plans = DEV_MOCK.data.device_topup_options?.plans || []; + return ( + plans.find( + (plan) => + String(plan.tariff_key || "") === String(body.tariff_key || plan.tariff_key || "") && + Number(plan.device_count || plan.purchased_hwid_devices || plan.months || 0) === deviceCount + ) || + plans.find( + (plan) => + Number(plan.device_count || plan.purchased_hwid_devices || plan.months || 0) === deviceCount + ) || + null + ); +} + +function applyDemoDeviceTopup(deviceCount) { + const count = Math.max(1, Number(deviceCount || 0)); + const subscription = DEV_MOCK.data.subscription || {}; + const devicesPayload = DEV_MOCK.data.devices || {}; + const topupOptions = DEV_MOCK.data.device_topup_options || {}; + const devices = Array.isArray(devicesPayload.devices) ? devicesPayload.devices : []; + const currentMax = Number( + subscription.max_devices || + devicesPayload.max_devices || + topupOptions.max_devices || + topupOptions.current_limit || + 0 + ); + const nextMax = currentMax > 0 ? currentMax + count : currentMax; + const currentExtra = Number( + subscription.extra_hwid_devices || topupOptions.extra_hwid_devices || 0 + ); + const nextExtra = currentExtra + count; + const validUntil = + subscription.extra_hwid_devices_valid_until_text || + topupOptions.extra_hwid_devices_valid_until_text || + subscription.end_date_text || + "28.06.2026 12:00"; + + DEV_MOCK.data.subscription = { + ...subscription, + active: true, + can_topup_devices: true, + max_devices: nextMax, + extra_hwid_devices: nextExtra, + extra_hwid_devices_valid_until_text: validUntil, + }; + DEV_MOCK.data.devices = { + ...devicesPayload, + ok: true, + enabled: true, + current_devices: devices.length || Number(devicesPayload.current_devices || 0), + max_devices: nextMax, + max_devices_label: nextMax > 0 ? String(nextMax) : devicesPayload.max_devices_label || "∞", + }; + DEV_MOCK.data.device_topup_options = { + ...topupOptions, + ok: true, + enabled: true, + current_devices: devices.length || Number(topupOptions.current_devices || 0), + max_devices: nextMax, + current_limit: nextMax, + extra_hwid_devices: nextExtra, + extra_hwid_devices_valid_until_text: validUntil, + }; +} + function writeDemoLanguage(language) { if (typeof window === "undefined") return; try { @@ -1681,6 +1757,26 @@ export async function mockApi(path, options = {}, context = {}) { return { ok: true, csrf_token: "local-preview-csrf" }; } if (path === "/payments" && String(options.method || "").toUpperCase() === "POST") { + const body = jsonBody(options); + if (isDeviceTopupSaleMode(body.sale_mode)) { + const plan = demoDeviceTopupPlan(body); + const deviceCount = Number( + body.device_count || plan?.device_count || plan?.purchased_hwid_devices || body.months || 1 + ); + const paymentId = ++demoPaymentSequence; + demoPaymentStatuses.set(String(paymentId), { + status: "pending_yookassa", + paid: false, + sale_mode: body.sale_mode || "hwid_devices", + device_count: deviceCount, + applied: false, + }); + return { + ok: true, + action: "invoice_sent", + payment_id: paymentId, + }; + } return { ok: true, action: "open_link", @@ -1689,6 +1785,22 @@ export async function mockApi(path, options = {}, context = {}) { }; } if (/^\/payments\/\d+$/.test(path) && String(options.method || "GET").toUpperCase() === "GET") { + const paymentId = String(path.split("/").pop()); + const status = demoPaymentStatuses.get(paymentId); + if (status) { + if (!status.applied && isDeviceTopupSaleMode(status.sale_mode)) { + applyDemoDeviceTopup(status.device_count); + status.applied = true; + } + status.status = "succeeded"; + status.paid = true; + return { + ok: true, + payment_id: Number(paymentId), + status: status.status, + paid: status.paid, + }; + } return { ok: true, payment_id: Number(path.split("/").pop()), diff --git a/frontend/src/lib/webapp/previewMock.js b/frontend/src/lib/webapp/previewMock.js index d59eabf..b11d3b9 100644 --- a/frontend/src/lib/webapp/previewMock.js +++ b/frontend/src/lib/webapp/previewMock.js @@ -778,31 +778,40 @@ export function applyPreviewMock(kind) { }; DEV_MOCK.data.device_topup_options = { ok: true, + enabled: true, + tariff_key: "standard", + tariff_name: "Стандарт", + current_limit: 5, current_devices: 5, max_devices: 5, available_extra_devices: 3, extra_hwid_devices: 0, + extra_hwid_devices_valid_until_text: "", + renewal_available: false, + renewal_recommended_count: 0, plans: [ { id: "standard:hwid:1", tariff_key: "standard", tariff_name: "Стандарт", - sale_mode: "hwid_device", + sale_mode: "hwid_devices", purchased_hwid_devices: 1, price: 120, currency: "RUB", title: "+1 устройство", + subtitle: "Стандарт", device_count: 1, }, { id: "standard:hwid:3", tariff_key: "standard", tariff_name: "Стандарт", - sale_mode: "hwid_device", + sale_mode: "hwid_devices", purchased_hwid_devices: 3, price: 290, currency: "RUB", title: "+3 устройства", + subtitle: "Стандарт", device_count: 3, }, ],