mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-20 19:31:07 +00:00
Merge branch 'main' of https://github.com/Gy9vin/remnawave-bedolaga-telegram-bot
This commit is contained in:
6
.github/workflows/docker-hub.yml
vendored
6
.github/workflows/docker-hub.yml
vendored
@@ -36,15 +36,15 @@ jobs:
|
||||
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
|
||||
echo "🏷️ Собираем релизную версию: $VERSION"
|
||||
elif [[ $GITHUB_REF == refs/heads/main ]]; then
|
||||
VERSION="v2.9.3-$(git rev-parse --short HEAD)"
|
||||
VERSION="v2.9.4-$(git rev-parse --short HEAD)"
|
||||
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
|
||||
echo "🚀 Собираем версию из main: $VERSION"
|
||||
elif [[ $GITHUB_REF == refs/heads/dev ]]; then
|
||||
VERSION="v2.9.3-dev-$(git rev-parse --short HEAD)"
|
||||
VERSION="v2.9.4-dev-$(git rev-parse --short HEAD)"
|
||||
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:dev,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
|
||||
echo "🧪 Собираем dev версию: $VERSION"
|
||||
else
|
||||
VERSION="v2.9.3-pr-$(git rev-parse --short HEAD)"
|
||||
VERSION="v2.9.4-pr-$(git rev-parse --short HEAD)"
|
||||
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:pr-$(git rev-parse --short HEAD)"
|
||||
echo "🔀 Собираем PR версию: $VERSION"
|
||||
fi
|
||||
|
||||
6
.github/workflows/docker-registry.yml
vendored
6
.github/workflows/docker-registry.yml
vendored
@@ -49,13 +49,13 @@ jobs:
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
echo "🏷️ Building release version: $VERSION"
|
||||
elif [[ $GITHUB_REF == refs/heads/main ]]; then
|
||||
VERSION="v2.9.3-$(git rev-parse --short HEAD)"
|
||||
VERSION="v2.9.4-$(git rev-parse --short HEAD)"
|
||||
echo "🚀 Building main version: $VERSION"
|
||||
elif [[ $GITHUB_REF == refs/heads/dev ]]; then
|
||||
VERSION="v2.9.3-dev-$(git rev-parse --short HEAD)"
|
||||
VERSION="v2.9.4-dev-$(git rev-parse --short HEAD)"
|
||||
echo "🧪 Building dev version: $VERSION"
|
||||
else
|
||||
VERSION="v2.9.3-pr-$(git rev-parse --short HEAD)"
|
||||
VERSION="v2.9.4-pr-$(git rev-parse --short HEAD)"
|
||||
echo "🔀 Building PR version: $VERSION"
|
||||
fi
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
|
||||
|
||||
FROM python:3.13-slim
|
||||
|
||||
ARG VERSION="v2.9.3"
|
||||
ARG VERSION="v2.9.4"
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
|
||||
|
||||
@@ -158,6 +158,10 @@ class Settings(BaseSettings):
|
||||
BASE_PROMO_GROUP_PERIOD_DISCOUNTS_ENABLED: bool = False
|
||||
BASE_PROMO_GROUP_PERIOD_DISCOUNTS: str = ""
|
||||
|
||||
# Режим выбора трафика:
|
||||
# - selectable: пользователь выбирает трафик при покупке и может докупать
|
||||
# - fixed: фиксированный лимит, без выбора и без докупки
|
||||
# - fixed_with_topup: фиксированный лимит при покупке, но докупка разрешена (при продлении сброс до лимита)
|
||||
TRAFFIC_SELECTION_MODE: str = "selectable"
|
||||
FIXED_TRAFFIC_LIMIT_GB: int = 100
|
||||
BUY_TRAFFIC_BUTTON_VISIBLE: bool = True
|
||||
@@ -1058,6 +1062,11 @@ class Settings(BaseSettings):
|
||||
return self.TRAFFIC_SELECTION_MODE.lower() == "selectable"
|
||||
|
||||
def is_traffic_fixed(self) -> bool:
|
||||
"""Возвращает True если выбор трафика отключён (fixed или fixed_with_topup)"""
|
||||
return self.TRAFFIC_SELECTION_MODE.lower() in ("fixed", "fixed_with_topup")
|
||||
|
||||
def is_traffic_topup_blocked(self) -> bool:
|
||||
"""Возвращает True если докупка трафика полностью заблокирована (только fixed)"""
|
||||
return self.TRAFFIC_SELECTION_MODE.lower() == "fixed"
|
||||
|
||||
def get_fixed_traffic_limit(self) -> int:
|
||||
|
||||
@@ -344,6 +344,15 @@ async def extend_subscription(
|
||||
subscription.purchased_traffic_gb = 0 # Сбрасываем докупленный трафик вместе с использованным
|
||||
logger.info("🔄 Сбрасываем использованный и докупленный трафик согласно настройке RESET_TRAFFIC_ON_PAYMENT")
|
||||
|
||||
# В режиме fixed_with_topup при продлении сбрасываем трафик до фиксированного лимита
|
||||
if settings.is_traffic_fixed() and days > 0:
|
||||
fixed_limit = settings.get_fixed_traffic_limit()
|
||||
old_limit = subscription.traffic_limit_gb
|
||||
if subscription.traffic_limit_gb != fixed_limit or (subscription.purchased_traffic_gb or 0) > 0:
|
||||
subscription.traffic_limit_gb = fixed_limit
|
||||
subscription.purchased_traffic_gb = 0
|
||||
logger.info(f"🔄 Сброс трафика при продлении (fixed_with_topup): {old_limit} ГБ → {fixed_limit} ГБ")
|
||||
|
||||
subscription.updated_at = current_time
|
||||
|
||||
await db.commit()
|
||||
@@ -1158,7 +1167,12 @@ async def get_subscription_renewal_cost(
|
||||
total_servers_cost = discounted_servers_per_month * months_in_period
|
||||
total_servers_discount = servers_discount_per_month * months_in_period
|
||||
|
||||
traffic_price_per_month = settings.get_traffic_price(subscription.traffic_limit_gb)
|
||||
# В режиме fixed_with_topup при продлении используем фиксированный лимит
|
||||
if settings.is_traffic_fixed():
|
||||
renewal_traffic_gb = settings.get_fixed_traffic_limit()
|
||||
else:
|
||||
renewal_traffic_gb = subscription.traffic_limit_gb
|
||||
traffic_price_per_month = settings.get_traffic_price(renewal_traffic_gb)
|
||||
traffic_discount_percent = _get_discount_percent(
|
||||
user,
|
||||
promo_group,
|
||||
|
||||
@@ -198,6 +198,7 @@ CORE_PRICING_ENTRIES: Tuple[SettingEntry, ...] = (
|
||||
choices=(
|
||||
ChoiceOption("selectable", "Выбор пакетов", "Selectable"),
|
||||
ChoiceOption("fixed", "Фиксированный лимит", "Fixed limit"),
|
||||
ChoiceOption("fixed_with_topup", "Фикс. лимит + докупка", "Fixed + topup"),
|
||||
),
|
||||
description_ru="Определяет, выбирают ли пользователи пакеты или получают фиксированный лимит.",
|
||||
description_en="Defines whether users pick packages or use a fixed limit.",
|
||||
@@ -351,8 +352,11 @@ def _format_core_summary(lang_code: str) -> str:
|
||||
base_price = settings.format_price(settings.BASE_SUBSCRIPTION_PRICE)
|
||||
device_limit = settings.DEFAULT_DEVICE_LIMIT
|
||||
traffic_limit = settings.DEFAULT_TRAFFIC_LIMIT_GB
|
||||
if settings.TRAFFIC_SELECTION_MODE == "fixed":
|
||||
mode = settings.TRAFFIC_SELECTION_MODE.lower()
|
||||
if mode == "fixed":
|
||||
traffic_mode = "⚙️ fixed"
|
||||
elif mode == "fixed_with_topup":
|
||||
traffic_mode = "⚙️ fixed+topup"
|
||||
else:
|
||||
traffic_mode = "⚙️ selectable"
|
||||
traffic_label = _format_traffic_label(traffic_limit, lang_code, short=True)
|
||||
|
||||
@@ -1362,7 +1362,12 @@ async def handle_extend_subscription(
|
||||
devices_price_info = calculate_user_price(db_user, devices_total_base, days, "devices")
|
||||
|
||||
# 4. Calculate traffic price with promo group discount
|
||||
traffic_price_per_month = settings.get_traffic_price(subscription.traffic_limit_gb)
|
||||
# В режиме fixed_with_topup при продлении трафик сбрасывается до фиксированного лимита
|
||||
if settings.is_traffic_fixed():
|
||||
renewal_traffic_gb = settings.get_fixed_traffic_limit()
|
||||
else:
|
||||
renewal_traffic_gb = subscription.traffic_limit_gb
|
||||
traffic_price_per_month = settings.get_traffic_price(renewal_traffic_gb)
|
||||
traffic_total_base = traffic_price_per_month * months_in_period
|
||||
traffic_price_info = calculate_user_price(db_user, traffic_total_base, days, "traffic")
|
||||
|
||||
@@ -1579,7 +1584,12 @@ async def confirm_extend_subscription(
|
||||
discounted_devices_price_per_month = devices_price_per_month - devices_discount_per_month
|
||||
total_devices_price = discounted_devices_price_per_month * months_in_period
|
||||
|
||||
traffic_price_per_month = settings.get_traffic_price(subscription.traffic_limit_gb)
|
||||
# В режиме fixed_with_topup при продлении трафик сбрасывается до фиксированного лимита
|
||||
if settings.is_traffic_fixed():
|
||||
renewal_traffic_gb = settings.get_fixed_traffic_limit()
|
||||
else:
|
||||
renewal_traffic_gb = subscription.traffic_limit_gb
|
||||
traffic_price_per_month = settings.get_traffic_price(renewal_traffic_gb)
|
||||
traffic_discount_percent = db_user.get_promo_discount(
|
||||
"traffic",
|
||||
days,
|
||||
@@ -1731,6 +1741,17 @@ async def confirm_extend_subscription(
|
||||
subscription.status = SubscriptionStatus.ACTIVE.value
|
||||
subscription.updated_at = current_time
|
||||
|
||||
# В режиме fixed_with_topup при продлении сбрасываем трафик до фиксированного лимита
|
||||
traffic_was_reset = False
|
||||
old_traffic_limit = subscription.traffic_limit_gb
|
||||
if settings.is_traffic_fixed():
|
||||
fixed_limit = settings.get_fixed_traffic_limit()
|
||||
if subscription.traffic_limit_gb != fixed_limit or (subscription.purchased_traffic_gb or 0) > 0:
|
||||
traffic_was_reset = True
|
||||
subscription.traffic_limit_gb = fixed_limit
|
||||
subscription.purchased_traffic_gb = 0
|
||||
logger.info(f"🔄 Сброс трафика при продлении: {old_traffic_limit} ГБ → {fixed_limit} ГБ")
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
await db.refresh(db_user)
|
||||
@@ -1803,6 +1824,11 @@ async def confirm_extend_subscription(
|
||||
f"💰 Списано: {texts.format_price(price)}"
|
||||
)
|
||||
|
||||
# Добавляем уведомление о сбросе трафика
|
||||
if traffic_was_reset:
|
||||
fixed_limit = settings.get_fixed_traffic_limit()
|
||||
success_message += f"\n\n📊 Трафик сброшен до {fixed_limit} ГБ"
|
||||
|
||||
if promo_component["discount"] > 0:
|
||||
success_message += (
|
||||
f" (включая доп. скидку {promo_component['percent']}%:"
|
||||
|
||||
@@ -107,7 +107,7 @@ async def handle_add_traffic(
|
||||
)
|
||||
return
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
if settings.is_traffic_topup_blocked():
|
||||
await callback.answer(
|
||||
texts.t(
|
||||
"TRAFFIC_FIXED_MODE",
|
||||
@@ -206,7 +206,7 @@ async def handle_reset_traffic(
|
||||
):
|
||||
from app.config import settings
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
if settings.is_traffic_topup_blocked():
|
||||
await callback.answer("⚠️ В текущем режиме трафик фиксированный и не может быть сброшен", show_alert=True)
|
||||
return
|
||||
|
||||
@@ -266,7 +266,7 @@ async def confirm_reset_traffic(
|
||||
):
|
||||
from app.config import settings
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
if settings.is_traffic_topup_blocked():
|
||||
await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True)
|
||||
return
|
||||
|
||||
@@ -461,7 +461,7 @@ async def add_traffic(
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
if settings.is_traffic_fixed():
|
||||
if settings.is_traffic_topup_blocked():
|
||||
await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True)
|
||||
return
|
||||
|
||||
@@ -609,7 +609,7 @@ async def handle_switch_traffic(
|
||||
):
|
||||
from app.config import settings
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
if settings.is_traffic_topup_blocked():
|
||||
await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True)
|
||||
return
|
||||
|
||||
|
||||
@@ -511,6 +511,7 @@ def get_main_menu_keyboard(
|
||||
if (
|
||||
settings.BUY_TRAFFIC_BUTTON_VISIBLE
|
||||
and settings.is_traffic_topup_enabled()
|
||||
and not settings.is_traffic_topup_blocked()
|
||||
and subscription
|
||||
and not subscription.is_trial
|
||||
and (subscription.traffic_limit_gb or 0) > 0
|
||||
@@ -992,7 +993,20 @@ def get_subscription_keyboard(
|
||||
callback_data="subscription_settings",
|
||||
)
|
||||
])
|
||||
|
||||
# Кнопка докупки трафика для платных подписок
|
||||
if (
|
||||
settings.is_traffic_topup_enabled()
|
||||
and not settings.is_traffic_topup_blocked()
|
||||
and subscription
|
||||
and (subscription.traffic_limit_gb or 0) > 0
|
||||
):
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text=texts.t("BUY_TRAFFIC_BUTTON", "📈 Докупить трафик"),
|
||||
callback_data="buy_traffic"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
|
||||
])
|
||||
@@ -1134,10 +1148,10 @@ def get_subscription_period_keyboard(
|
||||
def get_traffic_packages_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from app.config import settings
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
|
||||
if settings.is_traffic_topup_blocked():
|
||||
return get_back_keyboard(language)
|
||||
|
||||
logger.info(f"🔍 RAW CONFIG: '{settings.TRAFFIC_PACKAGES_CONFIG}'")
|
||||
@@ -1863,8 +1877,8 @@ def get_reset_traffic_confirm_keyboard(
|
||||
missing_kopeks: int = 0,
|
||||
) -> InlineKeyboardMarkup:
|
||||
from app.config import settings
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
|
||||
if settings.is_traffic_topup_blocked():
|
||||
return get_back_keyboard(language)
|
||||
|
||||
texts = get_texts(language)
|
||||
|
||||
@@ -7,9 +7,7 @@ import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -35,12 +33,10 @@ class CloudPaymentsService:
|
||||
public_id: Optional[str] = None,
|
||||
api_secret: Optional[str] = None,
|
||||
api_url: Optional[str] = None,
|
||||
widget_url: Optional[str] = None,
|
||||
) -> None:
|
||||
self.public_id = public_id or settings.CLOUDPAYMENTS_PUBLIC_ID
|
||||
self.api_secret = api_secret or settings.CLOUDPAYMENTS_API_SECRET
|
||||
self.api_url = (api_url or settings.CLOUDPAYMENTS_API_URL).rstrip("/")
|
||||
self.widget_url = (widget_url or settings.CLOUDPAYMENTS_WIDGET_URL).rstrip("/")
|
||||
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
@@ -114,16 +110,18 @@ class CloudPaymentsService:
|
||||
"""Convert rubles to kopeks."""
|
||||
return int(amount * 100)
|
||||
|
||||
def generate_payment_link(
|
||||
async def generate_payment_link(
|
||||
self,
|
||||
telegram_id: int,
|
||||
amount_kopeks: int,
|
||||
invoice_id: str,
|
||||
description: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
success_redirect_url: Optional[str] = None,
|
||||
fail_redirect_url: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Generate a payment widget URL for CloudPayments.
|
||||
Create a payment order via CloudPayments API and return payment URL.
|
||||
|
||||
Args:
|
||||
telegram_id: User's Telegram ID (will be used as AccountId)
|
||||
@@ -131,35 +129,65 @@ class CloudPaymentsService:
|
||||
invoice_id: Unique invoice ID for this payment
|
||||
description: Payment description
|
||||
email: User's email (optional)
|
||||
success_redirect_url: Redirect URL after successful payment
|
||||
fail_redirect_url: Redirect URL after failed payment
|
||||
|
||||
Returns:
|
||||
URL to CloudPayments payment widget
|
||||
URL to CloudPayments payment page
|
||||
"""
|
||||
if not self.public_id:
|
||||
raise CloudPaymentsAPIError("CloudPayments public_id not configured")
|
||||
if not self.is_configured:
|
||||
raise CloudPaymentsAPIError("CloudPayments is not configured")
|
||||
|
||||
amount = self._amount_from_kopeks(amount_kopeks)
|
||||
|
||||
params = {
|
||||
"publicId": self.public_id,
|
||||
"description": description or settings.CLOUDPAYMENTS_DESCRIPTION,
|
||||
"amount": amount,
|
||||
"currency": settings.CLOUDPAYMENTS_CURRENCY,
|
||||
"accountId": str(telegram_id),
|
||||
"invoiceId": invoice_id,
|
||||
"skin": settings.CLOUDPAYMENTS_SKIN,
|
||||
# Формируем данные для создания заказа через API /orders/create
|
||||
payload: Dict[str, Any] = {
|
||||
"Amount": amount,
|
||||
"Currency": settings.CLOUDPAYMENTS_CURRENCY,
|
||||
"Description": description or settings.CLOUDPAYMENTS_DESCRIPTION,
|
||||
"AccountId": str(telegram_id),
|
||||
"InvoiceId": invoice_id,
|
||||
"JsonData": {
|
||||
"telegram_id": telegram_id,
|
||||
"invoice_id": invoice_id,
|
||||
},
|
||||
}
|
||||
|
||||
if settings.CLOUDPAYMENTS_REQUIRE_EMAIL:
|
||||
params["requireEmail"] = "true"
|
||||
|
||||
if email:
|
||||
params["email"] = email
|
||||
payload["Email"] = email
|
||||
|
||||
# Добавляем JSON данные для webhook
|
||||
params["data"] = f'{{"telegram_id": {telegram_id}, "invoice_id": "{invoice_id}"}}'
|
||||
if settings.CLOUDPAYMENTS_REQUIRE_EMAIL:
|
||||
payload["RequireConfirmation"] = False
|
||||
|
||||
return f"{self.widget_url}?{urlencode(params)}"
|
||||
# URL для редиректа после оплаты
|
||||
if success_redirect_url or settings.CLOUDPAYMENTS_RETURN_URL:
|
||||
payload["SuccessRedirectUrl"] = success_redirect_url or settings.CLOUDPAYMENTS_RETURN_URL
|
||||
|
||||
if fail_redirect_url:
|
||||
payload["FailRedirectUrl"] = fail_redirect_url
|
||||
|
||||
# Создаём заказ через API
|
||||
response = await self._request("POST", "/orders/create", json=payload)
|
||||
|
||||
if not response.get("Success"):
|
||||
error_message = response.get("Message", "Unknown error")
|
||||
logger.error("CloudPayments orders/create failed: %s", error_message)
|
||||
raise CloudPaymentsAPIError(f"Failed to create order: {error_message}")
|
||||
|
||||
model = response.get("Model", {})
|
||||
payment_url = model.get("Url")
|
||||
|
||||
if not payment_url:
|
||||
logger.error("CloudPayments orders/create returned no URL: %s", response)
|
||||
raise CloudPaymentsAPIError("CloudPayments API returned no payment URL")
|
||||
|
||||
logger.info(
|
||||
"CloudPayments order created: id=%s, url=%s",
|
||||
model.get("Id"),
|
||||
payment_url,
|
||||
)
|
||||
|
||||
return payment_url
|
||||
|
||||
def generate_invoice_id(self, telegram_id: int) -> str:
|
||||
"""Generate unique invoice ID for a payment."""
|
||||
|
||||
@@ -75,8 +75,8 @@ class CloudPaymentsPaymentMixin:
|
||||
invoice_id = self.cloudpayments_service.generate_invoice_id(telegram_id)
|
||||
|
||||
try:
|
||||
# Generate payment widget URL
|
||||
payment_url = self.cloudpayments_service.generate_payment_link(
|
||||
# Create payment order via CloudPayments API
|
||||
payment_url = await self.cloudpayments_service.generate_payment_link(
|
||||
telegram_id=telegram_id,
|
||||
amount_kopeks=amount_kopeks,
|
||||
invoice_id=invoice_id,
|
||||
|
||||
@@ -680,11 +680,19 @@ async def auto_activate_subscription_after_topup(
|
||||
# Определяем параметры подписки
|
||||
if subscription:
|
||||
device_limit = subscription.device_limit or settings.DEFAULT_DEVICE_LIMIT
|
||||
traffic_limit_gb = subscription.traffic_limit_gb or 0
|
||||
# В режиме fixed_with_topup при автоактивации используем фиксированный лимит
|
||||
if settings.is_traffic_fixed():
|
||||
traffic_limit_gb = settings.get_fixed_traffic_limit()
|
||||
else:
|
||||
traffic_limit_gb = subscription.traffic_limit_gb or 0
|
||||
connected_squads = subscription.connected_squads or []
|
||||
else:
|
||||
device_limit = settings.DEFAULT_DEVICE_LIMIT
|
||||
traffic_limit_gb = 0
|
||||
# В режиме fixed_with_topup при автоактивации используем фиксированный лимит
|
||||
if settings.is_traffic_fixed():
|
||||
traffic_limit_gb = settings.get_fixed_traffic_limit()
|
||||
else:
|
||||
traffic_limit_gb = 0
|
||||
connected_squads = []
|
||||
|
||||
# Если серверы не выбраны — берём бесплатные по умолчанию
|
||||
|
||||
@@ -511,9 +511,11 @@ class MiniAppSubscriptionPurchaseService:
|
||||
) -> PurchaseTrafficConfig:
|
||||
if settings.is_traffic_fixed():
|
||||
value = fixed_traffic_value if fixed_traffic_value is not None else settings.get_fixed_traffic_limit()
|
||||
# Передаём актуальный режим (fixed или fixed_with_topup)
|
||||
actual_mode = settings.TRAFFIC_SELECTION_MODE.lower()
|
||||
return PurchaseTrafficConfig(
|
||||
selectable=False,
|
||||
mode="fixed",
|
||||
mode=actual_mode,
|
||||
options=[],
|
||||
default_value=value,
|
||||
current_value=value,
|
||||
|
||||
@@ -319,9 +319,13 @@ class SubscriptionRenewalService:
|
||||
if connected_uuids:
|
||||
server_ids = await get_server_ids_by_uuids(db, connected_uuids)
|
||||
|
||||
traffic_limit = subscription.traffic_limit_gb
|
||||
if traffic_limit is None:
|
||||
traffic_limit = settings.DEFAULT_TRAFFIC_LIMIT_GB
|
||||
# В режиме fixed_with_topup при продлении используем фиксированный лимит
|
||||
if settings.is_traffic_fixed():
|
||||
traffic_limit = settings.get_fixed_traffic_limit()
|
||||
else:
|
||||
traffic_limit = subscription.traffic_limit_gb
|
||||
if traffic_limit is None:
|
||||
traffic_limit = settings.DEFAULT_TRAFFIC_LIMIT_GB
|
||||
|
||||
devices_limit = subscription.device_limit
|
||||
if devices_limit is None:
|
||||
|
||||
@@ -730,7 +730,12 @@ class SubscriptionService:
|
||||
devices_discount = devices_price * devices_discount_percent // 100
|
||||
discounted_devices_price = devices_price - devices_discount
|
||||
|
||||
traffic_price = settings.get_traffic_price(subscription.traffic_limit_gb)
|
||||
# В режиме fixed_with_topup при продлении используем фиксированный лимит
|
||||
if settings.is_traffic_fixed():
|
||||
renewal_traffic_gb = settings.get_fixed_traffic_limit()
|
||||
else:
|
||||
renewal_traffic_gb = subscription.traffic_limit_gb
|
||||
traffic_price = settings.get_traffic_price(renewal_traffic_gb)
|
||||
traffic_discount_percent = _resolve_discount_percent(
|
||||
user,
|
||||
promo_group,
|
||||
@@ -1072,7 +1077,12 @@ class SubscriptionService:
|
||||
discounted_devices_per_month = devices_price_per_month - devices_discount_per_month
|
||||
total_devices_price = discounted_devices_per_month * months_in_period
|
||||
|
||||
traffic_price_per_month = settings.get_traffic_price(subscription.traffic_limit_gb)
|
||||
# В режиме fixed_with_topup при продлении используем фиксированный лимит
|
||||
if settings.is_traffic_fixed():
|
||||
renewal_traffic_gb = settings.get_fixed_traffic_limit()
|
||||
else:
|
||||
renewal_traffic_gb = subscription.traffic_limit_gb
|
||||
traffic_price_per_month = settings.get_traffic_price(renewal_traffic_gb)
|
||||
traffic_discount_percent = _resolve_discount_percent(
|
||||
user,
|
||||
promo_group,
|
||||
|
||||
@@ -355,6 +355,7 @@ class BotConfigurationService:
|
||||
"TRAFFIC_SELECTION_MODE": [
|
||||
ChoiceOption("selectable", "📦 Выбор пакетов"),
|
||||
ChoiceOption("fixed", "📏 Фиксированный лимит"),
|
||||
ChoiceOption("fixed_with_topup", "📏 Фикс. лимит + докупка"),
|
||||
],
|
||||
"DEFAULT_TRAFFIC_RESET_STRATEGY": [
|
||||
ChoiceOption("NO_RESET", "♾️ Без сброса"),
|
||||
|
||||
@@ -4690,7 +4690,8 @@ async def _build_subscription_settings(
|
||||
)
|
||||
|
||||
traffic_options: List[MiniAppSubscriptionTrafficOption] = []
|
||||
if settings.is_traffic_selectable():
|
||||
# В режиме fixed_with_topup показываем опции трафика (для докупки)
|
||||
if not settings.is_traffic_topup_blocked():
|
||||
for package in settings.get_traffic_packages():
|
||||
is_enabled = bool(package.get("enabled", True))
|
||||
if package.get("is_active") is False:
|
||||
@@ -4764,7 +4765,7 @@ async def _build_subscription_settings(
|
||||
),
|
||||
traffic=MiniAppSubscriptionTrafficSettings(
|
||||
options=traffic_options,
|
||||
can_update=settings.is_traffic_selectable(),
|
||||
can_update=not settings.is_traffic_topup_blocked(),
|
||||
current_value=subscription.traffic_limit_gb,
|
||||
),
|
||||
devices=MiniAppSubscriptionDevicesSettings(
|
||||
@@ -5512,7 +5513,9 @@ async def update_subscription_traffic_endpoint(
|
||||
if new_traffic == subscription.traffic_limit_gb:
|
||||
return MiniAppSubscriptionUpdateResponse(success=True, message="No changes")
|
||||
|
||||
if not settings.is_traffic_selectable():
|
||||
# В режиме fixed полностью блокируем изменение трафика
|
||||
# В режиме fixed_with_topup разрешаем докупку (is_traffic_topup_blocked = False)
|
||||
if settings.is_traffic_topup_blocked():
|
||||
raise HTTPException(
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
detail={
|
||||
|
||||
@@ -16166,7 +16166,8 @@
|
||||
}
|
||||
|
||||
function ensurePurchaseTrafficSelection(config) {
|
||||
const selectable = config && config.selectable !== false && String(config.mode || '').toLowerCase() !== 'fixed';
|
||||
const mode = String(config?.mode || '').toLowerCase();
|
||||
const selectable = config && config.selectable !== false && !['fixed', 'fixed_with_topup'].includes(mode);
|
||||
const options = ensureArray(config?.options || config?.available || []);
|
||||
if (!selectable || !options.length) {
|
||||
if (config && (config.current !== undefined || config.default !== undefined)) {
|
||||
@@ -16937,7 +16938,8 @@
|
||||
}
|
||||
|
||||
const config = getSubscriptionPurchaseTrafficConfig(period);
|
||||
const selectable = config && config.selectable !== false && String(config.mode || '').toLowerCase() !== 'fixed';
|
||||
const mode = String(config?.mode || '').toLowerCase();
|
||||
const selectable = config && config.selectable !== false && !['fixed', 'fixed_with_topup'].includes(mode);
|
||||
const options = ensureArray(config?.options || config?.available || []);
|
||||
|
||||
optionsContainer.innerHTML = '';
|
||||
|
||||
Reference in New Issue
Block a user