Add promo groups info section
This commit is contained in:
@@ -50,6 +50,25 @@ async def get_promo_groups_with_counts(
|
||||
return result.all()
|
||||
|
||||
|
||||
async def get_auto_assign_promo_groups(db: AsyncSession) -> List[PromoGroup]:
|
||||
result = await db.execute(
|
||||
select(PromoGroup)
|
||||
.where(PromoGroup.auto_assign_total_spent_kopeks.is_not(None))
|
||||
.where(PromoGroup.auto_assign_total_spent_kopeks > 0)
|
||||
.order_by(PromoGroup.auto_assign_total_spent_kopeks, PromoGroup.id)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def has_auto_assign_promo_groups(db: AsyncSession) -> bool:
|
||||
result = await db.execute(
|
||||
select(func.count(PromoGroup.id))
|
||||
.where(PromoGroup.auto_assign_total_spent_kopeks.is_not(None))
|
||||
.where(PromoGroup.auto_assign_total_spent_kopeks > 0)
|
||||
)
|
||||
return bool(result.scalar_one())
|
||||
|
||||
|
||||
async def get_promo_group_by_id(db: AsyncSession, group_id: int) -> Optional[PromoGroup]:
|
||||
return await db.get(PromoGroup, group_id)
|
||||
|
||||
|
||||
+254
-1
@@ -1,5 +1,7 @@
|
||||
import html
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from typing import Dict, List
|
||||
from aiogram import Dispatcher, types, F
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
@@ -8,13 +10,18 @@ from datetime import datetime
|
||||
|
||||
from app.config import settings
|
||||
from app.database.crud.user import get_user_by_telegram_id, update_user
|
||||
from app.database.crud.promo_group import (
|
||||
get_auto_assign_promo_groups,
|
||||
has_auto_assign_promo_groups,
|
||||
)
|
||||
from app.database.crud.transaction import get_user_total_spent_kopeks
|
||||
from app.keyboards.inline import (
|
||||
get_main_menu_keyboard,
|
||||
get_language_selection_keyboard,
|
||||
get_info_menu_keyboard,
|
||||
)
|
||||
from app.localization.texts import get_texts, get_rules
|
||||
from app.database.models import User
|
||||
from app.database.models import PromoGroup, User
|
||||
from app.database.crud.user_message import get_random_active_message
|
||||
from app.services.subscription_checkout_service import (
|
||||
has_subscription_checkout_draft,
|
||||
@@ -29,10 +36,109 @@ from app.utils.promo_offer import (
|
||||
from app.services.privacy_policy_service import PrivacyPolicyService
|
||||
from app.services.public_offer_service import PublicOfferService
|
||||
from app.services.faq_service import FaqService
|
||||
from app.utils.pricing_utils import format_period_description
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _format_rubles(amount_kopeks: int) -> str:
|
||||
rubles = Decimal(amount_kopeks) / Decimal(100)
|
||||
|
||||
if rubles == rubles.to_integral_value():
|
||||
formatted = f"{rubles:,.0f}"
|
||||
else:
|
||||
formatted = f"{rubles:,.2f}"
|
||||
|
||||
return f"{formatted.replace(',', ' ')} ₽"
|
||||
|
||||
|
||||
def _collect_period_discounts(group: PromoGroup) -> Dict[int, int]:
|
||||
discounts: Dict[int, int] = {}
|
||||
raw_discounts = getattr(group, "period_discounts", None)
|
||||
|
||||
if isinstance(raw_discounts, dict):
|
||||
for key, value in raw_discounts.items():
|
||||
try:
|
||||
period = int(key)
|
||||
percent = int(value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
normalized_percent = max(0, min(100, percent))
|
||||
if normalized_percent > 0:
|
||||
discounts[period] = normalized_percent
|
||||
|
||||
if group.is_default and settings.is_base_promo_group_period_discount_enabled():
|
||||
try:
|
||||
base_discounts = settings.get_base_promo_group_period_discounts() or {}
|
||||
except Exception:
|
||||
base_discounts = {}
|
||||
|
||||
for key, value in base_discounts.items():
|
||||
try:
|
||||
period = int(key)
|
||||
percent = int(value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
if period in discounts:
|
||||
continue
|
||||
|
||||
normalized_percent = max(0, min(100, percent))
|
||||
if normalized_percent > 0:
|
||||
discounts[period] = normalized_percent
|
||||
|
||||
return dict(sorted(discounts.items()))
|
||||
|
||||
|
||||
def _build_group_discount_lines(group: PromoGroup, texts, language: str) -> list[str]:
|
||||
lines: list[str] = []
|
||||
|
||||
if getattr(group, "server_discount_percent", 0) > 0:
|
||||
lines.append(
|
||||
texts.t("PROMO_GROUP_DISCOUNT_SERVERS", "🌍 Серверы: {percent}%").format(
|
||||
percent=group.server_discount_percent
|
||||
)
|
||||
)
|
||||
|
||||
if getattr(group, "traffic_discount_percent", 0) > 0:
|
||||
lines.append(
|
||||
texts.t("PROMO_GROUP_DISCOUNT_TRAFFIC", "📊 Трафик: {percent}%").format(
|
||||
percent=group.traffic_discount_percent
|
||||
)
|
||||
)
|
||||
|
||||
if getattr(group, "device_discount_percent", 0) > 0:
|
||||
lines.append(
|
||||
texts.t("PROMO_GROUP_DISCOUNT_DEVICES", "📱 Доп. устройства: {percent}%").format(
|
||||
percent=group.device_discount_percent
|
||||
)
|
||||
)
|
||||
|
||||
period_discounts = _collect_period_discounts(group)
|
||||
|
||||
if period_discounts:
|
||||
lines.append(
|
||||
texts.t(
|
||||
"PROMO_GROUP_PERIOD_DISCOUNTS_HEADER",
|
||||
"⏳ Скидки за длительный период:",
|
||||
)
|
||||
)
|
||||
|
||||
for period_days, percent in period_discounts.items():
|
||||
lines.append(
|
||||
texts.t(
|
||||
"PROMO_GROUP_PERIOD_DISCOUNT_ITEM",
|
||||
"{period} — {percent}%",
|
||||
).format(
|
||||
period=format_period_description(period_days, language),
|
||||
percent=percent,
|
||||
)
|
||||
)
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
async def show_main_menu(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
@@ -117,6 +223,7 @@ async def show_info_menu(
|
||||
privacy_enabled = await PrivacyPolicyService.is_policy_enabled(db, db_user.language)
|
||||
public_offer_enabled = await PublicOfferService.is_offer_enabled(db, db_user.language)
|
||||
faq_enabled = await FaqService.is_enabled(db, db_user.language)
|
||||
promo_groups_available = await has_auto_assign_promo_groups(db)
|
||||
|
||||
await edit_or_answer_photo(
|
||||
callback=callback,
|
||||
@@ -126,12 +233,153 @@ async def show_info_menu(
|
||||
show_privacy_policy=privacy_enabled,
|
||||
show_public_offer=public_offer_enabled,
|
||||
show_faq=faq_enabled,
|
||||
show_promo_groups=promo_groups_available,
|
||||
),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
async def show_promo_groups_info(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
promo_groups = await get_auto_assign_promo_groups(db)
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_info")]]
|
||||
)
|
||||
|
||||
if not promo_groups:
|
||||
empty_text = texts.t(
|
||||
"PROMO_GROUPS_INFO_EMPTY",
|
||||
"Промогруппы с автовыдачей ещё не настроены.",
|
||||
)
|
||||
header = texts.t("PROMO_GROUPS_INFO_HEADER", "🎯 <b>Промогруппы</b>")
|
||||
message = f"{header}\n\n{empty_text}" if empty_text else header
|
||||
|
||||
await callback.message.edit_text(
|
||||
message,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
total_spent_kopeks = await get_user_total_spent_kopeks(db, db_user.id)
|
||||
total_spent_text = _format_rubles(total_spent_kopeks)
|
||||
|
||||
sorted_groups = sorted(
|
||||
promo_groups,
|
||||
key=lambda group: (group.auto_assign_total_spent_kopeks or 0, group.id),
|
||||
)
|
||||
|
||||
achieved_groups: List[PromoGroup] = [
|
||||
group
|
||||
for group in sorted_groups
|
||||
if (group.auto_assign_total_spent_kopeks or 0) > 0
|
||||
and total_spent_kopeks >= (group.auto_assign_total_spent_kopeks or 0)
|
||||
]
|
||||
|
||||
current_group = next(
|
||||
(group for group in sorted_groups if group.id == db_user.promo_group_id),
|
||||
None,
|
||||
)
|
||||
|
||||
if not current_group and achieved_groups:
|
||||
current_group = achieved_groups[-1]
|
||||
|
||||
next_group = next(
|
||||
(
|
||||
group
|
||||
for group in sorted_groups
|
||||
if (group.auto_assign_total_spent_kopeks or 0) > total_spent_kopeks
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
header = texts.t("PROMO_GROUPS_INFO_HEADER", "🎯 <b>Промогруппы</b>")
|
||||
lines: List[str] = [header, ""]
|
||||
|
||||
spent_line = texts.t(
|
||||
"PROMO_GROUPS_INFO_TOTAL_SPENT",
|
||||
"💰 Потрачено в боте: {amount}",
|
||||
).format(amount=total_spent_text)
|
||||
lines.append(spent_line)
|
||||
|
||||
if current_group:
|
||||
lines.append(
|
||||
texts.t(
|
||||
"PROMO_GROUPS_INFO_CURRENT_LEVEL",
|
||||
"🏆 Текущий уровень: {name}",
|
||||
).format(name=html.escape(current_group.name)),
|
||||
)
|
||||
else:
|
||||
lines.append(
|
||||
texts.t(
|
||||
"PROMO_GROUPS_INFO_NO_LEVEL",
|
||||
"🏆 Текущий уровень: пока не получен",
|
||||
)
|
||||
)
|
||||
|
||||
if next_group:
|
||||
remaining_kopeks = (next_group.auto_assign_total_spent_kopeks or 0) - total_spent_kopeks
|
||||
lines.append(
|
||||
texts.t(
|
||||
"PROMO_GROUPS_INFO_NEXT_LEVEL",
|
||||
"📈 До уровня «{name}»: осталось {amount}",
|
||||
).format(
|
||||
name=html.escape(next_group.name),
|
||||
amount=_format_rubles(max(remaining_kopeks, 0)),
|
||||
)
|
||||
)
|
||||
else:
|
||||
lines.append(
|
||||
texts.t(
|
||||
"PROMO_GROUPS_INFO_MAX_LEVEL",
|
||||
"🏆 Вы уже получили максимальный уровень скидок!",
|
||||
)
|
||||
)
|
||||
|
||||
lines.extend(["", texts.t("PROMO_GROUPS_INFO_LEVELS_HEADER", "📋 Уровни с автовыдачей:")])
|
||||
|
||||
for group in sorted_groups:
|
||||
threshold = group.auto_assign_total_spent_kopeks or 0
|
||||
status_icon = "✅" if total_spent_kopeks >= threshold else "🔒"
|
||||
lines.append(
|
||||
texts.t(
|
||||
"PROMO_GROUPS_INFO_LEVEL_LINE",
|
||||
"{status} <b>{name}</b> — от {amount}",
|
||||
).format(
|
||||
status=status_icon,
|
||||
name=html.escape(group.name),
|
||||
amount=_format_rubles(threshold),
|
||||
)
|
||||
)
|
||||
|
||||
discount_lines = _build_group_discount_lines(group, texts, db_user.language)
|
||||
for discount_line in discount_lines:
|
||||
if discount_line:
|
||||
lines.append(f" {discount_line}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
while lines and not lines[-1]:
|
||||
lines.pop()
|
||||
|
||||
message_text = "\n".join(lines)
|
||||
|
||||
await callback.message.edit_text(
|
||||
message_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
async def show_faq_pages(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
@@ -782,6 +1030,11 @@ def register_handlers(dp: Dispatcher):
|
||||
F.data == "menu_info",
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_promo_groups_info,
|
||||
F.data == "menu_info_promo_groups",
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_faq_pages,
|
||||
F.data == "menu_faq",
|
||||
|
||||
@@ -285,6 +285,7 @@ def get_info_menu_keyboard(
|
||||
show_privacy_policy: bool = False,
|
||||
show_public_offer: bool = False,
|
||||
show_faq: bool = False,
|
||||
show_promo_groups: bool = False,
|
||||
) -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
|
||||
@@ -298,6 +299,14 @@ def get_info_menu_keyboard(
|
||||
)
|
||||
])
|
||||
|
||||
if show_promo_groups:
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=texts.t("MENU_PROMO_GROUPS_INFO", "🎯 Промогруппы"),
|
||||
callback_data="menu_info_promo_groups",
|
||||
)
|
||||
])
|
||||
|
||||
if show_privacy_policy:
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
|
||||
@@ -305,6 +305,16 @@
|
||||
"MENU_INFO": "ℹ️ Info",
|
||||
"MENU_INFO_HEADER": "ℹ️ <b>Info</b>",
|
||||
"MENU_INFO_PROMPT": "Choose a section:",
|
||||
"MENU_PROMO_GROUPS_INFO": "🎯 Promo groups",
|
||||
"PROMO_GROUPS_INFO_HEADER": "🎯 <b>Promo groups</b>",
|
||||
"PROMO_GROUPS_INFO_TOTAL_SPENT": "💰 Spent in the bot: {amount}",
|
||||
"PROMO_GROUPS_INFO_CURRENT_LEVEL": "🏆 Current level: {name}",
|
||||
"PROMO_GROUPS_INFO_NO_LEVEL": "🏆 Current level: not unlocked yet",
|
||||
"PROMO_GROUPS_INFO_NEXT_LEVEL": "📈 Until level “{name}”: spend {amount} more",
|
||||
"PROMO_GROUPS_INFO_MAX_LEVEL": "🏆 You already have the highest discount level!",
|
||||
"PROMO_GROUPS_INFO_LEVELS_HEADER": "📋 Auto-assigned levels:",
|
||||
"PROMO_GROUPS_INFO_LEVEL_LINE": "{status} <b>{name}</b> — from {amount}",
|
||||
"PROMO_GROUPS_INFO_EMPTY": "Auto-assigned promo groups are not configured yet.",
|
||||
"MENU_PROMOCODE": "🎫 Promo code",
|
||||
"MENU_REFERRALS": "🤝 Referral program",
|
||||
"MENU_RULES": "📋 Service rules",
|
||||
|
||||
@@ -234,6 +234,16 @@
|
||||
"MENU_INFO": "ℹ️ Инфо",
|
||||
"MENU_INFO_HEADER": "ℹ️ <b>Инфо</b>",
|
||||
"MENU_INFO_PROMPT": "Выберите раздел:",
|
||||
"MENU_PROMO_GROUPS_INFO": "🎯 Промогруппы",
|
||||
"PROMO_GROUPS_INFO_HEADER": "🎯 <b>Промогруппы</b>",
|
||||
"PROMO_GROUPS_INFO_TOTAL_SPENT": "💰 Потрачено в боте: {amount}",
|
||||
"PROMO_GROUPS_INFO_CURRENT_LEVEL": "🏆 Текущий уровень: {name}",
|
||||
"PROMO_GROUPS_INFO_NO_LEVEL": "🏆 Текущий уровень: пока не получен",
|
||||
"PROMO_GROUPS_INFO_NEXT_LEVEL": "📈 До уровня «{name}»: осталось {amount}",
|
||||
"PROMO_GROUPS_INFO_MAX_LEVEL": "🏆 Вы уже получили максимальный уровень скидок!",
|
||||
"PROMO_GROUPS_INFO_LEVELS_HEADER": "📋 Уровни с автовыдачей:",
|
||||
"PROMO_GROUPS_INFO_LEVEL_LINE": "{status} <b>{name}</b> — от {amount}",
|
||||
"PROMO_GROUPS_INFO_EMPTY": "Промогруппы с автовыдачей ещё не настроены.",
|
||||
"MENU_PRIVACY_POLICY": "🛡️ Политика конф.",
|
||||
"MENU_PUBLIC_OFFER": "📄 Оферта",
|
||||
"PRIVACY_POLICY_HEADER": "🛡️ <b>Политика конфиденциальности</b>",
|
||||
|
||||
@@ -333,6 +333,16 @@
|
||||
"MENU_INFO": "ℹ️ Info",
|
||||
"MENU_INFO_HEADER": "ℹ️ <b>Info</b>",
|
||||
"MENU_INFO_PROMPT": "Choose a section:",
|
||||
"MENU_PROMO_GROUPS_INFO": "🎯 Promo groups",
|
||||
"PROMO_GROUPS_INFO_HEADER": "🎯 <b>Promo groups</b>",
|
||||
"PROMO_GROUPS_INFO_TOTAL_SPENT": "💰 Spent in the bot: {amount}",
|
||||
"PROMO_GROUPS_INFO_CURRENT_LEVEL": "🏆 Current level: {name}",
|
||||
"PROMO_GROUPS_INFO_NO_LEVEL": "🏆 Current level: not unlocked yet",
|
||||
"PROMO_GROUPS_INFO_NEXT_LEVEL": "📈 Until level “{name}”: spend {amount} more",
|
||||
"PROMO_GROUPS_INFO_MAX_LEVEL": "🏆 You already have the highest discount level!",
|
||||
"PROMO_GROUPS_INFO_LEVELS_HEADER": "📋 Auto-assigned levels:",
|
||||
"PROMO_GROUPS_INFO_LEVEL_LINE": "{status} <b>{name}</b> — from {amount}",
|
||||
"PROMO_GROUPS_INFO_EMPTY": "Auto-assigned promo groups are not configured yet.",
|
||||
"MENU_FAQ": "❓ FAQ",
|
||||
"FAQ_HEADER": "❓ <b>FAQ</b>",
|
||||
"FAQ_PAGES_PROMPT": "Select a topic:",
|
||||
|
||||
@@ -246,6 +246,16 @@
|
||||
"MENU_INFO": "ℹ️ Инфо",
|
||||
"MENU_INFO_HEADER": "ℹ️ <b>Инфо</b>",
|
||||
"MENU_INFO_PROMPT": "Выберите раздел:",
|
||||
"MENU_PROMO_GROUPS_INFO": "🎯 Промогруппы",
|
||||
"PROMO_GROUPS_INFO_HEADER": "🎯 <b>Промогруппы</b>",
|
||||
"PROMO_GROUPS_INFO_TOTAL_SPENT": "💰 Потрачено в боте: {amount}",
|
||||
"PROMO_GROUPS_INFO_CURRENT_LEVEL": "🏆 Текущий уровень: {name}",
|
||||
"PROMO_GROUPS_INFO_NO_LEVEL": "🏆 Текущий уровень: пока не получен",
|
||||
"PROMO_GROUPS_INFO_NEXT_LEVEL": "📈 До уровня «{name}»: осталось {amount}",
|
||||
"PROMO_GROUPS_INFO_MAX_LEVEL": "🏆 Вы уже получили максимальный уровень скидок!",
|
||||
"PROMO_GROUPS_INFO_LEVELS_HEADER": "📋 Уровни с автовыдачей:",
|
||||
"PROMO_GROUPS_INFO_LEVEL_LINE": "{status} <b>{name}</b> — от {amount}",
|
||||
"PROMO_GROUPS_INFO_EMPTY": "Промогруппы с автовыдачей ещё не настроены.",
|
||||
"MENU_FAQ": "❓ FAQ",
|
||||
"FAQ_HEADER": "❓ <b>FAQ</b>",
|
||||
"FAQ_PAGES_PROMPT": "Выберите вопрос:",
|
||||
|
||||
Reference in New Issue
Block a user