Merge pull request #1161 from Fr1ngg/revert-1160-u5iv6j-bedolaga/update-miniapp/index.html-logic

Revert "Handle registration and empty subscription states in miniapp"
This commit is contained in:
Egor
2025-10-11 00:59:41 +03:00
committed by GitHub
3 changed files with 33 additions and 402 deletions
+28 -58
View File
@@ -2085,69 +2085,39 @@ async def get_subscription_details(
user = await get_user_by_telegram_id(db, telegram_id)
purchase_url = (settings.MINIAPP_PURCHASE_URL or "").strip()
if not user:
bot_username = settings.get_bot_username()
detail: Dict[str, Any] = {
"code": "user_not_registered",
"message": "User is not registered in the bot",
}
if bot_username:
detail["bot_url"] = f"https://t.me/{bot_username}"
if not user or not user.subscription:
detail: Union[str, Dict[str, str]] = "Subscription not found"
if purchase_url:
detail["purchase_url"] = purchase_url
detail["title"] = "Registration required"
detail = {
"message": "Subscription not found",
"purchase_url": purchase_url,
}
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=detail,
)
subscription = user.subscription
traffic_used = 0.0
traffic_limit = 0
traffic_used = _format_gb(subscription.traffic_used_gb)
traffic_limit = subscription.traffic_limit_gb or 0
lifetime_used = _bytes_to_gb(getattr(user, "lifetime_used_traffic_bytes", 0))
status_actual = "inactive"
subscription_status_value = "inactive"
subscription_type_value = "none"
subscription_id = 0
remnawave_short_uuid: Optional[str] = None
subscription_url: Optional[str] = None
subscription_crypto_link: Optional[str] = None
happ_redirect_link: Optional[str] = None
links_payload: Dict[str, Any] = {}
connected_squads: List[str] = []
connected_servers: List[MiniAppConnectedServer] = []
links: List[str] = []
ss_conf_links: Dict[str, str] = {}
status_actual = subscription.actual_status
links_payload = await _load_subscription_links(subscription)
if subscription:
traffic_used = _format_gb(subscription.traffic_used_gb)
traffic_limit = subscription.traffic_limit_gb or 0
status_actual = subscription.actual_status
subscription_status_value = subscription.status
subscription_type_value = "trial" if subscription.is_trial else "paid"
subscription_id = subscription.id
remnawave_short_uuid = subscription.remnawave_short_uuid
links_payload = await _load_subscription_links(subscription)
subscription_url = (
links_payload.get("subscription_url")
or subscription.subscription_url
)
subscription_crypto_link = (
links_payload.get("happ_crypto_link")
or subscription.subscription_crypto_link
)
happ_redirect_link = get_happ_cryptolink_redirect_link(
subscription_crypto_link
)
connected_squads = list(subscription.connected_squads or [])
connected_servers = await _resolve_connected_servers(db, connected_squads)
links = links_payload.get("links") or connected_squads
ss_conf_links = links_payload.get("ss_conf_links") or {}
subscription_url = links_payload.get("subscription_url") or subscription.subscription_url
subscription_crypto_link = (
links_payload.get("happ_crypto_link")
or subscription.subscription_crypto_link
)
happ_redirect_link = get_happ_cryptolink_redirect_link(subscription_crypto_link)
connected_squads: List[str] = list(subscription.connected_squads or [])
connected_servers = await _resolve_connected_servers(db, connected_squads)
devices_count, devices = await _load_devices_info(user)
links: List[str] = links_payload.get("links") or connected_squads
ss_conf_links: Dict[str, str] = links_payload.get("ss_conf_links") or {}
transactions_query = (
select(Transaction)
@@ -2357,11 +2327,11 @@ async def get_subscription_details(
),
language=user.language,
status=user.status,
subscription_status=subscription_status_value,
subscription_status=subscription.status,
subscription_actual_status=status_actual,
status_label=_status_label(status_actual),
expires_at=subscription.end_date if subscription else None,
device_limit=subscription.device_limit if subscription else 0,
expires_at=subscription.end_date,
device_limit=subscription.device_limit,
traffic_used_gb=round(traffic_used, 2),
traffic_used_label=_format_gb_label(traffic_used),
traffic_limit_gb=traffic_limit,
@@ -2376,8 +2346,8 @@ async def get_subscription_details(
referral_info = await _build_referral_info(db, user)
return MiniAppSubscriptionResponse(
subscription_id=subscription_id,
remnawave_short_uuid=remnawave_short_uuid,
subscription_id=subscription.id,
remnawave_short_uuid=subscription.remnawave_short_uuid,
user=response_user,
subscription_url=subscription_url,
subscription_crypto_link=subscription_crypto_link,
@@ -2410,8 +2380,8 @@ async def get_subscription_details(
total_spent_kopeks=total_spent_kopeks,
total_spent_rubles=round(total_spent_kopeks / 100, 2),
total_spent_label=settings.format_price(total_spent_kopeks),
subscription_type=subscription_type_value,
autopay_enabled=bool(subscription.autopay_enabled) if subscription else False,
subscription_type="trial" if subscription.is_trial else "paid",
autopay_enabled=bool(subscription.autopay_enabled),
branding=settings.get_miniapp_branding(),
faq=faq_payload,
legal_documents=legal_documents_payload,
+5 -212
View File
@@ -331,31 +331,6 @@
min-width: 220px;
}
.register-callout {
margin-top: 24px;
padding: 20px;
border-radius: var(--radius-xl);
background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.12), rgba(var(--primary-rgb), 0.04));
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
text-align: center;
}
.register-callout-title {
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
}
.register-callout-text {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.5;
}
/* Cards */
.card {
background: var(--bg-secondary);
@@ -371,44 +346,6 @@
transform: translateY(-2px);
}
.no-subscription-card {
display: flex;
align-items: center;
gap: 18px;
padding: 20px;
border: 1px solid rgba(var(--primary-rgb), 0.18);
background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.1), rgba(var(--primary-rgb), 0.02));
}
.no-subscription-icon {
font-size: 32px;
line-height: 1;
}
.no-subscription-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.no-subscription-title {
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
}
.no-subscription-text {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.5;
}
.no-subscription-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.card-header {
padding: 16px;
cursor: pointer;
@@ -769,16 +706,6 @@
color: #fff;
}
:root[data-theme="dark"] .register-callout {
background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.2), rgba(var(--primary-rgb), 0.08));
box-shadow: var(--shadow-md);
}
:root[data-theme="dark"] .no-subscription-card {
border-color: rgba(var(--primary-rgb), 0.32);
background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.28), rgba(var(--primary-rgb), 0.1));
}
.subscription-purchase-card {
position: relative;
margin-top: 16px;
@@ -4286,11 +4213,6 @@
<div class="error-icon">⚠️</div>
<div class="error-title" id="errorTitle" data-i18n="error.default.title">Subscription Not Found</div>
<div class="error-text" id="errorText" data-i18n="error.default.message">Please contact support to activate your subscription</div>
<div class="register-callout hidden" id="registerCallout">
<div class="register-callout-title" data-i18n="register.callout.title">Register in the bot</div>
<div class="register-callout-text" data-i18n="register.callout.text">Open the Telegram bot to finish setting up your account before purchasing a subscription.</div>
<button class="btn btn-primary" type="button" id="openBotBtn" data-i18n="register.callout.action">Open bot</button>
</div>
<div class="error-actions">
<button
id="purchaseBtn"
@@ -4306,17 +4228,6 @@
<!-- Promo Offers -->
<div id="promoOffersContainer" class="promo-offers hidden"></div>
<div class="card no-subscription-card hidden" id="noSubscriptionCard">
<div class="no-subscription-icon" aria-hidden="true">🛡️</div>
<div class="no-subscription-content">
<div class="no-subscription-title" data-i18n="no_subscription.title">No active subscription</div>
<div class="no-subscription-text" data-i18n="no_subscription.subtitle">Purchase a VPN plan to start using the service.</div>
<div class="no-subscription-actions">
<button class="btn btn-primary" type="button" id="noSubscriptionPurchaseBtn" data-i18n="no_subscription.action">Buy subscription</button>
</div>
</div>
</div>
<!-- User Card -->
<div class="card user-card animate-in">
<div class="user-header">
@@ -5061,8 +4972,6 @@
'app.loading': 'Loading your subscription...',
'error.default.title': 'Subscription Not Found',
'error.default.message': 'Please contact support to activate your subscription.',
'error.user_not_registered.title': 'Registration required',
'error.user_not_registered.message': 'Please open the Telegram bot and register to continue.',
'stats.days_left': 'Days left',
'stats.servers': 'Servers',
'stats.devices': 'Devices',
@@ -5076,12 +4985,6 @@
'button.connect.default': 'Connect to VPN',
'button.connect.happ': 'Connect',
'button.copy': 'Copy subscription link',
'register.callout.title': 'Register in the bot',
'register.callout.text': 'Open the Telegram bot to finish setting up your account before purchasing a subscription.',
'register.callout.action': 'Open bot',
'no_subscription.title': 'No active subscription',
'no_subscription.subtitle': 'Purchase a VPN plan to start using the service.',
'no_subscription.action': 'Buy subscription',
'button.topup_balance': 'Top up balance',
'topup.title': 'Top up balance',
'topup.subtitle': 'Choose a payment method',
@@ -5377,11 +5280,9 @@
'status.trial': 'Trial',
'status.expired': 'Expired',
'status.disabled': 'Disabled',
'status.inactive': 'Inactive',
'status.unknown': 'Unknown',
'subscription.type.trial': 'Trial',
'subscription.type.paid': 'Paid',
'subscription.type.none': 'Not active',
'autopay.enabled': 'Enabled',
'autopay.disabled': 'Disabled',
'platform.ios': 'iOS',
@@ -5426,8 +5327,6 @@
'app.loading': 'Загружаем вашу подписку...',
'error.default.title': 'Подписка не найдена',
'error.default.message': 'Свяжитесь с поддержкой, чтобы активировать подписку.',
'error.user_not_registered.title': 'Нужна регистрация',
'error.user_not_registered.message': 'Откройте Telegram-бота и завершите регистрацию, чтобы продолжить.',
'stats.days_left': 'Осталось дней',
'stats.servers': 'Серверы',
'stats.devices': 'Устройства',
@@ -5441,12 +5340,6 @@
'button.connect.default': 'Подключиться к VPN',
'button.connect.happ': 'Подключиться',
'button.copy': 'Скопировать ссылку подписки',
'register.callout.title': 'Зарегистрируйтесь в боте',
'register.callout.text': 'Откройте Telegram-бота, чтобы завершить регистрацию перед покупкой подписки.',
'register.callout.action': 'Открыть бота',
'no_subscription.title': 'Подписка не активирована',
'no_subscription.subtitle': 'Купите подписку, чтобы начать пользоваться VPN.',
'no_subscription.action': 'Купить подписку',
'button.topup_balance': 'Пополнить баланс',
'topup.title': 'Пополнение баланса',
'topup.subtitle': 'Выберите способ оплаты',
@@ -5742,11 +5635,9 @@
'status.trial': 'Пробная',
'status.expired': 'Истекла',
'status.disabled': 'Отключена',
'status.inactive': 'Неактивна',
'status.unknown': 'Неизвестно',
'subscription.type.trial': 'Триал',
'subscription.type.paid': 'Платная',
'subscription.type.none': 'Нет подписки',
'autopay.enabled': 'Включен',
'autopay.disabled': 'Выключен',
'platform.ios': 'iOS',
@@ -6589,47 +6480,16 @@
if (!titleElement || !textElement) {
return;
}
let title = currentErrorState?.title || t('error.default.title');
let message = currentErrorState?.message || t('error.default.message');
const isRegistrationError = currentErrorState?.code === 'user_not_registered';
if (isRegistrationError) {
const titleKey = 'error.user_not_registered.title';
const messageKey = 'error.user_not_registered.message';
const translatedTitle = t(titleKey);
const translatedMessage = t(messageKey);
if (translatedTitle && translatedTitle !== titleKey) {
title = translatedTitle;
}
if (translatedMessage && translatedMessage !== messageKey) {
message = translatedMessage;
}
}
const title = currentErrorState?.title || t('error.default.title');
const message = currentErrorState?.message || t('error.default.message');
titleElement.textContent = title;
textElement.textContent = message;
const registerCallout = document.getElementById('registerCallout');
const botButton = document.getElementById('openBotBtn');
const normalizedBotUrl = normalizeUrl(currentErrorState?.botUrl);
if (registerCallout) {
registerCallout.classList.toggle('hidden', !isRegistrationError);
}
if (botButton) {
const hasLink = Boolean(isRegistrationError && normalizedBotUrl);
if (hasLink) {
botButton.dataset.botUrl = normalizedBotUrl;
} else {
delete botButton.dataset.botUrl;
}
botButton.disabled = !hasLink;
}
const purchaseButton = document.getElementById('purchaseBtn');
if (purchaseButton) {
const link = getEffectivePurchaseUrl();
const shouldHide = !link || isRegistrationError;
purchaseButton.classList.toggle('hidden', shouldHide);
purchaseButton.disabled = shouldHide;
purchaseButton.classList.toggle('hidden', !link);
purchaseButton.disabled = !link;
}
}
@@ -6762,8 +6622,6 @@
: 'Subscription not found';
let title = response.status === 401 ? 'Authorization Error' : 'Subscription Not Found';
let purchaseUrl = null;
let errorCode = null;
let botUrl = null;
try {
const errorPayload = await response.json();
@@ -6774,16 +6632,9 @@
if (typeof errorPayload.detail.message === 'string') {
detail = errorPayload.detail.message;
}
if (typeof errorPayload.detail.code === 'string') {
errorCode = errorPayload.detail.code;
}
purchaseUrl = errorPayload.detail.purchase_url
|| errorPayload.detail.purchaseUrl
|| purchaseUrl;
botUrl = botUrl
|| errorPayload.detail.bot_url
|| errorPayload.detail.botUrl
|| botUrl;
}
} else if (typeof errorPayload?.message === 'string') {
detail = errorPayload.message;
@@ -6797,11 +6648,6 @@
|| errorPayload?.purchase_url
|| errorPayload?.purchaseUrl
|| null;
errorCode = errorCode || (typeof errorPayload?.code === 'string' ? errorPayload.code : errorCode);
botUrl = botUrl
|| errorPayload?.bot_url
|| errorPayload?.botUrl
|| botUrl;
} catch (parseError) {
// ignore JSON parsing errors
}
@@ -6811,13 +6657,6 @@
if (normalizedPurchaseUrl) {
errorObject.purchaseUrl = normalizedPurchaseUrl;
}
if (typeof errorCode === 'string' && errorCode) {
errorObject.code = errorCode;
}
const normalizedBotUrl = normalizeUrl(botUrl);
if (normalizedBotUrl) {
errorObject.botUrl = normalizedBotUrl;
}
throw errorObject;
}
@@ -6831,15 +6670,6 @@
userData.subscriptionCryptoLink = userData.subscription_crypto_link || null;
userData.referral = userData.referral || null;
const rawSubscriptionId = userData.subscription_id ?? userData.subscriptionId;
if (rawSubscriptionId !== undefined && rawSubscriptionId !== null) {
const normalizedId = Number(rawSubscriptionId);
if (!Number.isFinite(normalizedId) || normalizedId <= 0) {
userData.subscription_id = null;
userData.subscriptionId = null;
}
}
resetSubscriptionRenewalState(null);
prepareSubscriptionRenewalFromUserData();
@@ -6960,16 +6790,6 @@
}
const user = userData.user;
const hasActiveSubscription = Boolean(user?.has_active_subscription);
const userCardElement = document.querySelector('.user-card');
if (userCardElement) {
userCardElement.classList.toggle('hidden', !hasActiveSubscription);
}
const noSubscriptionCard = document.getElementById('noSubscriptionCard');
if (noSubscriptionCard) {
noSubscriptionCard.classList.toggle('hidden', hasActiveSubscription);
}
const rawName = user.display_name || user.username || '';
const fallbackName = rawName
|| [user.first_name, user.last_name].filter(Boolean).join(' ')
@@ -6979,7 +6799,7 @@
document.getElementById('userAvatar').textContent = avatarChar;
document.getElementById('userName').textContent = fallbackName;
const knownStatuses = ['active', 'trial', 'expired', 'disabled', 'inactive'];
const knownStatuses = ['active', 'trial', 'expired', 'disabled'];
const statusValueRaw = (user.subscription_actual_status || user.subscription_status || 'active').toLowerCase();
const statusClass = knownStatuses.includes(statusValueRaw) ? statusValueRaw : 'unknown';
const statusBadge = document.getElementById('statusBadge');
@@ -15460,8 +15280,6 @@
title: error?.title,
message: error?.message,
purchaseUrl: normalizeUrl(error?.purchaseUrl) || null,
code: error?.code || null,
botUrl: normalizeUrl(error?.botUrl) || null,
};
updateErrorTexts();
document.getElementById('errorState').classList.remove('hidden');
@@ -15482,17 +15300,6 @@
openExternalLink(link);
});
const openBotBtn = document.getElementById('openBotBtn');
if (openBotBtn) {
openBotBtn.addEventListener('click', () => {
const url = openBotBtn.dataset.botUrl || currentErrorState?.botUrl;
const normalized = normalizeUrl(url);
if (normalized) {
openExternalLink(normalized);
}
});
}
const topupButton = document.getElementById('topupBalanceBtn');
if (topupButton) {
topupButton.addEventListener('click', () => {
@@ -15500,20 +15307,6 @@
});
}
const noSubscriptionPurchaseBtn = document.getElementById('noSubscriptionPurchaseBtn');
if (noSubscriptionPurchaseBtn) {
noSubscriptionPurchaseBtn.addEventListener('click', () => {
if (shouldShowPurchaseConfigurator()) {
openSubscriptionPurchaseModal();
return;
}
const link = getEffectivePurchaseUrl();
if (link) {
openExternalLink(link);
}
});
}
const topupModal = document.getElementById('topupModal');
if (topupModal) {
topupModal.addEventListener('click', event => {
-132
View File
@@ -5,7 +5,6 @@ from datetime import datetime, timedelta
from pathlib import Path
import pytest
from fastapi import HTTPException, status
ROOT_DIR = Path(__file__).resolve().parents[1]
if str(ROOT_DIR) not in sys.path:
@@ -23,7 +22,6 @@ from app.webapi.schemas.miniapp import (
MiniAppPaymentCreateRequest,
MiniAppPaymentMethodsRequest,
MiniAppPaymentStatusQuery,
MiniAppSubscriptionRequest,
)
@@ -201,136 +199,6 @@ async def test_resolve_pal24_status_includes_identifiers(monkeypatch):
assert result.extra['remote_status'] == 'PAID'
@pytest.mark.anyio("asyncio")
async def test_get_subscription_details_returns_registration_error(monkeypatch):
async def fake_get_user_by_telegram_id(db, telegram_id):
return None
monkeypatch.setattr(miniapp, 'parse_webapp_init_data', lambda payload, token: {'user': {'id': 123}})
monkeypatch.setattr(miniapp, 'get_user_by_telegram_id', fake_get_user_by_telegram_id)
monkeypatch.setattr(miniapp.settings, 'MINIAPP_PURCHASE_URL', 'https://example.com/buy', raising=False)
monkeypatch.setattr(miniapp.settings, 'BOT_TOKEN', 'token', raising=False)
monkeypatch.setattr(miniapp.settings, 'BOT_USERNAME', 'awesome_bot', raising=False)
request = MiniAppSubscriptionRequest(initData='payload')
with pytest.raises(HTTPException) as excinfo:
await miniapp.get_subscription_details(request, db=types.SimpleNamespace())
error = excinfo.value
assert error.status_code == status.HTTP_404_NOT_FOUND
assert isinstance(error.detail, dict)
assert error.detail['code'] == 'user_not_registered'
assert error.detail['title'] == 'Registration required'
assert error.detail['purchase_url'] == 'https://example.com/buy'
assert error.detail['bot_url'] == 'https://t.me/awesome_bot'
@pytest.mark.anyio("asyncio")
async def test_get_subscription_details_without_active_subscription(monkeypatch):
user = types.SimpleNamespace(
id=1,
telegram_id=123,
username='testuser',
first_name='Test',
last_name='User',
language='ru',
status='active',
subscription=None,
balance_kopeks=0,
balance_rubles=0.0,
balance_currency='RUB',
promo_group=None,
promo_offer_discount_percent=0,
promo_offer_discount_expires_at=None,
promo_offer_discount_source=None,
lifetime_used_traffic_bytes=0,
)
class DummyResult:
def scalars(self):
return types.SimpleNamespace(all=lambda: [])
class DummyDB:
async def execute(self, query):
return DummyResult()
async def fake_get_user_by_telegram_id(db, telegram_id):
return user
async def fake_load_devices_info(_: types.SimpleNamespace):
return 0, []
async def fake_get_user_total_spent_kopeks(db, user_id):
return 0
async def fake_get_auto_assign_groups(db):
return []
async def fake_list_active_discount_offers_for_user(db, user_id):
return []
async def fake_get_latest_claimed_offer_for_user(db, user_id, source):
return None
async def fake_find_active_test_access_offers(db, subscription):
return []
async def fake_build_promo_offer_models(db, offers, contexts, user=None):
return []
async def fake_build_referral_info(db, user_instance):
return None
async def fake_get_faq_pages(db, language, include_inactive=False, fallback=True):
return []
async def fake_get_faq_setting(db, language, fallback=True):
return None
async def fake_get_public_offer(db, language):
return None
async def fake_get_privacy_policy(db, language):
return None
async def fake_get_rules_by_language(db, language):
return None
monkeypatch.setattr(miniapp, 'parse_webapp_init_data', lambda payload, token: {'user': {'id': 123}})
monkeypatch.setattr(miniapp, 'get_user_by_telegram_id', fake_get_user_by_telegram_id)
monkeypatch.setattr(miniapp, '_load_devices_info', fake_load_devices_info)
monkeypatch.setattr(miniapp, 'get_user_total_spent_kopeks', fake_get_user_total_spent_kopeks)
monkeypatch.setattr(miniapp, 'get_auto_assign_promo_groups', fake_get_auto_assign_groups)
monkeypatch.setattr(miniapp, 'list_active_discount_offers_for_user', fake_list_active_discount_offers_for_user)
monkeypatch.setattr(miniapp, 'get_latest_claimed_offer_for_user', fake_get_latest_claimed_offer_for_user)
monkeypatch.setattr(miniapp, '_find_active_test_access_offers', fake_find_active_test_access_offers)
monkeypatch.setattr(miniapp, '_build_promo_offer_models', fake_build_promo_offer_models)
monkeypatch.setattr(miniapp, '_build_referral_info', fake_build_referral_info)
monkeypatch.setattr(miniapp.FaqService, 'get_pages', fake_get_faq_pages)
monkeypatch.setattr(miniapp.FaqService, 'get_setting', fake_get_faq_setting)
monkeypatch.setattr(miniapp.PublicOfferService, 'get_active_offer', fake_get_public_offer)
monkeypatch.setattr(miniapp.PrivacyPolicyService, 'get_active_policy', fake_get_privacy_policy)
monkeypatch.setattr(miniapp, 'get_rules_by_language', fake_get_rules_by_language)
monkeypatch.setattr(miniapp.settings, 'MINIAPP_PURCHASE_URL', 'https://example.com/buy', raising=False)
monkeypatch.setattr(miniapp.settings, 'BOT_TOKEN', 'token', raising=False)
request = MiniAppSubscriptionRequest(initData='payload')
response = await miniapp.get_subscription_details(request, db=DummyDB())
assert response.success is True
assert response.subscription_id == 0
assert response.subscription_type == 'none'
assert response.subscription_purchase_url == 'https://example.com/buy'
assert response.user.has_active_subscription is False
assert response.user.subscription_actual_status == 'inactive'
assert response.user.display_name == 'testuser'
assert response.balance_rubles == 0.0
assert response.transactions == []
assert response.promo_offers == []
assert response.referral is None
@pytest.mark.anyio("asyncio")
async def test_create_payment_link_stars_normalizes_amount(monkeypatch):
monkeypatch.setattr(settings, 'TELEGRAM_STARS_ENABLED', True, raising=False)