interest coupons/ picture for rates/ small improvements

This commit is contained in:
Vladless
2026-02-04 23:45:34 +03:00
parent a967352e9a
commit db16c0d49c
10 changed files with 575 additions and 76 deletions
+44 -3
View File
@@ -11,10 +11,13 @@ from logger import logger
async def create_coupon(
session: AsyncSession,
code: str,
amount: int,
amount: int | None,
usage_limit: int,
days: int = None,
days: int | None = None,
new_users_only: bool = False,
percent: int | None = None,
max_discount_amount: int | None = None,
min_order_amount: int | None = None,
) -> bool:
try:
exists = await session.scalar(select(Coupon.id).where(Coupon.code == code))
@@ -22,15 +25,33 @@ async def create_coupon(
logger.warning(f"[Coupon] ⚠️ Купон с кодом {code} уже существует.")
return False
if percent is not None:
try:
percent_value = int(percent)
except (TypeError, ValueError):
logger.warning(f"[Coupon] ⚠️ Некорректный процент для купона {code}.")
return False
if percent_value <= 0 or percent_value > 100:
logger.warning(f"[Coupon] ⚠️ процент должен быть в диапазоне 1..100 для купона {code}.")
return False
if (amount or 0) > 0 or (days or 0) > 0:
logger.warning(f"[Coupon] ⚠️ Купон {code} не может одновременно иметь percent и amount/days.")
return False
await session.execute(
insert(Coupon).values(
code=code,
amount=amount,
amount=int(amount) if amount is not None else 0,
usage_limit=usage_limit,
usage_count=0,
is_used=False,
days=days,
new_users_only=new_users_only,
percent=percent,
max_discount_amount=max_discount_amount,
min_order_amount=min_order_amount,
)
)
await session.commit()
@@ -115,3 +136,23 @@ async def update_coupon_usage_count(session: AsyncSession, coupon_id: int):
except SQLAlchemyError as e:
logger.error(f"❌ Ошибка при обновлении купона {coupon_id}: {e}")
await session.rollback()
def apply_percent_coupon(price_rub: int, coupon: Coupon) -> tuple[int, int]:
percent = coupon.percent
if percent is None:
return price_rub, 0
if coupon.min_order_amount is not None and price_rub < int(coupon.min_order_amount):
return price_rub, 0
discount = (price_rub * int(percent)) // 100
if coupon.max_discount_amount is not None:
discount = min(discount, int(coupon.max_discount_amount))
final_price = price_rub - discount
if final_price < 0:
final_price = 0
return final_price, discount
+5 -1
View File
@@ -16,7 +16,7 @@ from sqlalchemy import (
String,
Text,
UniqueConstraint,
text
text,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, declarative_base, mapped_column, relationship
@@ -186,6 +186,10 @@ class Coupon(DictLikeMixin, Base):
days = Column(Integer, nullable=True)
new_users_only = Column(Boolean, nullable=False, server_default=text("false"))
percent = Column(Integer, nullable=True)
max_discount_amount = Column(Integer, nullable=True)
min_order_amount = Column(Integer, nullable=True)
class CouponUsage(DictLikeMixin, Base):
__tablename__ = "coupon_usages"
+133 -25
View File
@@ -28,7 +28,6 @@ from .keyboard import (
format_coupons_list,
)
router = Router()
@@ -37,6 +36,7 @@ class AdminCouponsState(StatesGroup):
waiting_for_coupon_audience = State()
waiting_for_balance_data = State()
waiting_for_days_data = State()
waiting_for_percent_data = State()
@router.callback_query(
@@ -56,6 +56,7 @@ async def handle_coupons_create(callback_query: CallbackQuery, state: FSMContext
kb = InlineKeyboardBuilder()
kb.button(text="💰 Баланс", callback_data="coupon_type_balance")
kb.button(text="⏳ Время", callback_data="coupon_type_days")
kb.button(text="📉 Процент", callback_data="coupon_type_percent")
kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
kb.adjust(1)
@@ -87,11 +88,16 @@ async def handle_days_coupon_selection(callback_query: CallbackQuery, state: FSM
await show_coupon_audience_step(callback_query, state, "days")
@router.callback_query(F.data == "coupon_type_percent", IsAdminFilter())
async def handle_percent_coupon_selection(callback_query: CallbackQuery, state: FSMContext):
await show_coupon_audience_step(callback_query, state, "percent")
@router.callback_query(F.data.in_(("coupon_audience_all", "coupon_audience_new")), IsAdminFilter())
async def handle_coupon_audience(callback_query: CallbackQuery, state: FSMContext):
data = await state.get_data()
coupon_type = data.get("coupon_type")
if coupon_type not in ("balance", "days"):
if coupon_type not in ("balance", "days", "percent"):
await callback_query.answer("Ошибка: тип купона не найден", show_alert=True)
return
@@ -105,19 +111,30 @@ async def handle_coupon_audience(callback_query: CallbackQuery, state: FSMContex
text = (
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
"📝 <i>код</i> 💰 <i>сумма</i> 🔢 <i>лимит</i>\n\n"
"Пример: <b>'COUPON1 50 5'</b> 👈\n\n"
"Пример: <b>'COUPON1 50 5'</b>\n\n"
)
await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup())
await state.set_state(AdminCouponsState.waiting_for_balance_data)
return
if coupon_type == "days":
text = (
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
"📝 <i>код</i> ⏳ <i>дни</i> 🔢 <i>лимит</i>\n\n"
"Пример: <b>'DAYS10 10 50'</b> 👈\n\n"
"Пример: <b>'DAYS10 10 50'</b>\n\n"
)
await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup())
await state.set_state(AdminCouponsState.waiting_for_days_data)
return
text = (
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
"📝 <i>код</i> 📉 <i>процент</i> 🔢 <i>лимит</i>\n\n"
"Пример: <b>'SALE20 20 10'</b>\n"
"Где 20 — это скидка 20%\n\n"
)
await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup())
await state.set_state(AdminCouponsState.waiting_for_percent_data)
@router.message(AdminCouponsState.waiting_for_balance_data, IsAdminFilter())
@@ -131,9 +148,9 @@ async def handle_balance_coupon_input(message: Message, state: FSMContext, sessi
if len(parts) != 3:
text = (
"❌ <b>Некорректный формат!</b> 📝 Пожалуйста, введите данные в формате:\n"
"❌ <b>Некорректный формат!</b>\n"
"🏷️ <b>код</b> 💰 <b>сумма</b> 🔢 <b>лимит</b>\n"
"Пример: <b>'COUPON1 50 5'</b> 👈"
"Пример: <b>'COUPON1 50 5'</b>"
)
await message.answer(text=text, reply_markup=kb.as_markup())
return
@@ -143,9 +160,11 @@ async def handle_balance_coupon_input(message: Message, state: FSMContext, sessi
coupon_amount = int(parts[1])
usage_limit = int(parts[2])
if coupon_amount <= 0:
raise ValueError("Сумма должна быть больше 0")
raise ValueError
if usage_limit <= 0:
raise ValueError
except ValueError:
text = "⚠️ <b>Проверьте правильность введенных данных!</b>\n💱 Сумма должна быть числом, а лимит — целым числом."
text = "⚠️ <b>Проверьте данные!</b>\nСумма и лимит должны быть целыми числами больше 0."
await message.answer(text=text, reply_markup=kb.as_markup())
return
@@ -160,6 +179,7 @@ async def handle_balance_coupon_input(message: Message, state: FSMContext, sessi
usage_limit,
days=None,
new_users_only=new_users_only,
percent=None,
)
if not ok:
await message.answer("❌ Купон с таким кодом уже существует.", reply_markup=kb.as_markup())
@@ -169,9 +189,9 @@ async def handle_balance_coupon_input(message: Message, state: FSMContext, sessi
audience_txt = "🆕 Только новым" if new_users_only else "👤 Всем"
text = (
f"✅ Купон с кодом <b>{coupon_code}</b> успешно создан!\n"
f"✅ Купон <b>{coupon_code}</b> создан!\n"
f"💰 Сумма: <b>{coupon_amount} рублей</b>\n"
f"🔢 Лимит использования: <b>{usage_limit} раз</b>\n"
f"🔢 Лимит: <b>{usage_limit} раз</b>\n"
f"🎯 Доступ: <b>{audience_txt}</b>\n"
f"🔗 <b>Ссылка:</b> <code>{coupon_link}</code>\n"
)
@@ -184,7 +204,6 @@ async def handle_balance_coupon_input(message: Message, state: FSMContext, sessi
await message.answer(text=text, reply_markup=kb.as_markup())
await state.clear()
except Exception as e:
logger.error(f"Ошибка при создании купона: {e}")
await message.answer("❌ Произошла ошибка при создании купона.", reply_markup=kb.as_markup())
@@ -201,9 +220,7 @@ async def handle_days_coupon_input(message: Message, state: FSMContext, session:
if len(parts) != 3:
text = (
"❌ <b>Некорректный формат!</b> 📝 Пожалуйста, введите данные в формате:\n"
"🏷️ <b>код</b> ⏳ <i>дни</i> 🔢 <b>лимит</b>\n"
"Пример: <b>'DAYS10 10 50'</b> 👈"
"❌ <b>Некорректный формат!</b>\n🏷️ <b>код</b> ⏳ <b>дни</b> 🔢 <b>лимит</b>\nПример: <b>'DAYS10 10 50'</b>"
)
await message.answer(text=text, reply_markup=kb.as_markup())
return
@@ -213,9 +230,11 @@ async def handle_days_coupon_input(message: Message, state: FSMContext, session:
days = int(parts[1])
usage_limit = int(parts[2])
if days <= 0:
raise ValueError("Количество дней должно быть больше 0")
raise ValueError
if usage_limit <= 0:
raise ValueError
except ValueError:
text = "⚠️ <b>Проверьте правильность введенных данных!</b>\n💱 Дни должны быть числом, а лимит — целым числом."
text = "⚠️ <b>Проверьте данные!</b>\nДни и лимит должны быть целыми числами больше 0."
await message.answer(text=text, reply_markup=kb.as_markup())
return
@@ -230,6 +249,7 @@ async def handle_days_coupon_input(message: Message, state: FSMContext, session:
usage_limit,
days=days,
new_users_only=new_users_only,
percent=None,
)
if not ok:
await message.answer("❌ Купон с таким кодом уже существует.", reply_markup=kb.as_markup())
@@ -239,9 +259,9 @@ async def handle_days_coupon_input(message: Message, state: FSMContext, session:
audience_txt = "🆕 Только новым" if new_users_only else "👤 Всем"
text = (
f"✅ Купон с кодом <b>{coupon_code}</b> успешно создан!\n"
f"✅ Купон <b>{coupon_code}</b> создан!\n"
f"⏳ <b>{format_days(days)}</b>\n"
f"🔢 Лимит использования: <b>{usage_limit} раз</b>\n"
f"🔢 Лимит: <b>{usage_limit} раз</b>\n"
f"🎯 Доступ: <b>{audience_txt}</b>\n"
f"🔗 <b>Ссылка:</b> <code>{coupon_link}</code>\n"
)
@@ -254,7 +274,74 @@ async def handle_days_coupon_input(message: Message, state: FSMContext, session:
await message.answer(text=text, reply_markup=kb.as_markup())
await state.clear()
except Exception as e:
logger.error(f"Ошибка при создании купона: {e}")
await message.answer("❌ Произошла ошибка при создании купона.", reply_markup=kb.as_markup())
@router.message(AdminCouponsState.waiting_for_percent_data, IsAdminFilter())
async def handle_percent_coupon_input(message: Message, state: FSMContext, session: Any):
text = message.text.strip()
parts = text.split()
kb = InlineKeyboardBuilder()
kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
kb.adjust(1)
if len(parts) != 3:
text = (
"❌ <b>Некорректный формат!</b>\n"
"🏷️ <b>код</b> 📉 <b>процент</b> 🔢 <b>лимит</b>\n"
"Пример: <b>'SALE20 20 10'</b>"
)
await message.answer(text=text, reply_markup=kb.as_markup())
return
try:
coupon_code = parts[0]
percent = int(parts[1])
usage_limit = int(parts[2])
if percent <= 0 or percent > 100:
raise ValueError
if usage_limit <= 0:
raise ValueError
except ValueError:
text = "⚠️ <b>Проверьте данные!</b>\nПроцент должен быть 1..100, лимит — целое число больше 0."
await message.answer(text=text, reply_markup=kb.as_markup())
return
try:
data = await state.get_data()
new_users_only = bool(data.get("new_users_only"))
ok = await create_coupon(
session,
coupon_code,
0,
usage_limit,
days=None,
new_users_only=new_users_only,
percent=percent,
)
if not ok:
await message.answer("❌ Купон с таким кодом уже существует.", reply_markup=kb.as_markup())
return
audience_txt = "🆕 Только новым" if new_users_only else "👤 Всем"
text = (
f"✅ Купон <b>{coupon_code}</b> создан!\n"
f"📉 Скидка: <b>{percent}%</b>\n"
f"🔢 Лимит: <b>{usage_limit} раз</b>\n"
f"🎯 Доступ: <b>{audience_txt}</b>\n"
)
kb = InlineKeyboardBuilder()
kb.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
kb.adjust(1)
await message.answer(text=text, reply_markup=kb.as_markup())
await state.clear()
except Exception as e:
logger.error(f"Ошибка при создании купона: {e}")
await message.answer("❌ Произошла ошибка при создании купона.", reply_markup=kb.as_markup())
@@ -350,7 +437,6 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any):
return
coupon_code = inline_query.query.split("coupon_")[1]
coupon_link = f"https://t.me/{USERNAME_BOT}?start=coupons_{coupon_code}"
coupons = await get_all_coupons(session, page=1, per_page=10)
coupon = next((c for c in coupons["coupons"] if c["code"] == coupon_code), None)
@@ -364,17 +450,39 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any):
)
return
title = f"Купон {coupon['code']}"
description = (
f"Получи {coupon['amount']} рублей!"
if coupon["amount"] > 0
else f"Продли подписку на {format_days(coupon['days'])}!"
percent_value = coupon.get("percent")
if percent_value is not None and int(percent_value) > 0:
await inline_query.answer(
results=[],
switch_pm_text="Процентные купоны не публикуются ссылкой",
switch_pm_parameter="coupons",
cache_time=1,
)
return
coupon_link = f"https://t.me/{USERNAME_BOT}?start=coupons_{coupon_code}"
title = f"Купон {coupon['code']}"
days_value = coupon.get("days")
amount_value = coupon.get("amount") or 0
if days_value is not None and int(days_value) > 0:
days_int = int(days_value)
description = f"Продли подписку на {format_days(days_int)}!"
message_text = (
f"🎫 <b>Купон:</b> {coupon['code']}\n"
f"{'💰 <b>Бонус:</b> ' + str(coupon['amount']) + ' рублей' if coupon['amount'] > 0 else '⏳ <b>Продление:</b> ' + format_days(coupon['days'])}\n"
f"⏳ <b>Продление:</b> {format_days(days_int)}\n"
f"👇 Нажми, чтобы активировать!"
)
elif int(amount_value) > 0:
amount_int = int(amount_value)
description = f"Получи {amount_int} рублей!"
message_text = (
f"🎫 <b>Купон:</b> {coupon['code']}\n💰 <b>Бонус:</b> {amount_int} рублей\n👇 Нажми, чтобы активировать!"
)
else:
description = "Купон"
message_text = f"🎫 <b>Купон:</b> {coupon['code']}\n👇 Нажми, чтобы активировать!"
builder = InlineKeyboardBuilder()
builder.button(text="Активировать купон", url=coupon_link)
+26 -14
View File
@@ -67,18 +67,30 @@ def build_coupons_list_kb(coupons: list, current_page: int, total_pages: int) ->
def format_coupons_list(coupons: list, username_bot: str) -> str:
coupon_list = "📜 Список всех купонов:\n\n"
for coupon in coupons:
value_text = (
f"💰 <b>Сумма:</b> {coupon['amount']} рублей"
if coupon["amount"] > 0
else f"⏳ <b>{format_days(coupon['days'])}</b>"
text = "📜 <b>Список купонов</b>\n\n"
for i, coupon in enumerate(coupons, start=1):
percent_value = coupon.get("percent")
days_value = coupon.get("days")
amount_value = coupon.get("amount") or 0
if percent_value is not None and int(percent_value) > 0:
value_line = f"📉 <b>Скидка:</b> {int(percent_value)}%"
elif days_value is not None and int(days_value) > 0:
value_line = f"⏳ <b>Продление:</b> {format_days(int(days_value))}"
elif int(amount_value) > 0:
value_line = f"💰 <b>Баланс:</b> {int(amount_value)}"
else:
value_line = ""
text += (
f"<blockquote>"
f"<b>{i}. {coupon['code']}</b>\n"
f"{value_line}\n"
f"🔢 <b>Лимит:</b> {coupon['usage_limit']} | "
f"✅ <b>Использовано:</b> {coupon['usage_count']}\n"
f"<code>https://t.me/{username_bot}?start=coupons_{coupon['code']}</code>"
f"</blockquote>\n\n"
)
coupon_list += (
f"🏷️ <b>Код:</b> {coupon['code']}\n"
f"{value_text}\n"
f"🔢 <b>Лимит использования:</b> {coupon['usage_limit']} раз\n"
f"✅ <b>Использовано:</b> {coupon['usage_count']} раз\n"
f"🔗 <b>Ссылка:</b> <code>https://t.me/{username_bot}?start=coupons_{coupon['code']}</code>\n\n"
)
return coupon_list
return text
+17 -8
View File
@@ -1,4 +1,5 @@
from datetime import datetime
from aiogram.exceptions import TelegramBadRequest
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
@@ -464,16 +465,20 @@ async def handle_cfg_renew_devices(callback_query: CallbackQuery, state: FSMCont
else (f"{int(selected_traffic_gb)} ГБ" if selected_traffic_gb is not None else "")
)
await callback_query.message.edit_text(
text=(
text = (
"🧩 <b>Выбор конфигурации тарифа</b>\n\n"
f"📦 <b>Тариф:</b> {tariff.get('name', '')}\n"
f"📱 <b>Устройства:</b> {devices_label}\n"
f"📊 <b>Трафик:</b> {traffic_label}\n\n"
"Выберите параметры и нажмите «✅ Применить»."
),
reply_markup=builder.as_markup(),
)
try:
await callback_query.message.edit_text(text=text, reply_markup=builder.as_markup())
except TelegramBadRequest as e:
if "message is not modified" not in str(e):
raise
await callback_query.answer()
@@ -577,16 +582,20 @@ async def handle_cfg_renew_traffic(callback_query: CallbackQuery, state: FSMCont
)
traffic_label = "Безлимит трафика" if selected_traffic_gb <= 0 else f"{selected_traffic_gb} ГБ"
await callback_query.message.edit_text(
text=(
text = (
"🧩 <b>Выбор конфигурации тарифа</b>\n\n"
f"📦 <b>Тариф:</b> {tariff.get('name', '')}\n"
f"📱 <b>Устройства:</b> {devices_label}\n"
f"📊 <b>Трафик:</b> {traffic_label}\n\n"
"Выберите параметры и нажмите «✅ Применить»."
),
reply_markup=builder.as_markup(),
)
try:
await callback_query.message.edit_text(text=text, reply_markup=builder.as_markup())
except TelegramBadRequest as e:
if "message is not modified" not in str(e):
raise
await callback_query.answer()
+4
View File
@@ -1,3 +1,4 @@
import os
from collections import defaultdict
from datetime import datetime, timedelta
from typing import Any
@@ -296,6 +297,9 @@ async def handle_key_creation(
target_message=target_message,
text=SELECT_TARIFF_PLAN_MSG + discount_message,
reply_markup=builder.as_markup(),
media_path=os.path.join("img", "tariffs.jpg"),
disable_web_page_preview=False,
force_text=True,
)
await state.update_data(
+1
View File
@@ -168,6 +168,7 @@ async def build_keys_response(records: list[Key] | None, session: AsyncSession,
if getattr(record, "tariff_id", None):
try:
from handlers.tariffs.tariff_display import resolve_vless_enabled
is_vless = await resolve_vless_enabled(session, int(record.tariff_id))
except Exception:
is_vless = False
+328 -8
View File
@@ -1,14 +1,25 @@
from typing import Any
from math import ceil
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy import select
from config import USE_NEW_PAYMENT_FLOW, TRIBUTE_LINK
from core.bootstrap import PAYMENTS_CONFIG
from core.settings.money_config import get_currency_mode
from database import (
check_coupon_usage,
create_coupon_usage,
get_balance,
get_coupon_by_code,
update_coupon_usage_count,
)
from database.coupons import apply_percent_coupon
from database.models import CouponUsage
from database.temporary_data import create_temporary_data
from handlers import buttons as btn
from handlers.payments.currency_flow import (
@@ -17,7 +28,7 @@ from handlers.payments.currency_flow import (
currency_label,
)
from handlers.payments.providers import get_providers_with_hooks
from handlers.texts import FAST_PAY_CHOOSE_CURRENCY, FAST_PAY_CHOOSE_PROVIDER
from handlers.texts import FAST_PAY_CHOOSE_CURRENCY, FAST_PAY_CHOOSE_PROVIDER, FASTFLOW_COUPON_APPLIED_TEMPLATE
from handlers.utils import edit_or_send_message
from logger import logger
@@ -25,6 +36,10 @@ from logger import logger
router = Router()
class FastFlowCouponState(StatesGroup):
waiting_for_coupon_code = State()
async def get_payment_providers_config() -> dict[str, bool]:
config = PAYMENTS_CONFIG or {}
return dict(config)
@@ -130,10 +145,30 @@ async def try_fast_payment_flow(
if not providers and not show_tribute:
return False
keyboard = build_currency_choice_kb(
show_stars=show_stars,
show_tribute=show_tribute,
)
keyboard_original = build_currency_choice_kb(show_stars=show_stars, show_tribute=show_tribute)
rows = keyboard_original.export()
profile_row = None
kept_rows: list[list[InlineKeyboardButton]] = []
for row in rows:
if any(getattr(button, "callback_data", None) == "profile" for button in row):
if profile_row is None:
profile_row = row
continue
kept_rows.append(row)
keyboard = InlineKeyboardBuilder()
for row in kept_rows:
keyboard.row(*row)
keyboard.row(InlineKeyboardButton(text=btn.COUPON, callback_data="fastflow_coupon"))
if profile_row:
keyboard.row(*profile_row)
else:
keyboard.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
lead_text = await shortfall_lead_text(
session,
tg_id,
@@ -161,6 +196,7 @@ async def try_fast_payment_flow(
currency = cfg.get("currency")
if currency:
await state.update_data(chosen_currency=currency)
await state.update_data(temp_key=temp_key, temp_payload=temp_payload, required_amount=required_amount)
if await _run_provider_flow(single_provider, callback_query, session, state, required_amount):
return True
return False
@@ -188,6 +224,7 @@ async def try_fast_payment_flow(
)
)
keyboard.row(InlineKeyboardButton(text=btn.COUPON, callback_data="fastflow_coupon"))
keyboard.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
lead_text = await shortfall_lead_text(
@@ -209,6 +246,288 @@ async def try_fast_payment_flow(
return True
@router.callback_query(F.data == "fastflow_coupon_back")
async def fastflow_coupon_back(callback_query: CallbackQuery, state: FSMContext, session: Any):
amount_not_found_text = "Сумма не найдена"
data = await state.get_data()
temp_key = data.get("temp_key")
temp_payload = data.get("temp_payload")
required_amount = data.get("required_amount")
await state.set_state(None)
if not temp_key or not isinstance(temp_payload, dict) or required_amount is None:
await edit_or_send_message(
target_message=callback_query.message,
text=amount_not_found_text,
reply_markup=InlineKeyboardBuilder()
.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
.as_markup(),
)
await callback_query.answer()
return
await try_fast_payment_flow(
callback_query,
session,
state,
tg_id=callback_query.from_user.id,
temp_key=str(temp_key),
temp_payload=dict(temp_payload),
required_amount=int(required_amount),
)
await callback_query.answer()
@router.callback_query(F.data == "fastflow_coupon")
async def fastflow_coupon(callback_query: CallbackQuery, state: FSMContext):
input_text = "Введите купон:"
amount_not_found_text = "Сумма не найдена"
data = await state.get_data()
if data.get("required_amount") is None:
await callback_query.answer(amount_not_found_text, show_alert=True)
return
await state.set_state(FastFlowCouponState.waiting_for_coupon_code)
await edit_or_send_message(
target_message=callback_query.message,
text=input_text,
reply_markup=InlineKeyboardBuilder()
.row(InlineKeyboardButton(text=btn.BACK, callback_data="fastflow_coupon_back"))
.as_markup(),
)
@router.message(FastFlowCouponState.waiting_for_coupon_code)
async def fastflow_apply_coupon(message: Message, state: FSMContext, session: Any):
input_text = "Введите купон:"
amount_not_found_text = "Сумма не найдена"
not_found_text = "Купон не найден"
exhausted_text = "Купон исчерпан"
already_used_text = "Вы уже использовали этот купон"
new_users_only_text = "Купон доступен только новым пользователям"
not_applicable_text = "Купон не применим к текущей сумме"
no_methods_text = "Нет доступных способов оплаты"
back_markup = (
InlineKeyboardBuilder()
.row(InlineKeyboardButton(text=btn.BACK, callback_data="fastflow_coupon_back"))
.as_markup()
)
code = (message.text or "").strip()
if not code:
await message.answer(input_text, reply_markup=back_markup)
return
data = await state.get_data()
required_amount = data.get("required_amount")
if required_amount is None:
await state.set_state(None)
await message.answer(amount_not_found_text, reply_markup=back_markup)
return
temp_key = data.get("temp_key")
temp_payload = data.get("temp_payload")
if not temp_key or not isinstance(temp_payload, dict):
await state.set_state(None)
await message.answer(amount_not_found_text, reply_markup=back_markup)
return
coupon = await get_coupon_by_code(session, code)
if not coupon:
await message.answer(not_found_text, reply_markup=back_markup)
return
if bool(getattr(coupon, "is_used", False)):
await message.answer(exhausted_text, reply_markup=back_markup)
return
usage_count = getattr(coupon, "usage_count", None)
usage_limit = getattr(coupon, "usage_limit", None)
if usage_count is not None and usage_limit is not None and int(usage_count) >= int(usage_limit):
await message.answer(exhausted_text, reply_markup=back_markup)
return
if await check_coupon_usage(session, coupon.id, message.from_user.id):
await message.answer(already_used_text, reply_markup=back_markup)
return
if bool(getattr(coupon, "new_users_only", False)):
used_any = await session.scalar(
select(CouponUsage.id).where(CouponUsage.user_id == message.from_user.id).limit(1)
)
if used_any:
await message.answer(new_users_only_text, reply_markup=back_markup)
return
balance_now = await get_balance(session, message.from_user.id)
base_price_raw = temp_payload.get("selected_price_rub")
if base_price_raw is None:
base_price_raw = temp_payload.get("cost")
if base_price_raw is None:
try:
base_price_raw = int(float(balance_now) + float(required_amount))
except Exception:
base_price_raw = required_amount
try:
base_price = int(base_price_raw)
except (TypeError, ValueError):
base_price = int(required_amount)
new_price, discount = apply_percent_coupon(int(base_price), coupon)
if int(discount) <= 0:
await message.answer(not_applicable_text, reply_markup=back_markup)
return
required_amount_new = int(max(0, ceil(float(new_price) - float(balance_now))))
await create_coupon_usage(session, coupon.id, message.from_user.id)
await update_coupon_usage_count(session, coupon.id)
percent_value = int(getattr(coupon, "percent", 0) or 0)
temp_payload_updated = dict(temp_payload)
temp_payload_updated["required_amount"] = int(required_amount_new)
if "selected_price_rub" in temp_payload_updated:
temp_payload_updated["selected_price_rub"] = int(new_price)
if "cost" in temp_payload_updated:
temp_payload_updated["cost"] = int(new_price)
await create_temporary_data(session, message.from_user.id, str(temp_key), temp_payload_updated)
await state.update_data(
required_amount=int(required_amount_new),
temp_payload=temp_payload_updated,
applied_coupon={
"code": code,
"percent": percent_value,
"discount": int(discount),
"old_price": int(base_price),
"new_price": int(new_price),
},
)
await state.set_state(None)
payment_config = await get_payment_providers_config()
providers_map = await get_providers_with_hooks(payment_config)
configured = [str(p) for p in (USE_NEW_PAYMENT_FLOW or [])]
configured_upper = [p.upper() for p in configured]
configured_set = set(configured_upper)
providers: list[str] = []
for p_up in configured_upper:
cfg = providers_map.get(p_up) or {}
if cfg.get("fast") and cfg.get("enabled", True):
providers.append(p_up)
mode, one_screen = get_currency_mode()
multicurrency_mode = mode == "RUB+USD"
if not multicurrency_mode:
allowed_currency = "RUB" if mode == "RUB" else "USD"
filtered: list[str] = []
for p_up in providers:
cfg = providers_map.get(p_up) or {}
curr = str(cfg.get("currency") or "").upper()
if curr in (allowed_currency, "RUB+USD"):
filtered.append(p_up)
providers = filtered
tribute_cfg = providers_map.get("TRIBUTE") or {}
tribute_link = (TRIBUTE_LINK or "").strip()
tribute_enabled = "TRIBUTE" in configured_set and tribute_cfg.get("enabled", True) and bool(tribute_link)
stars_cfg = providers_map.get("STARS") or {}
stars_enabled_for_fast = "STARS" in configured_set and stars_cfg.get("fast") and stars_cfg.get("enabled", True)
lead_text = await shortfall_lead_text(
session,
message.from_user.id,
int(required_amount_new),
getattr(message.from_user, "language_code", None),
)
coupon_text = FASTFLOW_COUPON_APPLIED_TEMPLATE.format(
code=code,
percent=percent_value,
old_price=int(base_price),
discount=int(discount),
new_price=int(new_price),
)
if multicurrency_mode and not one_screen:
keyboard_original = build_currency_choice_kb(show_stars=stars_enabled_for_fast, show_tribute=tribute_enabled)
rows = keyboard_original.export()
profile_row = None
kept_rows: list[list[InlineKeyboardButton]] = []
for row in rows:
if any(getattr(button, "callback_data", None) == "profile" for button in row):
if profile_row is None:
profile_row = row
continue
kept_rows.append(row)
keyboard = InlineKeyboardBuilder()
for row in kept_rows:
keyboard.row(*row)
keyboard.row(InlineKeyboardButton(text=btn.COUPON_RESTART, callback_data="fastflow_coupon"))
if profile_row:
keyboard.row(*profile_row)
else:
keyboard.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
await message.answer(
f"{lead_text}\n\n{coupon_text}\n\n{FAST_PAY_CHOOSE_CURRENCY}",
reply_markup=keyboard.as_markup(),
)
return
if not providers and not tribute_enabled:
await message.answer(no_methods_text)
return
keyboard = InlineKeyboardBuilder()
for provider_upper in providers:
button_text = getattr(btn, provider_upper, provider_upper)
if one_screen:
cfg = providers_map.get(provider_upper) or {}
curr = cfg.get("currency")
if curr and curr != "RUB+USD":
button_text = f"{button_text} ({currency_label(curr)})"
keyboard.row(
InlineKeyboardButton(
text=button_text,
callback_data=f"choose_payment_provider|{provider_upper}",
)
)
if tribute_enabled:
keyboard.row(
InlineKeyboardButton(
text=getattr(btn, "TRIBUTE", "TRIBUTE"),
url=tribute_link,
)
)
keyboard.row(InlineKeyboardButton(text=btn.COUPON_RESTART, callback_data="fastflow_coupon"))
keyboard.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
await message.answer(
f"{lead_text}\n\n{coupon_text}\n\n{FAST_PAY_CHOOSE_PROVIDER}",
reply_markup=keyboard.as_markup(),
)
@router.callback_query(F.data.startswith("choose_payment_currency|"))
async def choose_payment_currency(callback_query: CallbackQuery, state: FSMContext, session: Any):
payment_config = await get_payment_providers_config()
@@ -232,7 +551,7 @@ async def choose_payment_currency(callback_query: CallbackQuery, state: FSMConte
await state.update_data(chosen_currency=currency)
if not filtered:
keyboard = InlineKeyboardBuilder().row(InlineKeyboardButton(text="← Назад", callback_data="profile"))
keyboard = InlineKeyboardBuilder().row(InlineKeyboardButton(text=btn.BACK, callback_data="profile"))
await edit_or_send_message(
target_message=callback_query.message,
text="Для выбранной валюты нет доступных касс. Выберите другую валюту или вернитесь в меню.",
@@ -263,6 +582,7 @@ async def choose_payment_currency(callback_query: CallbackQuery, state: FSMConte
)
)
keyboard.row(InlineKeyboardButton(text=btn.COUPON, callback_data="fastflow_coupon"))
keyboard.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
lead_text = await shortfall_lead_text(
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

+1 -1
View File
@@ -92,4 +92,4 @@ def get_git_commit_number() -> str:
def get_version() -> str:
return f"v.5.1-b23012603 {get_git_commit_number()}"
return f"v.5.1-preRelease {get_git_commit_number()}"