mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
feat: add period-based discounts for promo groups
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -7,6 +7,24 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.database.models import PromoGroup, User
|
||||
|
||||
|
||||
def _normalize_period_discounts(period_discounts: Optional[Dict[int, int]]) -> Dict[int, int]:
|
||||
if not period_discounts:
|
||||
return {}
|
||||
|
||||
normalized: Dict[int, int] = {}
|
||||
|
||||
for key, value in period_discounts.items():
|
||||
try:
|
||||
period = int(key)
|
||||
percent = int(value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
normalized[period] = max(0, min(100, percent))
|
||||
|
||||
return normalized
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -40,12 +58,16 @@ async def create_promo_group(
|
||||
server_discount_percent: int,
|
||||
traffic_discount_percent: int,
|
||||
device_discount_percent: int,
|
||||
period_discounts: Optional[Dict[int, int]] = None,
|
||||
) -> PromoGroup:
|
||||
normalized_period_discounts = _normalize_period_discounts(period_discounts)
|
||||
|
||||
promo_group = PromoGroup(
|
||||
name=name.strip(),
|
||||
server_discount_percent=max(0, min(100, server_discount_percent)),
|
||||
traffic_discount_percent=max(0, min(100, traffic_discount_percent)),
|
||||
device_discount_percent=max(0, min(100, device_discount_percent)),
|
||||
period_discounts=normalized_period_discounts or None,
|
||||
is_default=False,
|
||||
)
|
||||
|
||||
@@ -54,11 +76,12 @@ async def create_promo_group(
|
||||
await db.refresh(promo_group)
|
||||
|
||||
logger.info(
|
||||
"Создана промогруппа '%s' с скидками (servers=%s%%, traffic=%s%%, devices=%s%%)",
|
||||
"Создана промогруппа '%s' с скидками (servers=%s%%, traffic=%s%%, devices=%s%%, periods=%s)",
|
||||
promo_group.name,
|
||||
promo_group.server_discount_percent,
|
||||
promo_group.traffic_discount_percent,
|
||||
promo_group.device_discount_percent,
|
||||
normalized_period_discounts,
|
||||
)
|
||||
|
||||
return promo_group
|
||||
@@ -72,6 +95,7 @@ async def update_promo_group(
|
||||
server_discount_percent: Optional[int] = None,
|
||||
traffic_discount_percent: Optional[int] = None,
|
||||
device_discount_percent: Optional[int] = None,
|
||||
period_discounts: Optional[Dict[int, int]] = None,
|
||||
) -> PromoGroup:
|
||||
if name is not None:
|
||||
group.name = name.strip()
|
||||
@@ -81,6 +105,9 @@ async def update_promo_group(
|
||||
group.traffic_discount_percent = max(0, min(100, traffic_discount_percent))
|
||||
if device_discount_percent is not None:
|
||||
group.device_discount_percent = max(0, min(100, device_discount_percent))
|
||||
if period_discounts is not None:
|
||||
normalized_period_discounts = _normalize_period_discounts(period_discounts)
|
||||
group.period_discounts = normalized_period_discounts or None
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(group)
|
||||
|
||||
@@ -531,7 +531,15 @@ async def calculate_subscription_total_cost(
|
||||
|
||||
months_in_period = calculate_months_from_days(period_days)
|
||||
|
||||
base_price = PERIOD_PRICES.get(period_days, 0)
|
||||
base_price_original = PERIOD_PRICES.get(period_days, 0)
|
||||
period_discount_percent = _get_discount_percent(
|
||||
user,
|
||||
promo_group,
|
||||
"period",
|
||||
period_days=period_days,
|
||||
)
|
||||
base_discount_total = base_price_original * period_discount_percent // 100
|
||||
base_price = base_price_original - base_discount_total
|
||||
|
||||
promo_group = promo_group or (user.promo_group if user else None)
|
||||
|
||||
@@ -577,6 +585,9 @@ async def calculate_subscription_total_cost(
|
||||
|
||||
details = {
|
||||
'base_price': base_price,
|
||||
'base_price_original': base_price_original,
|
||||
'base_discount_percent': period_discount_percent,
|
||||
'base_discount_total': base_discount_total,
|
||||
'traffic_price_per_month': traffic_price_per_month,
|
||||
'traffic_discount_percent': traffic_discount_percent,
|
||||
'traffic_discount_total': total_traffic_discount,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Dict
|
||||
from enum import Enum
|
||||
|
||||
from sqlalchemy import (
|
||||
@@ -270,13 +270,59 @@ class PromoGroup(Base):
|
||||
server_discount_percent = Column(Integer, nullable=False, default=0)
|
||||
traffic_discount_percent = Column(Integer, nullable=False, default=0)
|
||||
device_discount_percent = Column(Integer, nullable=False, default=0)
|
||||
period_discounts = Column(JSON, nullable=True, default=dict)
|
||||
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())
|
||||
|
||||
users = relationship("User", back_populates="promo_group")
|
||||
|
||||
def _get_period_discounts_map(self) -> Dict[int, int]:
|
||||
raw_discounts = self.period_discounts or {}
|
||||
|
||||
if isinstance(raw_discounts, dict):
|
||||
items = raw_discounts.items()
|
||||
else:
|
||||
items = []
|
||||
|
||||
normalized: Dict[int, int] = {}
|
||||
|
||||
for key, value in items:
|
||||
try:
|
||||
period = int(key)
|
||||
percent = int(value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
normalized[period] = max(0, min(100, percent))
|
||||
|
||||
return normalized
|
||||
|
||||
def _get_period_discount(self, period_days: Optional[int]) -> int:
|
||||
if not period_days:
|
||||
return 0
|
||||
|
||||
discounts = self._get_period_discounts_map()
|
||||
|
||||
if period_days in discounts:
|
||||
return discounts[period_days]
|
||||
|
||||
if self.is_default:
|
||||
try:
|
||||
from app.config import settings
|
||||
|
||||
if settings.is_base_promo_group_period_discount_enabled():
|
||||
config_discounts = settings.get_base_promo_group_period_discounts()
|
||||
return config_discounts.get(period_days, 0)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
return 0
|
||||
|
||||
def get_discount_percent(self, category: str, period_days: Optional[int] = None) -> int:
|
||||
if category == "period":
|
||||
return max(0, min(100, self._get_period_discount(period_days)))
|
||||
|
||||
mapping = {
|
||||
"servers": self.server_discount_percent,
|
||||
"traffic": self.traffic_discount_percent,
|
||||
@@ -284,18 +330,6 @@ class PromoGroup(Base):
|
||||
}
|
||||
percent = mapping.get(category, 0)
|
||||
|
||||
if self.is_default and period_days is not None:
|
||||
try:
|
||||
from app.config import settings
|
||||
|
||||
if settings.is_base_promo_group_period_discount_enabled():
|
||||
discounts = settings.get_base_promo_group_period_discounts()
|
||||
if period_days in discounts:
|
||||
period_discount = discounts[period_days]
|
||||
percent = period_discount
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return max(0, min(100, percent))
|
||||
|
||||
|
||||
|
||||
@@ -681,6 +681,46 @@ async def ensure_promo_groups_setup():
|
||||
f"Не удалось добавить уникальное ограничение uq_promo_groups_name: {e}"
|
||||
)
|
||||
|
||||
period_discounts_column_exists = await check_column_exists(
|
||||
"promo_groups", "period_discounts"
|
||||
)
|
||||
|
||||
if not period_discounts_column_exists:
|
||||
if db_type == "sqlite":
|
||||
await conn.execute(
|
||||
text("ALTER TABLE promo_groups ADD COLUMN period_discounts JSON")
|
||||
)
|
||||
await conn.execute(
|
||||
text("UPDATE promo_groups SET period_discounts = '{}' WHERE period_discounts IS NULL")
|
||||
)
|
||||
elif db_type == "postgresql":
|
||||
await conn.execute(
|
||||
text(
|
||||
"ALTER TABLE promo_groups ADD COLUMN period_discounts JSONB"
|
||||
)
|
||||
)
|
||||
await conn.execute(
|
||||
text(
|
||||
"UPDATE promo_groups SET period_discounts = '{}'::jsonb WHERE period_discounts IS NULL"
|
||||
)
|
||||
)
|
||||
elif db_type == "mysql":
|
||||
await conn.execute(
|
||||
text("ALTER TABLE promo_groups ADD COLUMN period_discounts JSON")
|
||||
)
|
||||
await conn.execute(
|
||||
text(
|
||||
"UPDATE promo_groups SET period_discounts = JSON_OBJECT() WHERE period_discounts IS NULL"
|
||||
)
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"Неподдерживаемый тип БД для promo_groups.period_discounts: {db_type}"
|
||||
)
|
||||
return False
|
||||
|
||||
logger.info("Добавлена колонка promo_groups.period_discounts")
|
||||
|
||||
column_exists = await check_column_exists("users", "promo_group_id")
|
||||
|
||||
if not column_exists:
|
||||
@@ -1543,7 +1583,8 @@ async def check_migration_status():
|
||||
"subscription_duplicates": False,
|
||||
"subscription_conversions_table": False,
|
||||
"promo_groups_table": False,
|
||||
"users_promo_group_column": False
|
||||
"users_promo_group_column": False,
|
||||
"promo_groups_period_discounts_column": False,
|
||||
}
|
||||
|
||||
status["has_made_first_topup_column"] = await check_column_exists('users', 'has_made_first_topup')
|
||||
@@ -1556,6 +1597,7 @@ async def check_migration_status():
|
||||
|
||||
status["welcome_texts_is_enabled_column"] = await check_column_exists('welcome_texts', 'is_enabled')
|
||||
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')
|
||||
|
||||
media_fields_exist = (
|
||||
await check_column_exists('broadcast_history', 'has_media') and
|
||||
@@ -1587,7 +1629,8 @@ async def check_migration_status():
|
||||
"subscription_conversions_table": "Таблица конверсий подписок",
|
||||
"subscription_duplicates": "Отсутствие дубликатов подписок",
|
||||
"promo_groups_table": "Таблица промо-групп",
|
||||
"users_promo_group_column": "Колонка promo_group_id у пользователей"
|
||||
"users_promo_group_column": "Колонка promo_group_id у пользователей",
|
||||
"promo_groups_period_discounts_column": "Колонка period_discounts у промо-групп",
|
||||
}
|
||||
|
||||
for check_key, check_status in status.items():
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
import logging
|
||||
from typing import Dict, Optional
|
||||
|
||||
from aiogram import Dispatcher, types, F
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.crud.promo_group import (
|
||||
get_promo_groups_with_counts,
|
||||
get_promo_group_by_id,
|
||||
@@ -22,7 +24,7 @@ from app.keyboards.admin import (
|
||||
get_admin_pagination_keyboard,
|
||||
get_confirmation_keyboard,
|
||||
)
|
||||
|
||||
from app.utils.pricing_utils import format_period_description
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -37,6 +39,128 @@ def _format_discount_line(texts, group) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _normalize_periods_dict(raw: Optional[Dict]) -> Dict[int, int]:
|
||||
if not raw or not isinstance(raw, dict):
|
||||
return {}
|
||||
|
||||
normalized: Dict[int, int] = {}
|
||||
|
||||
for key, value in raw.items():
|
||||
try:
|
||||
period = int(key)
|
||||
percent = int(value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
normalized[period] = max(0, min(100, percent))
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def _collect_period_discounts(group: PromoGroup) -> Dict[int, int]:
|
||||
discounts = _normalize_periods_dict(getattr(group, "period_discounts", None))
|
||||
|
||||
if discounts:
|
||||
return dict(sorted(discounts.items()))
|
||||
|
||||
if group.is_default and settings.is_base_promo_group_period_discount_enabled():
|
||||
try:
|
||||
base_discounts = settings.get_base_promo_group_period_discounts()
|
||||
normalized = _normalize_periods_dict(base_discounts)
|
||||
return dict(sorted(normalized.items()))
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _format_period_discounts_lines(texts, group: PromoGroup, language: str) -> list:
|
||||
discounts = _collect_period_discounts(group)
|
||||
|
||||
if not discounts:
|
||||
return []
|
||||
|
||||
header = texts.t(
|
||||
"ADMIN_PROMO_GROUP_PERIOD_DISCOUNTS_HEADER",
|
||||
"⏳ Скидки по периодам:",
|
||||
)
|
||||
|
||||
lines = [header]
|
||||
|
||||
for period_days, percent in discounts.items():
|
||||
period_display = format_period_description(period_days, language)
|
||||
lines.append(
|
||||
texts.t("PROMO_GROUP_PERIOD_DISCOUNT_ITEM", "{period} — {percent}%").format(
|
||||
period=period_display,
|
||||
percent=percent,
|
||||
)
|
||||
)
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def _format_period_discounts_value(discounts: Dict[int, int]) -> str:
|
||||
if not discounts:
|
||||
return "0"
|
||||
|
||||
return ", ".join(
|
||||
f"{period}:{percent}"
|
||||
for period, percent in sorted(discounts.items())
|
||||
)
|
||||
|
||||
|
||||
def _parse_period_discounts_input(value: str) -> Dict[int, int]:
|
||||
cleaned = (value or "").strip()
|
||||
|
||||
if not cleaned or cleaned in {"0", "-"}:
|
||||
return {}
|
||||
|
||||
cleaned = cleaned.replace(";", ",").replace("\n", ",")
|
||||
parts = [part.strip() for part in cleaned.split(",") if part.strip()]
|
||||
|
||||
if not parts:
|
||||
return {}
|
||||
|
||||
discounts: Dict[int, int] = {}
|
||||
|
||||
for part in parts:
|
||||
if ":" not in part:
|
||||
raise ValueError
|
||||
|
||||
period_raw, percent_raw = part.split(":", 1)
|
||||
|
||||
period = int(period_raw.strip())
|
||||
percent = int(percent_raw.strip())
|
||||
|
||||
if period <= 0:
|
||||
raise ValueError
|
||||
|
||||
discounts[period] = max(0, min(100, percent))
|
||||
|
||||
return discounts
|
||||
|
||||
|
||||
async def _prompt_for_period_discounts(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
prompt_key: str,
|
||||
default_text: str,
|
||||
*,
|
||||
current_value: Optional[str] = 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:
|
||||
try:
|
||||
prompt_text = prompt_text.format(current=current_value)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
await message.answer(prompt_text)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_promo_groups_menu(
|
||||
@@ -64,17 +188,20 @@ async def show_promo_groups_menu(
|
||||
if group.is_default
|
||||
else ""
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
f"{'⭐' if group.is_default else '🎯'} <b>{group.name}</b>{default_suffix}",
|
||||
_format_discount_line(texts, group),
|
||||
texts.t(
|
||||
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT",
|
||||
"Участников: {count}",
|
||||
).format(count=member_count),
|
||||
"",
|
||||
]
|
||||
)
|
||||
group_lines = [
|
||||
f"{'⭐' if group.is_default else '🎯'} <b>{group.name}</b>{default_suffix}",
|
||||
_format_discount_line(texts, group),
|
||||
texts.t(
|
||||
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT",
|
||||
"Участников: {count}",
|
||||
).format(count=member_count),
|
||||
]
|
||||
|
||||
period_lines = _format_period_discounts_lines(texts, group, db_user.language)
|
||||
group_lines.extend(period_lines)
|
||||
group_lines.append("")
|
||||
|
||||
lines.extend(group_lines)
|
||||
keyboard_rows.append([
|
||||
types.InlineKeyboardButton(
|
||||
text=f"{'⭐' if group.is_default else '🎯'} {group.name}",
|
||||
@@ -127,25 +254,30 @@ async def show_promo_group_details(
|
||||
member_count = await count_promo_group_members(db, group.id)
|
||||
|
||||
default_note = (
|
||||
"\n" + texts.t("ADMIN_PROMO_GROUP_DETAILS_DEFAULT", "Это базовая группа.")
|
||||
texts.t("ADMIN_PROMO_GROUP_DETAILS_DEFAULT", "Это базовая группа.")
|
||||
if group.is_default
|
||||
else ""
|
||||
)
|
||||
|
||||
text = "\n".join(
|
||||
[
|
||||
texts.t(
|
||||
"ADMIN_PROMO_GROUP_DETAILS_TITLE",
|
||||
"💳 <b>Промогруппа:</b> {name}",
|
||||
).format(name=group.name),
|
||||
_format_discount_line(texts, group),
|
||||
texts.t(
|
||||
"ADMIN_PROMO_GROUP_DETAILS_MEMBERS",
|
||||
"Участников: {count}",
|
||||
).format(count=member_count),
|
||||
default_note,
|
||||
]
|
||||
)
|
||||
lines = [
|
||||
texts.t(
|
||||
"ADMIN_PROMO_GROUP_DETAILS_TITLE",
|
||||
"💳 <b>Промогруппа:</b> {name}",
|
||||
).format(name=group.name),
|
||||
_format_discount_line(texts, group),
|
||||
texts.t(
|
||||
"ADMIN_PROMO_GROUP_DETAILS_MEMBERS",
|
||||
"Участников: {count}",
|
||||
).format(count=member_count),
|
||||
]
|
||||
|
||||
period_lines = _format_period_discounts_lines(texts, group, db_user.language)
|
||||
lines.extend(period_lines)
|
||||
|
||||
if default_note:
|
||||
lines.append(default_note)
|
||||
|
||||
text = "\n".join(line for line in lines if line)
|
||||
|
||||
keyboard_rows = []
|
||||
if member_count > 0:
|
||||
@@ -299,13 +431,47 @@ async def process_create_group_devices(
|
||||
await message.answer(texts.t("ADMIN_PROMO_GROUP_INVALID_PERCENT", "Введите число от 0 до 100."))
|
||||
return
|
||||
|
||||
await state.update_data(new_group_devices=devices_discount)
|
||||
await state.set_state(AdminStates.creating_promo_group_period_discount)
|
||||
|
||||
await _prompt_for_period_discounts(
|
||||
message,
|
||||
state,
|
||||
"ADMIN_PROMO_GROUP_CREATE_PERIOD_PROMPT",
|
||||
"Введите скидки на периоды подписки (например, 30:10, 90:15). Отправьте 0, если без скидок.",
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_create_group_period_discounts(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
db_user,
|
||||
db: AsyncSession,
|
||||
):
|
||||
data = await state.get_data()
|
||||
texts = get_texts(data.get("language", db_user.language))
|
||||
|
||||
try:
|
||||
period_discounts = _parse_period_discounts_input(message.text)
|
||||
except ValueError:
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"ADMIN_PROMO_GROUP_INVALID_PERIOD_DISCOUNTS",
|
||||
"Введите пары период:скидка через запятую, например 30:10, 90:15, или 0.",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
group = await create_promo_group(
|
||||
db,
|
||||
data["new_group_name"],
|
||||
traffic_discount_percent=data["new_group_traffic"],
|
||||
server_discount_percent=data["new_group_servers"],
|
||||
device_discount_percent=devices_discount,
|
||||
device_discount_percent=data["new_group_devices"],
|
||||
period_discounts=period_discounts,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось создать промогруппу: {e}")
|
||||
@@ -440,13 +606,55 @@ async def process_edit_group_devices(
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
await state.update_data(edit_group_devices=devices_discount)
|
||||
await state.set_state(AdminStates.editing_promo_group_period_discount)
|
||||
|
||||
current_discounts = _normalize_periods_dict(getattr(group, "period_discounts", None))
|
||||
await _prompt_for_period_discounts(
|
||||
message,
|
||||
state,
|
||||
"ADMIN_PROMO_GROUP_EDIT_PERIOD_PROMPT",
|
||||
"Введите новые скидки на периоды (текущие: {current}). Отправьте 0, если без скидок.",
|
||||
current_value=_format_period_discounts_value(current_discounts),
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_edit_group_period_discounts(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
db_user,
|
||||
db: AsyncSession,
|
||||
):
|
||||
data = await state.get_data()
|
||||
texts = get_texts(data.get("language", db_user.language))
|
||||
|
||||
try:
|
||||
period_discounts = _parse_period_discounts_input(message.text)
|
||||
except ValueError:
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"ADMIN_PROMO_GROUP_INVALID_PERIOD_DISCOUNTS",
|
||||
"Введите пары период:скидка через запятую, например 30:10, 90:15, или 0.",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
group = await get_promo_group_by_id(db, data["edit_group_id"])
|
||||
if not group:
|
||||
await message.answer("❌ Промогруппа не найдена")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
await update_promo_group(
|
||||
db,
|
||||
group,
|
||||
name=data["edit_group_name"],
|
||||
traffic_discount_percent=data["edit_group_traffic"],
|
||||
server_discount_percent=data["edit_group_servers"],
|
||||
device_discount_percent=devices_discount,
|
||||
device_discount_percent=data["edit_group_devices"],
|
||||
period_discounts=period_discounts,
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
@@ -616,6 +824,10 @@ def register_handlers(dp: Dispatcher):
|
||||
process_create_group_devices,
|
||||
AdminStates.creating_promo_group_device_discount,
|
||||
)
|
||||
dp.message.register(
|
||||
process_create_group_period_discounts,
|
||||
AdminStates.creating_promo_group_period_discount,
|
||||
)
|
||||
|
||||
dp.message.register(process_edit_group_name, AdminStates.editing_promo_group_name)
|
||||
dp.message.register(
|
||||
@@ -630,3 +842,7 @@ def register_handlers(dp: Dispatcher):
|
||||
process_edit_group_devices,
|
||||
AdminStates.editing_promo_group_device_discount,
|
||||
)
|
||||
dp.message.register(
|
||||
process_edit_group_period_discounts,
|
||||
AdminStates.editing_promo_group_period_discount,
|
||||
)
|
||||
|
||||
@@ -103,7 +103,15 @@ async def _prepare_subscription_summary(
|
||||
months_in_period = calculate_months_from_days(summary_data['period_days'])
|
||||
period_display = format_period_description(summary_data['period_days'], db_user.language)
|
||||
|
||||
base_price = PERIOD_PRICES[summary_data['period_days']]
|
||||
base_price_original = PERIOD_PRICES[summary_data['period_days']]
|
||||
period_discount_percent = db_user.get_promo_discount(
|
||||
"period",
|
||||
summary_data['period_days'],
|
||||
)
|
||||
base_price, base_discount_total = apply_percentage_discount(
|
||||
base_price_original,
|
||||
period_discount_percent,
|
||||
)
|
||||
|
||||
if settings.is_traffic_fixed():
|
||||
traffic_limit = settings.get_fixed_traffic_limit()
|
||||
@@ -195,6 +203,9 @@ async def _prepare_subscription_summary(
|
||||
summary_data['server_prices_for_period'] = selected_server_prices
|
||||
summary_data['months_in_period'] = months_in_period
|
||||
summary_data['base_price'] = base_price
|
||||
summary_data['base_price_original'] = base_price_original
|
||||
summary_data['base_discount_percent'] = period_discount_percent
|
||||
summary_data['base_discount_total'] = base_discount_total
|
||||
summary_data['final_traffic_gb'] = final_traffic_gb
|
||||
summary_data['traffic_price_per_month'] = traffic_price_per_month
|
||||
summary_data['traffic_discount_percent'] = traffic_component["discount_percent"]
|
||||
@@ -226,7 +237,15 @@ async def _prepare_subscription_summary(
|
||||
else:
|
||||
traffic_display = f"{summary_data.get('traffic_gb', 0)} ГБ"
|
||||
|
||||
details_lines = [f"- Базовый период: {texts.format_price(base_price)}"]
|
||||
base_line = f"- Базовый период: {texts.format_price(base_price_original)}"
|
||||
if base_discount_total > 0:
|
||||
base_line += (
|
||||
f" → {texts.format_price(base_price)}"
|
||||
f" (скидка {period_discount_percent}%:"
|
||||
f" -{texts.format_price(base_discount_total)})"
|
||||
)
|
||||
|
||||
details_lines = [base_line]
|
||||
|
||||
if total_traffic_price > 0:
|
||||
traffic_line = (
|
||||
@@ -317,26 +336,29 @@ def _build_promo_group_discount_text(
|
||||
|
||||
period_lines: List[str] = []
|
||||
|
||||
if (
|
||||
promo_group.is_default
|
||||
and periods
|
||||
and settings.is_base_promo_group_period_discount_enabled()
|
||||
):
|
||||
discounts = settings.get_base_promo_group_period_discounts()
|
||||
period_candidates: set[int] = set(periods or [])
|
||||
|
||||
for period_days in periods:
|
||||
percent = discounts.get(period_days, 0)
|
||||
|
||||
if percent <= 0:
|
||||
raw_period_discounts = getattr(promo_group, "period_discounts", None)
|
||||
if isinstance(raw_period_discounts, dict):
|
||||
for key in raw_period_discounts.keys():
|
||||
try:
|
||||
period_candidates.add(int(key))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
period_display = format_period_description(period_days, db_user.language)
|
||||
period_lines.append(
|
||||
texts.PROMO_GROUP_PERIOD_DISCOUNT_ITEM.format(
|
||||
period=period_display,
|
||||
percent=percent,
|
||||
)
|
||||
for period_days in sorted(period_candidates):
|
||||
percent = promo_group.get_discount_percent("period", period_days)
|
||||
|
||||
if percent <= 0:
|
||||
continue
|
||||
|
||||
period_display = format_period_description(period_days, db_user.language)
|
||||
period_lines.append(
|
||||
texts.PROMO_GROUP_PERIOD_DISCOUNT_ITEM.format(
|
||||
period=period_display,
|
||||
percent=percent,
|
||||
)
|
||||
)
|
||||
|
||||
if not service_lines and not period_lines:
|
||||
return ""
|
||||
@@ -666,8 +688,26 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
|
||||
base_cost = PERIOD_PRICES.get(30, 0)
|
||||
|
||||
base_cost_original = PERIOD_PRICES.get(30, 0)
|
||||
try:
|
||||
owner = subscription.user
|
||||
except AttributeError:
|
||||
owner = None
|
||||
|
||||
period_discount_percent = 0
|
||||
if owner:
|
||||
try:
|
||||
period_discount_percent = owner.get_promo_discount("period", 30)
|
||||
except AttributeError:
|
||||
period_discount_percent = 0
|
||||
|
||||
from app.utils.pricing_utils import apply_percentage_discount
|
||||
|
||||
base_cost, _ = apply_percentage_discount(
|
||||
base_cost_original,
|
||||
period_discount_percent,
|
||||
)
|
||||
|
||||
try:
|
||||
servers_cost, _ = await subscription_service.get_countries_price_by_uuids(
|
||||
subscription.connected_squads, db
|
||||
@@ -683,7 +723,14 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
|
||||
total_cost = base_cost + servers_cost + traffic_cost + devices_cost
|
||||
|
||||
logger.info(f"📊 Месячная стоимость конфигурации подписки {subscription.id}:")
|
||||
logger.info(f" 📅 Базовый тариф (30 дней): {base_cost/100}₽")
|
||||
base_log = f" 📅 Базовый тариф (30 дней): {base_cost_original/100}₽"
|
||||
if period_discount_percent > 0:
|
||||
discount_value = base_cost_original * period_discount_percent // 100
|
||||
base_log += (
|
||||
f" → {base_cost/100}₽"
|
||||
f" (скидка {period_discount_percent}%: -{discount_value/100}₽)"
|
||||
)
|
||||
logger.info(base_log)
|
||||
if servers_cost > 0:
|
||||
logger.info(f" 🌍 Серверы: {servers_cost/100}₽")
|
||||
if traffic_cost > 0:
|
||||
@@ -1801,7 +1848,14 @@ async def handle_extend_subscription(
|
||||
months_in_period = calculate_months_from_days(days)
|
||||
|
||||
from app.config import PERIOD_PRICES
|
||||
base_price = PERIOD_PRICES.get(days, 0)
|
||||
from app.utils.pricing_utils import apply_percentage_discount
|
||||
|
||||
base_price_original = PERIOD_PRICES.get(days, 0)
|
||||
period_discount_percent = db_user.get_promo_discount("period", days)
|
||||
base_price, _ = apply_percentage_discount(
|
||||
base_price_original,
|
||||
period_discount_percent,
|
||||
)
|
||||
|
||||
servers_price_per_month, _ = await subscription_service.get_countries_price_by_uuids(
|
||||
subscription.connected_squads, db
|
||||
@@ -2015,7 +2069,11 @@ async def confirm_extend_subscription(
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
from app.utils.pricing_utils import calculate_months_from_days, validate_pricing_calculation
|
||||
from app.utils.pricing_utils import (
|
||||
calculate_months_from_days,
|
||||
validate_pricing_calculation,
|
||||
apply_percentage_discount,
|
||||
)
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
|
||||
days = int(callback.data.split('_')[2])
|
||||
@@ -2032,8 +2090,14 @@ async def confirm_extend_subscription(
|
||||
|
||||
try:
|
||||
from app.config import PERIOD_PRICES
|
||||
from app.utils.pricing_utils import apply_percentage_discount
|
||||
|
||||
base_price = PERIOD_PRICES.get(days, 0)
|
||||
base_price_original = PERIOD_PRICES.get(days, 0)
|
||||
period_discount_percent = db_user.get_promo_discount("period", days)
|
||||
base_price, base_discount_total = apply_percentage_discount(
|
||||
base_price_original,
|
||||
period_discount_percent,
|
||||
)
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
servers_price_per_month, per_server_monthly_prices = await subscription_service.get_countries_price_by_uuids(
|
||||
@@ -2091,7 +2155,13 @@ async def confirm_extend_subscription(
|
||||
return
|
||||
|
||||
logger.info(f"💰 Расчет продления подписки {subscription.id} на {days} дней ({months_in_period} мес):")
|
||||
logger.info(f" 📅 Период {days} дней: {base_price/100}₽")
|
||||
base_log = f" 📅 Период {days} дней: {base_price_original/100}₽"
|
||||
if base_discount_total > 0:
|
||||
base_log += (
|
||||
f" → {base_price/100}₽"
|
||||
f" (скидка {period_discount_percent}%: -{base_discount_total/100}₽)"
|
||||
)
|
||||
logger.info(base_log)
|
||||
if total_servers_price > 0:
|
||||
logger.info(
|
||||
f" 🌐 Серверы: {servers_price_per_month/100}₽/мес × {months_in_period}"
|
||||
@@ -2543,7 +2613,15 @@ async def select_country(
|
||||
|
||||
countries = await _get_available_countries()
|
||||
|
||||
base_price = PERIOD_PRICES[data['period_days']] + settings.get_traffic_price(data['traffic_gb'])
|
||||
period_base_price = PERIOD_PRICES[data['period_days']]
|
||||
from app.utils.pricing_utils import apply_percentage_discount
|
||||
|
||||
discounted_base_price, _ = apply_percentage_discount(
|
||||
period_base_price,
|
||||
db_user.get_promo_discount("period", data['period_days']),
|
||||
)
|
||||
|
||||
base_price = discounted_base_price + settings.get_traffic_price(data['traffic_gb'])
|
||||
|
||||
try:
|
||||
subscription_service = SubscriptionService()
|
||||
@@ -2683,7 +2761,34 @@ async def confirm_purchase(
|
||||
'months_in_period', calculate_months_from_days(data['period_days'])
|
||||
)
|
||||
|
||||
base_price = data.get('base_price', PERIOD_PRICES[data['period_days']])
|
||||
base_price = data.get('base_price')
|
||||
base_price_original = data.get('base_price_original')
|
||||
base_discount_percent = data.get('base_discount_percent')
|
||||
base_discount_total = data.get('base_discount_total')
|
||||
|
||||
if base_price is None:
|
||||
base_price_original = PERIOD_PRICES[data['period_days']]
|
||||
base_discount_percent = db_user.get_promo_discount(
|
||||
"period",
|
||||
data['period_days'],
|
||||
)
|
||||
base_price, base_discount_total = apply_percentage_discount(
|
||||
base_price_original,
|
||||
base_discount_percent,
|
||||
)
|
||||
else:
|
||||
if base_price_original is None:
|
||||
base_price_original = PERIOD_PRICES[data['period_days']]
|
||||
if base_discount_percent is None:
|
||||
base_discount_percent = db_user.get_promo_discount(
|
||||
"period",
|
||||
data['period_days'],
|
||||
)
|
||||
if base_discount_total is None:
|
||||
_, base_discount_total = apply_percentage_discount(
|
||||
base_price_original,
|
||||
base_discount_percent,
|
||||
)
|
||||
server_prices = data.get('server_prices_for_period', [])
|
||||
|
||||
if not server_prices:
|
||||
@@ -2812,7 +2917,13 @@ async def confirm_purchase(
|
||||
return
|
||||
|
||||
logger.info(f"Расчет покупки подписки на {data['period_days']} дней ({months_in_period} мес):")
|
||||
logger.info(f" Период: {base_price/100}₽")
|
||||
base_log = f" Период: {base_price_original/100}₽"
|
||||
if base_discount_total and base_discount_total > 0:
|
||||
base_log += (
|
||||
f" → {base_price/100}₽"
|
||||
f" (скидка {base_discount_percent}%: -{base_discount_total/100}₽)"
|
||||
)
|
||||
logger.info(base_log)
|
||||
if total_traffic_price > 0:
|
||||
message = (
|
||||
f" Трафик: {traffic_price_per_month/100}₽/мес × {months_in_period}"
|
||||
|
||||
@@ -299,7 +299,15 @@ class SubscriptionService:
|
||||
if settings.MAX_DEVICES_LIMIT > 0 and devices > settings.MAX_DEVICES_LIMIT:
|
||||
raise ValueError(f"Превышен максимальный лимит устройств: {settings.MAX_DEVICES_LIMIT}")
|
||||
|
||||
base_price = PERIOD_PRICES.get(period_days, 0)
|
||||
base_price_original = PERIOD_PRICES.get(period_days, 0)
|
||||
period_discount_percent = _resolve_discount_percent(
|
||||
user,
|
||||
promo_group,
|
||||
"period",
|
||||
period_days=period_days,
|
||||
)
|
||||
base_discount_total = base_price_original * period_discount_percent // 100
|
||||
base_price = base_price_original - base_discount_total
|
||||
|
||||
promo_group = promo_group or (user.promo_group if user else None)
|
||||
|
||||
@@ -353,7 +361,13 @@ class SubscriptionService:
|
||||
total_price = base_price + discounted_traffic_price + total_servers_price + discounted_devices_price
|
||||
|
||||
logger.info(f"Расчет стоимости новой подписки:")
|
||||
logger.info(f" Период {period_days} дней: {base_price/100}₽")
|
||||
base_log = f" Период {period_days} дней: {base_price_original/100}₽"
|
||||
if base_discount_total > 0:
|
||||
base_log += (
|
||||
f" → {base_price/100}₽"
|
||||
f" (скидка {period_discount_percent}%: -{base_discount_total/100}₽)"
|
||||
)
|
||||
logger.info(base_log)
|
||||
if discounted_traffic_price > 0:
|
||||
message = f" Трафик {traffic_gb} ГБ: {traffic_price/100}₽"
|
||||
if traffic_discount > 0:
|
||||
@@ -391,7 +405,7 @@ class SubscriptionService:
|
||||
try:
|
||||
from app.config import PERIOD_PRICES
|
||||
|
||||
base_price = PERIOD_PRICES.get(period_days, 0)
|
||||
base_price_original = PERIOD_PRICES.get(period_days, 0)
|
||||
|
||||
if user is None:
|
||||
user = getattr(subscription, "user", None)
|
||||
@@ -430,6 +444,15 @@ class SubscriptionService:
|
||||
traffic_discount = traffic_price * traffic_discount_percent // 100
|
||||
discounted_traffic_price = traffic_price - traffic_discount
|
||||
|
||||
period_discount_percent = _resolve_discount_percent(
|
||||
user,
|
||||
promo_group,
|
||||
"period",
|
||||
period_days=period_days,
|
||||
)
|
||||
base_discount_total = base_price_original * period_discount_percent // 100
|
||||
base_price = base_price_original - base_discount_total
|
||||
|
||||
total_price = (
|
||||
base_price
|
||||
+ discounted_servers_price
|
||||
@@ -438,7 +461,13 @@ class SubscriptionService:
|
||||
)
|
||||
|
||||
logger.info(f"💰 Расчет стоимости продления для подписки {subscription.id} (по текущим ценам):")
|
||||
logger.info(f" 📅 Период {period_days} дней: {base_price/100}₽")
|
||||
base_log = f" 📅 Период {period_days} дней: {base_price_original/100}₽"
|
||||
if base_discount_total > 0:
|
||||
base_log += (
|
||||
f" → {base_price/100}₽"
|
||||
f" (скидка {period_discount_percent}%: -{base_discount_total/100}₽)"
|
||||
)
|
||||
logger.info(base_log)
|
||||
if servers_price > 0:
|
||||
message = f" 🌍 Серверы ({len(subscription.connected_squads)}) по текущим ценам: {discounted_servers_price/100}₽"
|
||||
if servers_discount > 0:
|
||||
@@ -577,7 +606,15 @@ class SubscriptionService:
|
||||
|
||||
months_in_period = calculate_months_from_days(period_days)
|
||||
|
||||
base_price = PERIOD_PRICES.get(period_days, 0)
|
||||
base_price_original = PERIOD_PRICES.get(period_days, 0)
|
||||
period_discount_percent = _resolve_discount_percent(
|
||||
user,
|
||||
promo_group,
|
||||
"period",
|
||||
period_days=period_days,
|
||||
)
|
||||
base_discount_total = base_price_original * period_discount_percent // 100
|
||||
base_price = base_price_original - base_discount_total
|
||||
|
||||
promo_group = promo_group or (user.promo_group if user else None)
|
||||
|
||||
@@ -637,7 +674,13 @@ class SubscriptionService:
|
||||
total_price = base_price + total_traffic_price + total_servers_price + total_devices_price
|
||||
|
||||
logger.info(f"Расчет стоимости новой подписки на {period_days} дней ({months_in_period} мес):")
|
||||
logger.info(f" Период {period_days} дней: {base_price/100}₽")
|
||||
base_log = f" Период {period_days} дней: {base_price_original/100}₽"
|
||||
if base_discount_total > 0:
|
||||
base_log += (
|
||||
f" → {base_price/100}₽"
|
||||
f" (скидка {period_discount_percent}%: -{base_discount_total/100}₽)"
|
||||
)
|
||||
logger.info(base_log)
|
||||
if total_traffic_price > 0:
|
||||
message = (
|
||||
f" Трафик {traffic_gb} ГБ: {traffic_price_per_month/100}₽/мес x {months_in_period} = {total_traffic_price/100}₽"
|
||||
@@ -681,7 +724,7 @@ class SubscriptionService:
|
||||
|
||||
months_in_period = calculate_months_from_days(period_days)
|
||||
|
||||
base_price = PERIOD_PRICES.get(period_days, 0)
|
||||
base_price_original = PERIOD_PRICES.get(period_days, 0)
|
||||
|
||||
if user is None:
|
||||
user = getattr(subscription, "user", None)
|
||||
@@ -723,10 +766,25 @@ class SubscriptionService:
|
||||
discounted_traffic_per_month = traffic_price_per_month - traffic_discount_per_month
|
||||
total_traffic_price = discounted_traffic_per_month * months_in_period
|
||||
|
||||
period_discount_percent = _resolve_discount_percent(
|
||||
user,
|
||||
promo_group,
|
||||
"period",
|
||||
period_days=period_days,
|
||||
)
|
||||
base_discount_total = base_price_original * period_discount_percent // 100
|
||||
base_price = base_price_original - base_discount_total
|
||||
|
||||
total_price = base_price + total_servers_price + total_devices_price + total_traffic_price
|
||||
|
||||
logger.info(f"💰 Расчет стоимости продления подписки {subscription.id} на {period_days} дней ({months_in_period} мес):")
|
||||
logger.info(f" 📅 Период {period_days} дней: {base_price/100}₽")
|
||||
base_log = f" 📅 Период {period_days} дней: {base_price_original/100}₽"
|
||||
if base_discount_total > 0:
|
||||
base_log += (
|
||||
f" → {base_price/100}₽"
|
||||
f" (скидка {period_discount_percent}%: -{base_discount_total/100}₽)"
|
||||
)
|
||||
logger.info(base_log)
|
||||
if total_servers_price > 0:
|
||||
message = (
|
||||
f" 🌍 Серверы: {servers_price_per_month/100}₽/мес x {months_in_period} = {total_servers_price/100}₽"
|
||||
|
||||
@@ -68,11 +68,13 @@ class AdminStates(StatesGroup):
|
||||
creating_promo_group_traffic_discount = State()
|
||||
creating_promo_group_server_discount = State()
|
||||
creating_promo_group_device_discount = State()
|
||||
creating_promo_group_period_discount = State()
|
||||
|
||||
editing_promo_group_name = State()
|
||||
editing_promo_group_traffic_discount = State()
|
||||
editing_promo_group_server_discount = State()
|
||||
editing_promo_group_device_discount = State()
|
||||
editing_promo_group_period_discount = State()
|
||||
|
||||
editing_squad_price = State()
|
||||
editing_traffic_price = State()
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
"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_PERIOD_DISCOUNTS_HEADER": "⏳ Period discounts:",
|
||||
"ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (default)",
|
||||
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT": "Members: {count}",
|
||||
"ADMIN_PROMO_GROUPS_EMPTY": "No promo groups found.",
|
||||
@@ -224,13 +225,16 @@
|
||||
"ADMIN_PROMO_GROUP_CREATE_TRAFFIC_PROMPT": "Enter traffic discount (0-100):",
|
||||
"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_INVALID_PERCENT": "Enter a number from 0 to 100.",
|
||||
"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",
|
||||
"ADMIN_PROMO_GROUP_EDIT_NAME_PROMPT": "Enter a new name (current: {name}):",
|
||||
"ADMIN_PROMO_GROUP_EDIT_TRAFFIC_PROMPT": "Enter new traffic discount (0-100):",
|
||||
"ADMIN_PROMO_GROUP_EDIT_SERVERS_PROMPT": "Enter new server discount (0-100):",
|
||||
"ADMIN_PROMO_GROUP_EDIT_DEVICES_PROMPT": "Enter new device discount (0-100):",
|
||||
"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_MEMBERS_TITLE": "👥 Members of {name}",
|
||||
"ADMIN_PROMO_GROUP_MEMBERS_EMPTY": "This group has no members yet.",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"ADMIN_PROMO_GROUPS_TITLE": "💳 <b>Промогруппы</b>",
|
||||
"ADMIN_PROMO_GROUPS_SUMMARY": "Всего групп: {count}\nВсего участников: {members}",
|
||||
"ADMIN_PROMO_GROUPS_DISCOUNTS": "Скидки — серверы: {servers}%, трафик: {traffic}%, устройства: {devices}%",
|
||||
"ADMIN_PROMO_GROUP_PERIOD_DISCOUNTS_HEADER": "⏳ Скидки по периодам:",
|
||||
"ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (базовая)",
|
||||
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT": "Участников: {count}",
|
||||
"ADMIN_PROMO_GROUPS_EMPTY": "Промогруппы не найдены.",
|
||||
@@ -102,13 +103,16 @@
|
||||
"ADMIN_PROMO_GROUP_CREATE_TRAFFIC_PROMPT": "Введите скидку на трафик (0-100):",
|
||||
"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_INVALID_PERCENT": "Введите число от 0 до 100.",
|
||||
"ADMIN_PROMO_GROUP_INVALID_PERIOD_DISCOUNTS": "Введите пары период:скидка через запятую, например 30:10, 90:15, или 0.",
|
||||
"ADMIN_PROMO_GROUP_CREATED": "Промогруппа «{name}» создана.",
|
||||
"ADMIN_PROMO_GROUP_CREATED_BACK_BUTTON": "↩️ К промогруппам",
|
||||
"ADMIN_PROMO_GROUP_EDIT_NAME_PROMPT": "Введите новое название промогруппы (текущее: {name}):",
|
||||
"ADMIN_PROMO_GROUP_EDIT_TRAFFIC_PROMPT": "Введите новую скидку на трафик (0-100):",
|
||||
"ADMIN_PROMO_GROUP_EDIT_SERVERS_PROMPT": "Введите новую скидку на серверы (0-100):",
|
||||
"ADMIN_PROMO_GROUP_EDIT_DEVICES_PROMPT": "Введите новую скидку на устройства (0-100):",
|
||||
"ADMIN_PROMO_GROUP_EDIT_PERIOD_PROMPT": "Введите новые скидки на периоды (текущие: {current}). Отправьте 0, если без скидок.",
|
||||
"ADMIN_PROMO_GROUP_UPDATED": "Промогруппа «{name}» обновлена.",
|
||||
"ADMIN_PROMO_GROUP_MEMBERS_TITLE": "👥 Участники группы {name}",
|
||||
"ADMIN_PROMO_GROUP_MEMBERS_EMPTY": "В этой группе пока нет участников.",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = "4b6b0f58c8f9"
|
||||
down_revision: Union[str, None] = "1f5f3a3f5a4d"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
dialect = bind.dialect.name if bind else ""
|
||||
|
||||
op.add_column(
|
||||
"promo_groups",
|
||||
sa.Column("period_discounts", sa.JSON(), nullable=True),
|
||||
)
|
||||
|
||||
if dialect == "postgresql":
|
||||
op.execute("UPDATE promo_groups SET period_discounts = '{}'::jsonb WHERE period_discounts IS NULL")
|
||||
else:
|
||||
op.execute("UPDATE promo_groups SET period_discounts = '{}' WHERE period_discounts IS NULL")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("promo_groups", "period_discounts")
|
||||
Reference in New Issue
Block a user