From b759ddb0f5e9f13eaa7cdb1254454ba9d24a4059 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Mon, 11 May 2026 00:15:29 +0300 Subject: [PATCH] feat: tune premium squads visual in web app --- bot/app/web/frontend/src/App.svelte | 108 +++++++++++------- .../web/frontend/src/admin/AdminPanel.svelte | 21 ++++ bot/app/web/frontend/src/styles.css | 57 ++++++++- bot/app/web/subscription_webapp.py | 43 +++++-- bot/services/panel_api_service.py | 32 ++++++ bot/services/subscription_service.py | 101 +++++++++++++++- config/tariffs_config.py | 5 + tests/test_tariffs_config.py | 3 + 8 files changed, 311 insertions(+), 59 deletions(-) diff --git a/bot/app/web/frontend/src/App.svelte b/bot/app/web/frontend/src/App.svelte index ac7bc36..385bdfb 100644 --- a/bot/app/web/frontend/src/App.svelte +++ b/bot/app/web/frontend/src/App.svelte @@ -134,7 +134,10 @@ premium_baseline_bytes: 53687091200, premium_topup_balance_bytes: 0, premium_is_limited: false, + premium_title: "Premium-серверы", premium_node_labels: ["Premium NL-1", "Premium DE-1"], + can_topup_regular_traffic: true, + can_topup_premium_traffic: true, max_devices: 5, }, devices: { @@ -249,6 +252,7 @@ let paymentStep = "tariff"; let selectedTariffKey = ""; let topupModalOpen = query.get("topup") === "1"; + let topupKind = "regular"; let deviceTopupModalOpen = query.get("device_topup") === "1"; let changeModalOpen = query.get("change") === "1"; let topupOptions = null; @@ -506,11 +510,17 @@ $: hasActiveTariffSubscription = Boolean(tariffMode && subscription?.active && subscription?.tariff_key); $: canChangeTariff = Boolean(hasActiveTariffSubscription && hasMultipleTariffs); $: currentTariffName = activeTariffName(subscription, plans); - $: canOpenTopupModal = Boolean( + $: canOpenRegularTopupModal = Boolean( hasActiveTariffSubscription && - subscription?.can_topup_traffic && - (Number(subscription?.traffic_limit_bytes || 0) > 0 || Number(subscription?.premium_limit_bytes || 0) > 0), + (subscription?.can_topup_regular_traffic ?? subscription?.can_topup_traffic) && + Number(subscription?.traffic_limit_bytes || 0) > 0, ); + $: canOpenPremiumTopupModal = Boolean( + hasActiveTariffSubscription && + (subscription?.can_topup_premium_traffic ?? subscription?.can_topup_traffic) && + Number(subscription?.premium_limit_bytes || 0) > 0, + ); + $: canOpenTopupModal = Boolean(canOpenRegularTopupModal || canOpenPremiumTopupModal); $: canShowTopupButton = Boolean( canOpenTopupModal && (trafficPercent(subscription) >= 85 || premiumTrafficPercent(subscription) >= 85), @@ -1171,7 +1181,13 @@ if (path === "/promo/apply") return { ok: true, end_date_text: "31.05.2026" }; if (path === "/devices") return structuredCloneSafe(DEV_MOCK.data.devices); if (path === "/devices/topup-options") return structuredCloneSafe(DEV_MOCK.data.device_topup_options || { ok: true, plans: [] }); - if (path === "/tariffs/topup-options") return structuredCloneSafe(DEV_MOCK.data.topup_options || { ok: true, plans: [] }); + if (cleanPath === "/tariffs/topup-options") { + const kind = new URLSearchParams(String(path || "").split("?")[1] || "").get("kind") || "regular"; + const payload = structuredCloneSafe(DEV_MOCK.data.topup_options || { ok: true, plans: [] }); + payload.topup_kind = kind; + payload.plans = (payload.plans || []).filter((plan) => kind === "premium" ? plan.sale_mode === "premium_topup" : plan.sale_mode !== "premium_topup"); + return payload; + } if (path === "/tariffs/change-options") return structuredCloneSafe(DEV_MOCK.data.tariff_change_options || { ok: true, targets: [] }); if (path === "/devices/disconnect" && String(options.method || "").toUpperCase() === "POST") { let payload = {}; @@ -1800,11 +1816,13 @@ } } - async function loadTopupOptions() { - if (topupOptions || tariffActionBusy) return; + async function loadTopupOptions(kind = topupKind) { + if (topupOptions?.topup_kind === kind || tariffActionBusy) return; tariffActionBusy = true; try { - const response = await api("/tariffs/topup-options"); + topupOptions = null; + selectedTopupPlan = null; + const response = await api(`/tariffs/topup-options?kind=${encodeURIComponent(kind)}`); if (!response?.ok) throw response; topupOptions = response; selectedTopupPlan = response.plans?.[0] || null; @@ -2252,10 +2270,11 @@ paymentModalOpen = false; } - function openTopupModal() { - if (!canOpenTopupModal) return; + function openTopupModal(kind = "regular") { + if (kind === "premium" ? !canOpenPremiumTopupModal : !canOpenRegularTopupModal) return; + topupKind = kind; topupModalOpen = true; - loadTopupOptions(); + loadTopupOptions(kind); } function closeTopupModal() { @@ -2460,6 +2479,10 @@ return t("wa_traffic_of", { used: sub?.premium_used || "0 GB", limit: sub?.premium_limit || "0 GB" }); } + function premiumTitle(sub = subscription) { + return String(sub?.premium_title || "").trim() || t("wa_premium_traffic_title", {}, "Premium-серверы"); + } + function premiumTrafficLeftLabel(sub) { const left = Math.max(0, Number(sub?.premium_limit_bytes || 0) - Number(sub?.premium_used_bytes || 0)); return formatTrafficBytes(left); @@ -2594,10 +2617,16 @@ function topupModalDescription() { if (!topupOptions) return ""; + if (topupKind === "premium") return topupOptions?.tariff_name ? t("wa_topup_for_tariff", { tariff: topupOptions.tariff_name }) : ""; if (singleTariffMode) return ""; return topupOptions?.tariff_name ? t("wa_topup_for_tariff", { tariff: topupOptions.tariff_name }) : ""; } + function topupModalTitle() { + if (topupKind === "premium") return premiumTitle(subscription); + return t("wa_topup_traffic"); + } + function topupCarryoverNotes() { const plans = topupOptions?.plans || []; if (!plans.length) return []; @@ -3009,9 +3038,9 @@ {#if subscription.active} - - {#if canOpenTopupModal} - + + {#if canOpenRegularTopupModal} + {/if}
{t("wa_home_traffic_used")} @@ -3026,41 +3055,38 @@
{#if Number(subscription?.premium_limit_bytes || 0) > 0} - - {#if canOpenTopupModal} - + + {#if canOpenPremiumTopupModal} + {/if}
- {t("wa_premium_traffic_title", {}, "Premium-серверы")} + {premiumTitle(subscription)} {premiumTrafficLabel(subscription)}
-
- {subscription?.premium_is_limited ? t("wa_premium_access_limited", {}, "Доступ к premium временно ограничен") : t("wa_premium_reset_monthly", {}, "Отдельный лимит на месяц")} +
+ {#if premiumServerLabels(subscription).length} +
+ + {subscription?.premium_is_limited ? t("wa_premium_access_limited", {}, "Доступ к premium временно ограничен") : t("wa_premium_reset_monthly", {}, "Отдельный лимит на месяц")} + + +
+ {t("wa_premium_servers_limited", {}, "Отдельный лимит действует на")} +
+ {#each premiumServerLabels(subscription).slice(0, 8) as label} + {label} + {/each} +
+
+
+ {:else} + {subscription?.premium_is_limited ? t("wa_premium_access_limited", {}, "Доступ к premium временно ограничен") : t("wa_premium_reset_monthly", {}, "Отдельный лимит на месяц")} + {/if} {premiumTrafficPercent(subscription)}%
-
- - {t("wa_premium_left", {}, "Осталось")} - {premiumTrafficLeftLabel(subscription)} - - - {t("wa_premium_topup_balance", {}, "Докупленный остаток")} - {premiumTopupBalanceLabel(subscription)} - -
- {#if premiumServerLabels(subscription).length} -
- {t("wa_premium_servers_limited", {}, "Отдельный лимит действует на")} -
- {#each premiumServerLabels(subscription).slice(0, 8) as label} - {label} - {/each} -
-
- {/if} {/if} {:else if appSettings?.trial_enabled && appSettings?.trial_available} @@ -3103,7 +3129,7 @@ {/if} {#if canShowTopupButton} - @@ -3755,7 +3781,7 @@ Premium-сквады дают пользователю доступ к более быстрым/премиальным нодам; их трафик считается отдельно от основного, чтобы можно было ограничить или продавать дополнительно
+
+ + Название premium-раздела, RU + Эта строка заменит «Premium-серверы» в кабинете, докупках и карточках лимитов. + + + + Название premium-раздела, EN + Опционально для английского интерфейса. + + +
Premium Internal Squads diff --git a/bot/app/web/frontend/src/styles.css b/bot/app/web/frontend/src/styles.css index 3f87c5e..99fbb92 100644 --- a/bot/app/web/frontend/src/styles.css +++ b/bot/app/web/frontend/src/styles.css @@ -422,8 +422,7 @@ a { } .premium-traffic-card { - display: grid; - gap: 10px; + display: block; } .premium-traffic-card-limited { @@ -434,6 +433,52 @@ a { background: linear-gradient(90deg, #38bdf8, var(--accent)); } +.premium-traffic-meta { + align-items: flex-start; +} + +.premium-server-dropdown { + position: relative; + z-index: 3; + min-width: 0; +} + +.premium-server-dropdown summary { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + min-height: 32px; + margin: -6px -8px; + padding: 6px 8px; + cursor: pointer; + list-style: none; + -webkit-tap-highlight-color: transparent; +} + +.premium-server-dropdown summary:focus { + outline: none; +} + +.premium-server-dropdown summary::-webkit-details-marker { + display: none; +} + +.premium-server-dropdown summary span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.premium-server-dropdown svg { + flex: 0 0 auto; + opacity: 0.72; +} + +.premium-server-list-dropdown { + margin-top: 8px; +} + .premium-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -473,13 +518,13 @@ a { .premium-server-list span { max-width: 100%; padding: 4px 8px; - overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 999px; color: var(--text); font-size: 11px; - text-overflow: ellipsis; - white-space: nowrap; + white-space: normal; + overflow-wrap: anywhere; + word-break: break-word; } .topup-summary-card { @@ -2656,7 +2701,7 @@ a { ============================================================ */ .admin-screen-wrap { - --admin-sidebar-w: 248px; + --admin-sidebar-w: var(--desktop-rail-width); --admin-header-h: 60px; position: fixed; diff --git a/bot/app/web/subscription_webapp.py b/bot/app/web/subscription_webapp.py index 7060f63..5787571 100644 --- a/bot/app/web/subscription_webapp.py +++ b/bot/app/web/subscription_webapp.py @@ -2084,6 +2084,9 @@ async def tariff_topup_options_route(request: web.Request) -> web.Response: user_id = _require_user_id(request) settings: Settings = request.app["settings"] config = settings.tariffs_config + topup_kind = str(request.query.get("kind") or "all").strip().lower() + if topup_kind not in {"all", "regular", "premium"}: + return _json_error(400, "invalid_topup_kind", "Invalid topup kind") if not config: return _json_error(404, "tariffs_unavailable", "Tariffs are not configured") @@ -2097,15 +2100,19 @@ async def tariff_topup_options_route(request: web.Request) -> web.Response: return _json_error(400, "subscription_required", "Active tariff subscription is required") lang = db_user.language_code or settings.DEFAULT_LANGUAGE tariff = config.require(sub.tariff_key) - plans = _serialize_topup_packages(settings, tariff, config.topup_packages_for(tariff), lang) + plans = ( + _serialize_topup_packages(settings, tariff, config.topup_packages_for(tariff), lang) + if topup_kind in {"all", "regular"} + else [] + ) premium_plans = _serialize_topup_packages( settings, tariff, tariff.premium_topup_packages, lang, sale_mode="premium_topup", - title_prefix="Premium ", - ) if tariff.premium_squad_uuids else [] + title_prefix=f"{tariff.premium_name(lang)} ", + ) if topup_kind in {"all", "premium"} and tariff.premium_squad_uuids else [] premium_limit_bytes = ( int(sub.premium_baseline_bytes or 0) + int(sub.premium_topup_balance_bytes or 0) @@ -2117,6 +2124,8 @@ async def tariff_topup_options_route(request: web.Request) -> web.Response: "ok": True, "tariff_key": tariff.key, "tariff_name": tariff.name(lang), + "topup_kind": topup_kind, + "premium_title": tariff.premium_name(lang), "traffic_percent": _traffic_percent(sub.traffic_used_bytes, sub.traffic_limit_bytes), "premium_traffic_percent": _traffic_percent( sub.premium_used_bytes, @@ -3222,16 +3231,23 @@ def _serialize_subscription( int((end_date - datetime.now(timezone.utc)).total_seconds()), ) + can_topup_regular_traffic = False + can_topup_premium_traffic = False can_topup_traffic = False if settings.tariffs_config and active.get("tariff_key"): try: tariff = settings.tariffs_config.require(str(active.get("tariff_key"))) packages = settings.tariffs_config.topup_packages_for(tariff) - can_topup_traffic = bool( - (packages and packages.has_any()) - or (tariff.premium_topup_packages and tariff.premium_topup_packages.has_any()) + can_topup_regular_traffic = bool(packages and packages.has_any()) + can_topup_premium_traffic = bool( + tariff.premium_squad_uuids + and tariff.premium_topup_packages + and tariff.premium_topup_packages.has_any() ) + can_topup_traffic = bool(can_topup_regular_traffic or can_topup_premium_traffic) except Exception: + can_topup_regular_traffic = False + can_topup_premium_traffic = False can_topup_traffic = False return { @@ -3243,18 +3259,19 @@ def _serialize_subscription( "remaining_text": _format_remaining(seconds_left, lang), "config_link": active.get("config_link"), "connect_url": active.get("connect_button_url") or active.get("config_link"), - "traffic_limit": _format_bytes(active.get("traffic_limit_bytes")), + "traffic_limit": _format_bytes(active.get("traffic_limit_bytes"), zero_as_unlimited=True), "traffic_used": _format_bytes(active.get("traffic_used_bytes")), "traffic_limit_bytes": _coerce_int_or_none(active.get("traffic_limit_bytes")), "traffic_used_bytes": _coerce_int_or_none(active.get("traffic_used_bytes")), "tariff_key": active.get("tariff_key"), "tariff_name": active.get("tariff_name"), "tariff_description": active.get("tariff_description"), + "premium_title": active.get("premium_title"), "billing_model": active.get("billing_model"), "traffic_limit_strategy": str(active.get("traffic_limit_strategy") or ""), "tier_baseline_bytes": _coerce_int_or_none(active.get("tier_baseline_bytes")), "topup_balance_bytes": _coerce_int_or_none(active.get("topup_balance_bytes")), - "premium_limit": _format_bytes(active.get("premium_limit_bytes")), + "premium_limit": _format_bytes(active.get("premium_limit_bytes"), zero_as_unlimited=True), "premium_used": _format_bytes(active.get("premium_used_bytes")), "premium_limit_bytes": _coerce_int_or_none(active.get("premium_limit_bytes")), "premium_used_bytes": _coerce_int_or_none(active.get("premium_used_bytes")), @@ -3265,6 +3282,8 @@ def _serialize_subscription( "premium_squad_labels": list(active.get("premium_squad_labels") or []), "premium_node_labels": list(active.get("premium_node_labels") or []), "can_topup_traffic": can_topup_traffic, + "can_topup_regular_traffic": can_topup_regular_traffic, + "can_topup_premium_traffic": can_topup_premium_traffic, "period_start_at": active.get("period_start_at").isoformat() if active.get("period_start_at") else None, "is_throttled": bool(active.get("is_throttled")), "max_devices": _coerce_int_or_none(active.get("max_devices")), @@ -3434,7 +3453,7 @@ def _serialize_topup_packages( "price": float(price or 0), "currency": settings.DEFAULT_CURRENCY_SYMBOL or "RUB", "title": f"{title_prefix}{_format_traffic_title(traffic_value, lang)}", - "subtitle": ("Premium-серверы" if lang == "ru" else "Premium servers") if sale_mode == "premium_topup" else tariff.name(lang), + "subtitle": tariff.premium_name(lang) if sale_mode == "premium_topup" else tariff.name(lang), } if stars_price is not None and int(stars_price) > 0: plan["stars_price"] = int(stars_price) @@ -4156,15 +4175,17 @@ def _coerce_int_or_none(value: Optional[Any]) -> Optional[int]: return None -def _format_bytes(value: Optional[Any]) -> str: +def _format_bytes(value: Optional[Any], *, zero_as_unlimited: bool = False) -> str: if value is None: return "N/A" try: size = float(value) except (TypeError, ValueError): return str(value) - if size <= 0: + if size <= 0 and zero_as_unlimited: return "∞" + if size <= 0: + size = 0 units = ["B", "KB", "MB", "GB", "TB"] index = 0 while size >= 1024 and index < len(units) - 1: diff --git a/bot/services/panel_api_service.py b/bot/services/panel_api_service.py index e862612..fec2452 100644 --- a/bot/services/panel_api_service.py +++ b/bot/services/panel_api_service.py @@ -606,6 +606,24 @@ class PanelApiService: logging.error("Failed to get internal squads. Response: %s", response_data) return None + async def get_internal_squad(self, squad_uuid: str) -> Optional[Dict[str, Any]]: + response_data = await self._request( + "GET", f"/internal-squads/{squad_uuid}", log_full_response=False + ) + if response_data and not response_data.get("error") and "response" in response_data: + response = response_data.get("response") + if isinstance(response, dict): + inner = response.get("internalSquad") or response.get("squad") + if isinstance(inner, dict): + return inner + return response + logging.error( + "Failed to get internal squad %s. Response: %s", + squad_uuid, + response_data, + ) + return None + async def get_internal_squad_accessible_nodes( self, squad_uuid: str, @@ -634,6 +652,20 @@ class PanelApiService: ) return None + async def get_hosts(self) -> Optional[List[Dict[str, Any]]]: + response_data = await self._request("GET", "/hosts", log_full_response=False) + if response_data and not response_data.get("error") and "response" in response_data: + response = response_data.get("response") + if isinstance(response, list): + return response + if isinstance(response, dict): + for key in ("hosts", "items", "data"): + value = response.get(key) + if isinstance(value, list): + return value + logging.error("Failed to get hosts. Response: %s", response_data) + return None + async def reset_user_traffic(self, user_uuid: str) -> bool: endpoint = f"/users/{user_uuid}/actions/reset-traffic" response_data = await self._request("POST", endpoint, log_full_response=False) diff --git a/bot/services/subscription_service.py b/bot/services/subscription_service.py index b033bd4..6372e38 100644 --- a/bot/services/subscription_service.py +++ b/bot/services/subscription_service.py @@ -119,7 +119,28 @@ class SubscriptionService: "node_labels": list(cached.get("node_labels") or []), } + def _extract_inbound_uuids(squad_obj: Dict[str, Any]) -> List[str]: + collected: List[str] = [] + for field in ("inbounds", "internalInbounds", "configProfileInbounds"): + value = squad_obj.get(field) + if not isinstance(value, list): + continue + for inbound in value: + if isinstance(inbound, dict): + ib_uuid = str( + inbound.get("uuid") + or inbound.get("inboundUuid") + or inbound.get("id") + or "" + ) + else: + ib_uuid = str(inbound or "") + if ib_uuid: + collected.append(ib_uuid) + return collected + squad_name_map: Dict[str, str] = {} + squad_inbound_map: Dict[str, List[str]] = {} try: squads = await self.panel_service.get_internal_squads() or [] for squad in squads: @@ -129,11 +150,80 @@ class SubscriptionService: if not squad_uuid: continue squad_name_map[squad_uuid] = str(squad.get("name") or squad.get("title") or squad_uuid) + squad_inbound_map[squad_uuid] = _extract_inbound_uuids(squad) except Exception: logging.debug("Failed to load internal squad names for premium display", exc_info=True) + for squad_uuid in tariff.premium_squad_uuids: + squad_uuid_str = str(squad_uuid) + if squad_inbound_map.get(squad_uuid_str): + continue + try: + detail = await self.panel_service.get_internal_squad(squad_uuid_str) + except Exception: + logging.debug("Failed to load internal squad detail for %s", squad_uuid_str, exc_info=True) + detail = None + if isinstance(detail, dict): + squad_inbound_map[squad_uuid_str] = _extract_inbound_uuids(detail) + if squad_uuid_str not in squad_name_map: + squad_name_map[squad_uuid_str] = str( + detail.get("name") or detail.get("title") or squad_uuid_str + ) + + hosts_by_inbound: Dict[str, List[Dict[str, Any]]] = {} + try: + hosts = await self.panel_service.get_hosts() or [] + for host in hosts: + if not isinstance(host, dict): + continue + inbound_field = host.get("inbound") if isinstance(host.get("inbound"), dict) else {} + inbound_uuid = ( + host.get("inboundUuid") + or host.get("inbound_uuid") + or host.get("configProfileInboundUuid") + or inbound_field.get("configProfileInboundUuid") + or inbound_field.get("inboundUuid") + or inbound_field.get("uuid") + or "" + ) + inbound_uuid = str(inbound_uuid) + if not inbound_uuid: + continue + hosts_by_inbound.setdefault(inbound_uuid, []).append(host) + logging.debug( + "Premium label resolution: %d hosts grouped across %d inbounds; squad inbound map: %s", + len(hosts), + len(hosts_by_inbound), + {k: len(v) for k, v in squad_inbound_map.items()}, + ) + except Exception: + logging.debug("Failed to load hosts for premium display", exc_info=True) + + def _host_remark(host: Dict[str, Any]) -> str: + for key in ("remark", "name", "label", "title"): + value = host.get(key) + if value is None: + continue + candidate = str(value).strip() + if candidate: + return candidate + return "" + node_labels: List[str] = [] for squad_uuid in tariff.premium_squad_uuids: + squad_uuid_str = str(squad_uuid) + inbound_uuids = squad_inbound_map.get(squad_uuid_str) or [] + host_labels_for_squad: List[str] = [] + for inbound_uuid in inbound_uuids: + for host in hosts_by_inbound.get(inbound_uuid, []): + remark = _host_remark(host) + if remark: + host_labels_for_squad.append(remark) + + if host_labels_for_squad: + node_labels.extend(host_labels_for_squad) + continue + try: nodes = await self.panel_service.get_internal_squad_accessible_nodes(squad_uuid) or [] except Exception: @@ -143,7 +233,15 @@ class SubscriptionService: if not isinstance(node, dict): continue node_uuid = str(node.get("uuid") or node.get("nodeUuid") or node.get("node_uuid") or "") - node_name = str(node.get("name") or node.get("address") or node.get("host") or "").strip() + node_name = "" + for key in ("nodeName", "name", "nodeRemark", "remark", "label", "title", "address", "host"): + value = node.get(key) + if value is None: + continue + candidate = str(value).strip() + if candidate: + node_name = candidate + break if node_name: label = node_name elif node_uuid: @@ -1820,6 +1918,7 @@ class SubscriptionService: "tariff_key": local_active_sub.tariff_key if local_active_sub else None, "tariff_name": tariff.name(db_user.language_code or self.settings.DEFAULT_LANGUAGE) if tariff else None, "tariff_description": tariff.description(db_user.language_code or self.settings.DEFAULT_LANGUAGE) if tariff else None, + "premium_title": tariff.premium_name(db_user.language_code or self.settings.DEFAULT_LANGUAGE) if tariff else None, "billing_model": billing_model_display, "tier_baseline_bytes": local_active_sub.tier_baseline_bytes if local_active_sub else None, "topup_balance_bytes": local_active_sub.topup_balance_bytes if local_active_sub else 0, diff --git a/config/tariffs_config.py b/config/tariffs_config.py index eed4d00..dc05424 100644 --- a/config/tariffs_config.py +++ b/config/tariffs_config.py @@ -62,6 +62,7 @@ class Tariff(BaseModel): key: str names: Dict[str, str] = Field(default_factory=dict) descriptions: Dict[str, str] = Field(default_factory=dict) + premium_names: Dict[str, str] = Field(default_factory=dict) squad_uuids: List[str] = Field(default_factory=list) billing_model: BillingModel enabled: bool = True @@ -130,6 +131,10 @@ class Tariff(BaseModel): def description(self, lang: str, fallback: str = "ru") -> str: return self.descriptions.get(lang) or self.descriptions.get(fallback) or "" + def premium_name(self, lang: str, fallback: str = "ru") -> str: + default = "Premium-серверы" if (lang or fallback) == "ru" else "Premium servers" + return self.premium_names.get(lang) or self.premium_names.get(fallback) or default + @property def monthly_bytes(self) -> int: if self.monthly_gb is None or self.monthly_gb <= 0: diff --git a/tests/test_tariffs_config.py b/tests/test_tariffs_config.py index 71782ca..cbade11 100644 --- a/tests/test_tariffs_config.py +++ b/tests/test_tariffs_config.py @@ -132,6 +132,7 @@ class TariffsConfigTests(unittest.TestCase): def test_premium_squad_limit_and_topups_load(self): data = _valid_config() data["tariffs"][0]["premium_squad_uuids"] = [" premium-squad "] + data["tariffs"][0]["premium_names"] = {"ru": "Обход глушилок", "en": "Anti-jamming"} data["tariffs"][0]["premium_monthly_gb"] = 50 data["tariffs"][0]["premium_topup_packages"] = { "rub": [{"gb": 10, "price": 99}], @@ -142,6 +143,8 @@ class TariffsConfigTests(unittest.TestCase): tariff = config.require("standard") self.assertEqual(tariff.premium_squad_uuids, ["premium-squad"]) + self.assertEqual(tariff.premium_name("ru"), "Обход глушилок") + self.assertEqual(tariff.premium_name("en"), "Anti-jamming") self.assertEqual(tariff.premium_monthly_bytes, 50 * 1024**3) self.assertTrue(tariff.has_premium_squad_limit())