Add promo group add-on discount toggle

This commit is contained in:
Egor
2025-09-25 13:25:58 +03:00
parent 0535353124
commit 6483efc099
9 changed files with 290 additions and 8 deletions
+7 -1
View File
@@ -60,6 +60,7 @@ async def create_promo_group(
device_discount_percent: int,
period_discounts: Optional[Dict[int, int]] = None,
auto_assign_total_spent_kopeks: Optional[int] = None,
apply_addon_discounts: bool = True,
) -> PromoGroup:
normalized_period_discounts = _normalize_period_discounts(period_discounts)
@@ -76,6 +77,7 @@ async def create_promo_group(
device_discount_percent=max(0, min(100, device_discount_percent)),
period_discounts=normalized_period_discounts or None,
auto_assign_total_spent_kopeks=auto_assign_total_spent_kopeks,
apply_addon_discounts=bool(apply_addon_discounts),
is_default=False,
)
@@ -84,12 +86,13 @@ async def create_promo_group(
await db.refresh(promo_group)
logger.info(
"Создана промогруппа '%s' с скидками (servers=%s%%, traffic=%s%%, devices=%s%%, periods=%s) и порогом автоприсвоения %s",
"Создана промогруппа '%s' с скидками (servers=%s%%, traffic=%s%%, devices=%s%%, periods=%s, addons=%s) и порогом автоприсвоения %s",
promo_group.name,
promo_group.server_discount_percent,
promo_group.traffic_discount_percent,
promo_group.device_discount_percent,
normalized_period_discounts,
promo_group.apply_addon_discounts,
(auto_assign_total_spent_kopeks or 0) / 100,
)
@@ -106,6 +109,7 @@ async def update_promo_group(
device_discount_percent: Optional[int] = None,
period_discounts: Optional[Dict[int, int]] = None,
auto_assign_total_spent_kopeks: Optional[int] = None,
apply_addon_discounts: Optional[bool] = None,
) -> PromoGroup:
if name is not None:
group.name = name.strip()
@@ -120,6 +124,8 @@ async def update_promo_group(
group.period_discounts = normalized_period_discounts or None
if auto_assign_total_spent_kopeks is not None:
group.auto_assign_total_spent_kopeks = max(0, auto_assign_total_spent_kopeks)
if apply_addon_discounts is not None:
group.apply_addon_discounts = bool(apply_addon_discounts)
await db.commit()
await db.refresh(group)
+14 -2
View File
@@ -504,15 +504,24 @@ def _get_discount_percent(
category: str,
*,
period_days: Optional[int] = None,
is_addon_purchase: bool = False,
) -> int:
if user is not None:
try:
return user.get_promo_discount(category, period_days)
return user.get_promo_discount(
category,
period_days,
is_addon_purchase=is_addon_purchase,
)
except AttributeError:
pass
if promo_group is not None:
return promo_group.get_discount_percent(category, period_days)
return promo_group.get_discount_percent(
category,
period_days,
is_addon_purchase=is_addon_purchase,
)
return 0
@@ -852,6 +861,7 @@ async def calculate_addon_cost_for_remaining_period(
promo_group,
"traffic",
period_days=period_hint_days,
is_addon_purchase=True,
)
traffic_discount_per_month = traffic_price_per_month * traffic_discount_percent // 100
discounted_traffic_per_month = traffic_price_per_month - traffic_discount_per_month
@@ -873,6 +883,7 @@ async def calculate_addon_cost_for_remaining_period(
promo_group,
"devices",
period_days=period_hint_days,
is_addon_purchase=True,
)
devices_discount_per_month = devices_price_per_month * devices_discount_percent // 100
discounted_devices_per_month = devices_price_per_month - devices_discount_per_month
@@ -902,6 +913,7 @@ async def calculate_addon_cost_for_remaining_period(
promo_group,
"servers",
period_days=period_hint_days,
is_addon_purchase=True,
)
server_discount_per_month = server_price_per_month * servers_discount_percent // 100
discounted_server_per_month = server_price_per_month - server_discount_per_month
+23 -3
View File
@@ -292,6 +292,7 @@ class PromoGroup(Base):
device_discount_percent = Column(Integer, nullable=False, default=0)
period_discounts = Column(JSON, nullable=True, default=dict)
auto_assign_total_spent_kopeks = Column(Integer, nullable=True, default=None)
apply_addon_discounts = Column(Boolean, nullable=False, default=True)
is_default = Column(Boolean, nullable=False, default=False)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
@@ -346,7 +347,16 @@ class PromoGroup(Base):
return 0
def get_discount_percent(self, category: str, period_days: Optional[int] = None) -> int:
def get_discount_percent(
self,
category: str,
period_days: Optional[int] = None,
*,
is_addon_purchase: bool = False,
) -> int:
if is_addon_purchase and not self.apply_addon_discounts:
return 0
if category == "period":
return max(0, min(100, self._get_period_discount(period_days)))
@@ -404,10 +414,20 @@ class User(Base):
parts = [self.first_name, self.last_name]
return " ".join(filter(None, parts)) or self.username or f"ID{self.telegram_id}"
def get_promo_discount(self, category: str, period_days: Optional[int] = None) -> int:
def get_promo_discount(
self,
category: str,
period_days: Optional[int] = None,
*,
is_addon_purchase: bool = False,
) -> int:
if not self.promo_group:
return 0
return self.promo_group.get_discount_percent(category, period_days)
return self.promo_group.get_discount_percent(
category,
period_days,
is_addon_purchase=is_addon_purchase,
)
def add_balance(self, kopeks: int) -> None:
self.balance_kopeks += kopeks
+44
View File
@@ -931,6 +931,47 @@ async def ensure_promo_groups_setup():
"Добавлена колонка promo_groups.auto_assign_total_spent_kopeks"
)
addon_discounts_column_exists = await check_column_exists(
"promo_groups", "apply_addon_discounts"
)
if not addon_discounts_column_exists:
if db_type == "sqlite":
await conn.execute(
text(
"ALTER TABLE promo_groups ADD COLUMN apply_addon_discounts BOOLEAN DEFAULT 1"
)
)
await conn.execute(
text(
"UPDATE promo_groups SET apply_addon_discounts = 1 WHERE apply_addon_discounts IS NULL"
)
)
elif db_type == "postgresql":
await conn.execute(
text(
"ALTER TABLE promo_groups ADD COLUMN apply_addon_discounts BOOLEAN NOT NULL DEFAULT TRUE"
)
)
elif db_type == "mysql":
await conn.execute(
text(
"ALTER TABLE promo_groups ADD COLUMN apply_addon_discounts BOOLEAN DEFAULT 1"
)
)
await conn.execute(
text(
"UPDATE promo_groups SET apply_addon_discounts = 1 WHERE apply_addon_discounts IS NULL"
)
)
else:
logger.error(
f"Неподдерживаемый тип БД для promo_groups.apply_addon_discounts: {db_type}"
)
return False
logger.info("Добавлена колонка promo_groups.apply_addon_discounts")
column_exists = await check_column_exists("users", "promo_group_id")
if not column_exists:
@@ -1994,6 +2035,7 @@ async def check_migration_status():
"users_promo_group_column": False,
"promo_groups_period_discounts_column": False,
"promo_groups_auto_assign_column": False,
"promo_groups_addon_discount_column": False,
"users_auto_promo_group_assigned_column": False,
"subscription_crypto_link_column": False,
}
@@ -2011,6 +2053,7 @@ async def check_migration_status():
status["users_promo_group_column"] = await check_column_exists('users', 'promo_group_id')
status["promo_groups_period_discounts_column"] = await check_column_exists('promo_groups', 'period_discounts')
status["promo_groups_auto_assign_column"] = await check_column_exists('promo_groups', 'auto_assign_total_spent_kopeks')
status["promo_groups_addon_discount_column"] = await check_column_exists('promo_groups', 'apply_addon_discounts')
status["users_auto_promo_group_assigned_column"] = await check_column_exists('users', 'auto_promo_group_assigned')
status["subscription_crypto_link_column"] = await check_column_exists('subscriptions', 'subscription_crypto_link')
@@ -2048,6 +2091,7 @@ async def check_migration_status():
"users_promo_group_column": "Колонка promo_group_id у пользователей",
"promo_groups_period_discounts_column": "Колонка period_discounts у промо-групп",
"promo_groups_auto_assign_column": "Колонка auto_assign_total_spent_kopeks у промо-групп",
"promo_groups_addon_discount_column": "Колонка apply_addon_discounts у промо-групп",
"users_auto_promo_group_assigned_column": "Флаг автоназначения промогруппы у пользователей",
"subscription_crypto_link_column": "Колонка subscription_crypto_link в subscriptions",
}
+170
View File
@@ -39,6 +39,32 @@ def _format_discount_line(texts, group) -> str:
)
def _format_addon_discount_line(texts, group) -> str:
if getattr(group, "apply_addon_discounts", True):
return texts.t(
"ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_ENABLED",
"Скидки на доп. услуги: включены",
)
return texts.t(
"ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_DISABLED",
"Скидки на доп. услуги: отключены",
)
def _format_addon_discount_short(texts, enabled: bool) -> str:
if enabled:
return texts.t(
"ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_ENABLED_SHORT",
"включены",
)
return texts.t(
"ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_DISABLED_SHORT",
"отключены",
)
def _normalize_periods_dict(raw: Optional[Dict]) -> Dict[int, int]:
if not raw or not isinstance(raw, dict):
return {}
@@ -161,6 +187,42 @@ async def _prompt_for_period_discounts(
await message.answer(prompt_text)
def _parse_boolean_choice(value: str) -> bool:
cleaned = (value or "").strip().lower()
truthy = {"1", "true", "yes", "y", "да", "on", "+"}
falsy = {"0", "false", "no", "n", "нет", "off", "-"}
if cleaned in truthy:
return True
if cleaned in falsy:
return False
raise ValueError
async def _prompt_for_addon_discount_choice(
message: types.Message,
state: FSMContext,
prompt_key: str,
default_text: str,
*,
current_value: Optional[bool] = None,
):
data = await state.get_data()
texts = get_texts(data.get("language", "ru"))
prompt_text = texts.t(prompt_key, default_text)
if current_value is not None:
current_display = _format_addon_discount_short(texts, current_value)
try:
prompt_text = prompt_text.format(current=current_display)
except KeyError:
pass
await message.answer(prompt_text)
def _format_rubles(amount_kopeks: int) -> str:
if amount_kopeks <= 0:
return "0"
@@ -257,6 +319,7 @@ def _build_edit_menu_content(
lines = [
header,
_format_discount_line(texts, group),
_format_addon_discount_line(texts, group),
_format_auto_assign_line(texts, group),
]
@@ -309,6 +372,15 @@ def _build_edit_menu_content(
callback_data=f"promo_group_edit_field_{group.id}_devices",
)
],
[
types.InlineKeyboardButton(
text=texts.t(
"ADMIN_PROMO_GROUP_EDIT_FIELD_ADDON_DISCOUNTS",
"💼 Скидки на доп. услуги",
),
callback_data=f"promo_group_edit_field_{group.id}_addon",
)
],
[
types.InlineKeyboardButton(
text=texts.t(
@@ -399,6 +471,7 @@ async def show_promo_groups_menu(
group_lines = [
f"{'' if group.is_default else '🎯'} <b>{group.name}</b>{default_suffix}",
_format_discount_line(texts, group),
_format_addon_discount_line(texts, group),
_format_auto_assign_line(texts, group),
texts.t(
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT",
@@ -474,6 +547,7 @@ async def show_promo_group_details(
"💳 <b>Промогруппа:</b> {name}",
).format(name=group.name),
_format_discount_line(texts, group),
_format_addon_discount_line(texts, group),
_format_auto_assign_line(texts, group),
texts.t(
"ADMIN_PROMO_GROUP_DETAILS_MEMBERS",
@@ -675,6 +749,39 @@ async def process_create_group_period_discounts(
return
await state.update_data(new_group_period_discounts=period_discounts)
await state.set_state(AdminStates.creating_promo_group_addon_discount)
await _prompt_for_addon_discount_choice(
message,
state,
"ADMIN_PROMO_GROUP_CREATE_ADDON_PROMPT",
"Включить скидки на докупку доп. услуг? (да/нет)",
)
@admin_required
@error_handler
async def process_create_group_addon_discount(
message: types.Message,
state: FSMContext,
db_user,
db: AsyncSession,
):
data = await state.get_data()
texts = get_texts(data.get("language", db_user.language))
try:
apply_addon_discounts = _parse_boolean_choice(message.text)
except ValueError:
await message.answer(
texts.t(
"ADMIN_PROMO_GROUP_INVALID_BOOLEAN",
"Введите «да» или «нет».",
)
)
return
await state.update_data(new_group_apply_addon_discounts=apply_addon_discounts)
await state.set_state(AdminStates.creating_promo_group_auto_assign)
await _prompt_for_auto_assign_threshold(
@@ -716,6 +823,7 @@ async def process_create_group_auto_assign(
device_discount_percent=data["new_group_devices"],
period_discounts=data.get("new_group_period_discounts"),
auto_assign_total_spent_kopeks=auto_assign_kopeks,
apply_addon_discounts=data.get("new_group_apply_addon_discounts", True),
)
except Exception as e:
logger.error(f"Не удалось создать промогруппу: {e}")
@@ -819,6 +927,16 @@ async def prompt_edit_promo_group_field(
"ADMIN_PROMO_GROUP_EDIT_DEVICES_PROMPT",
"Введите новую скидку на устройства (текущее значение: {current}%):",
).format(current=group.device_discount_percent)
elif field == "addon":
await state.set_state(AdminStates.editing_promo_group_addon_discount)
prompt = texts.t(
"ADMIN_PROMO_GROUP_EDIT_ADDON_PROMPT",
"Включить скидки на докупку доп. услуг? Сейчас: {current}. (да/нет)",
).format(
current=_format_addon_discount_short(
texts, getattr(group, "apply_addon_discounts", True)
)
)
elif field == "periods":
await state.set_state(AdminStates.editing_promo_group_period_discount)
current_discounts = _normalize_periods_dict(getattr(group, "period_discounts", None))
@@ -979,6 +1097,50 @@ async def process_edit_group_devices(
)
@admin_required
@error_handler
async def process_edit_group_addon_discount(
message: types.Message,
state: FSMContext,
db_user,
db: AsyncSession,
):
data = await state.get_data()
texts = get_texts(data.get("language", db_user.language))
try:
apply_addon_discounts = _parse_boolean_choice(message.text)
except ValueError:
await message.answer(
texts.t(
"ADMIN_PROMO_GROUP_INVALID_BOOLEAN",
"Введите «да» или «нет».",
)
)
return
group = await get_promo_group_by_id(db, data.get("edit_group_id"))
if not group:
await message.answer("❌ Промогруппа не найдена")
await state.clear()
return
group = await update_promo_group(
db,
group,
apply_addon_discounts=apply_addon_discounts,
)
await state.set_state(AdminStates.editing_promo_group_menu)
await _send_edit_menu_after_update(
message,
texts,
group,
data.get("language", db_user.language),
texts.t("ADMIN_PROMO_GROUP_UPDATED", "Промогруппа «{name}» обновлена.").format(name=group.name),
)
@admin_required
@error_handler
async def process_edit_group_period_discounts(
@@ -1235,6 +1397,10 @@ def register_handlers(dp: Dispatcher):
process_create_group_period_discounts,
AdminStates.creating_promo_group_period_discount,
)
dp.message.register(
process_create_group_addon_discount,
AdminStates.creating_promo_group_addon_discount,
)
dp.message.register(
process_create_group_auto_assign,
AdminStates.creating_promo_group_auto_assign,
@@ -1253,6 +1419,10 @@ def register_handlers(dp: Dispatcher):
process_edit_group_devices,
AdminStates.editing_promo_group_device_discount,
)
dp.message.register(
process_edit_group_addon_discount,
AdminStates.editing_promo_group_addon_discount,
)
dp.message.register(
process_edit_group_period_discounts,
AdminStates.editing_promo_group_period_discount,
+14 -2
View File
@@ -26,15 +26,24 @@ def _resolve_discount_percent(
category: str,
*,
period_days: Optional[int] = None,
is_addon_purchase: bool = False,
) -> int:
if user is not None:
try:
return user.get_promo_discount(category, period_days)
return user.get_promo_discount(
category,
period_days,
is_addon_purchase=is_addon_purchase,
)
except AttributeError:
pass
if promo_group is not None:
return promo_group.get_discount_percent(category, period_days)
return promo_group.get_discount_percent(
category,
period_days,
is_addon_purchase=is_addon_purchase,
)
return 0
@@ -863,6 +872,7 @@ class SubscriptionService:
promo_group,
"traffic",
period_days=period_hint_days,
is_addon_purchase=True,
)
traffic_discount_per_month = traffic_price_per_month * traffic_discount_percent // 100
discounted_traffic_per_month = traffic_price_per_month - traffic_discount_per_month
@@ -886,6 +896,7 @@ class SubscriptionService:
promo_group,
"devices",
period_days=period_hint_days,
is_addon_purchase=True,
)
devices_discount_per_month = devices_price_per_month * devices_discount_percent // 100
discounted_devices_per_month = devices_price_per_month - devices_discount_per_month
@@ -913,6 +924,7 @@ class SubscriptionService:
promo_group,
"servers",
period_days=period_hint_days,
is_addon_purchase=True,
)
server_discount_per_month = (
server_price_per_month * servers_discount_percent // 100
+2
View File
@@ -69,6 +69,7 @@ class AdminStates(StatesGroup):
creating_promo_group_server_discount = State()
creating_promo_group_device_discount = State()
creating_promo_group_period_discount = State()
creating_promo_group_addon_discount = State()
creating_promo_group_auto_assign = State()
editing_promo_group_menu = State()
@@ -76,6 +77,7 @@ class AdminStates(StatesGroup):
editing_promo_group_traffic_discount = State()
editing_promo_group_server_discount = State()
editing_promo_group_device_discount = State()
editing_promo_group_addon_discount = State()
editing_promo_group_period_discount = State()
editing_promo_group_auto_assign = State()
+8
View File
@@ -151,6 +151,10 @@
"ADMIN_PROMO_GROUPS_TITLE": "💳 <b>Promo groups</b>",
"ADMIN_PROMO_GROUPS_SUMMARY": "Groups total: {count}\nMembers total: {members}",
"ADMIN_PROMO_GROUPS_DISCOUNTS": "Discounts — servers: {servers}%, traffic: {traffic}%, devices: {devices}%",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_ENABLED": "Add-on discounts: enabled",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_DISABLED": "Add-on discounts: disabled",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_ENABLED_SHORT": "enabled",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_DISABLED_SHORT": "disabled",
"ADMIN_PROMO_GROUP_PERIOD_DISCOUNTS_HEADER": "⏳ Period discounts:",
"ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (default)",
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT": "Members: {count}",
@@ -244,7 +248,9 @@
"ADMIN_PROMO_GROUP_CREATE_SERVERS_PROMPT": "Enter server discount (0-100):",
"ADMIN_PROMO_GROUP_CREATE_DEVICES_PROMPT": "Enter device discount (0-100):",
"ADMIN_PROMO_GROUP_CREATE_PERIOD_PROMPT": "Enter subscription period discounts (e.g. 30:10, 90:15). Send 0 if none.",
"ADMIN_PROMO_GROUP_CREATE_ADDON_PROMPT": "Enable add-on discounts? (yes/no)",
"ADMIN_PROMO_GROUP_INVALID_PERCENT": "Enter a number from 0 to 100.",
"ADMIN_PROMO_GROUP_INVALID_BOOLEAN": "Enter “yes” or “no”.",
"ADMIN_PROMO_GROUP_INVALID_PERIOD_DISCOUNTS": "Enter period:discount pairs separated by commas, e.g. 30:10, 90:15, or 0.",
"ADMIN_PROMO_GROUP_CREATED": "Promo group “{name}” created.",
"ADMIN_PROMO_GROUP_CREATED_BACK_BUTTON": "↩️ Back to promo groups",
@@ -252,6 +258,7 @@
"ADMIN_PROMO_GROUP_EDIT_TRAFFIC_PROMPT": "Enter new traffic discount (0-100). Current value: {current}.",
"ADMIN_PROMO_GROUP_EDIT_SERVERS_PROMPT": "Enter new server discount (0-100). Current value: {current}.",
"ADMIN_PROMO_GROUP_EDIT_DEVICES_PROMPT": "Enter new device discount (0-100). Current value: {current}.",
"ADMIN_PROMO_GROUP_EDIT_ADDON_PROMPT": "Enable add-on discounts? Current: {current}. (yes/no)",
"ADMIN_PROMO_GROUP_EDIT_PERIOD_PROMPT": "Enter new period discounts (current: {current}). Send 0 if none.",
"ADMIN_PROMO_GROUP_UPDATED": "Promo group “{name}” updated.",
"ADMIN_PROMO_GROUP_AUTO_ASSIGN_DISABLED": "Auto assignment by total spending: disabled",
@@ -262,6 +269,7 @@
"ADMIN_PROMO_GROUP_EDIT_FIELD_TRAFFIC": "🌐 Traffic discount",
"ADMIN_PROMO_GROUP_EDIT_FIELD_SERVERS": "🖥 Server discount",
"ADMIN_PROMO_GROUP_EDIT_FIELD_DEVICES": "📱 Device discount",
"ADMIN_PROMO_GROUP_EDIT_FIELD_ADDON_DISCOUNTS": "💼 Add-on discounts",
"ADMIN_PROMO_GROUP_EDIT_FIELD_PERIODS": "⏳ Period discounts",
"ADMIN_PROMO_GROUP_EDIT_FIELD_AUTO_ASSIGN": "🤖 Auto assignment by spending",
"ADMIN_PROMO_GROUP_CREATE_AUTO_ASSIGN_PROMPT": "Enter total spending (in ₽) required for automatic assignment. Send 0 to disable.",
+8
View File
@@ -17,6 +17,10 @@
"ADMIN_PROMO_GROUPS_TITLE": "💳 <b>Промогруппы</b>",
"ADMIN_PROMO_GROUPS_SUMMARY": "Всего групп: {count}\nВсего участников: {members}",
"ADMIN_PROMO_GROUPS_DISCOUNTS": "Скидки — серверы: {servers}%, трафик: {traffic}%, устройства: {devices}%",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_ENABLED": "Скидки на доп. услуги: включены",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_DISABLED": "Скидки на доп. услуги: отключены",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_ENABLED_SHORT": "включены",
"ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_DISABLED_SHORT": "отключены",
"ADMIN_PROMO_GROUP_PERIOD_DISCOUNTS_HEADER": "⏳ Скидки по периодам:",
"ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (базовая)",
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT": "Участников: {count}",
@@ -110,7 +114,9 @@
"ADMIN_PROMO_GROUP_CREATE_SERVERS_PROMPT": "Введите скидку на серверы (0-100):",
"ADMIN_PROMO_GROUP_CREATE_DEVICES_PROMPT": "Введите скидку на устройства (0-100):",
"ADMIN_PROMO_GROUP_CREATE_PERIOD_PROMPT": "Введите скидки на периоды подписки (например, 30:10, 90:15). Отправьте 0, если без скидок.",
"ADMIN_PROMO_GROUP_CREATE_ADDON_PROMPT": "Включить скидки на докупку доп. услуг? (да/нет)",
"ADMIN_PROMO_GROUP_INVALID_PERCENT": "Введите число от 0 до 100.",
"ADMIN_PROMO_GROUP_INVALID_BOOLEAN": "Введите «да» или «нет».",
"ADMIN_PROMO_GROUP_INVALID_PERIOD_DISCOUNTS": "Введите пары период:скидка через запятую, например 30:10, 90:15, или 0.",
"ADMIN_PROMO_GROUP_CREATED": "Промогруппа «{name}» создана.",
"ADMIN_PROMO_GROUP_CREATED_BACK_BUTTON": "↩️ К промогруппам",
@@ -118,6 +124,7 @@
"ADMIN_PROMO_GROUP_EDIT_TRAFFIC_PROMPT": "Введите новую скидку на трафик (0-100). Текущее значение: {current}.",
"ADMIN_PROMO_GROUP_EDIT_SERVERS_PROMPT": "Введите новую скидку на серверы (0-100). Текущее значение: {current}.",
"ADMIN_PROMO_GROUP_EDIT_DEVICES_PROMPT": "Введите новую скидку на устройства (0-100). Текущее значение: {current}.",
"ADMIN_PROMO_GROUP_EDIT_ADDON_PROMPT": "Включить скидки на докупку доп. услуг? Сейчас: {current}. (да/нет)",
"ADMIN_PROMO_GROUP_EDIT_PERIOD_PROMPT": "Введите новые скидки на периоды (текущие: {current}). Отправьте 0, если без скидок.",
"ADMIN_PROMO_GROUP_UPDATED": "Промогруппа «{name}» обновлена.",
"ADMIN_PROMO_GROUP_AUTO_ASSIGN_DISABLED": "Автовыдача по суммарным тратам: отключена",
@@ -128,6 +135,7 @@
"ADMIN_PROMO_GROUP_EDIT_FIELD_TRAFFIC": "🌐 Скидка на трафик",
"ADMIN_PROMO_GROUP_EDIT_FIELD_SERVERS": "🖥 Скидка на серверы",
"ADMIN_PROMO_GROUP_EDIT_FIELD_DEVICES": "📱 Скидка на устройства",
"ADMIN_PROMO_GROUP_EDIT_FIELD_ADDON_DISCOUNTS": "💼 Скидки на доп. услуги",
"ADMIN_PROMO_GROUP_EDIT_FIELD_PERIODS": "⏳ Скидки по периодам",
"ADMIN_PROMO_GROUP_EDIT_FIELD_AUTO_ASSIGN": "🤖 Автовыдача по тратам",
"ADMIN_PROMO_GROUP_CREATE_AUTO_ASSIGN_PROMPT": "Введите сумму общих трат (в ₽) для автоматической выдачи этой группы. Отправьте 0, чтобы отключить.",