interest coupons/ picture for rates/ small improvements
This commit is contained in:
+44
-3
@@ -11,10 +11,13 @@ from logger import logger
|
|||||||
async def create_coupon(
|
async def create_coupon(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
code: str,
|
code: str,
|
||||||
amount: int,
|
amount: int | None,
|
||||||
usage_limit: int,
|
usage_limit: int,
|
||||||
days: int = None,
|
days: int | None = None,
|
||||||
new_users_only: bool = False,
|
new_users_only: bool = False,
|
||||||
|
percent: int | None = None,
|
||||||
|
max_discount_amount: int | None = None,
|
||||||
|
min_order_amount: int | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
try:
|
try:
|
||||||
exists = await session.scalar(select(Coupon.id).where(Coupon.code == code))
|
exists = await session.scalar(select(Coupon.id).where(Coupon.code == code))
|
||||||
@@ -22,15 +25,33 @@ async def create_coupon(
|
|||||||
logger.warning(f"[Coupon] ⚠️ Купон с кодом {code} уже существует.")
|
logger.warning(f"[Coupon] ⚠️ Купон с кодом {code} уже существует.")
|
||||||
return False
|
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(
|
await session.execute(
|
||||||
insert(Coupon).values(
|
insert(Coupon).values(
|
||||||
code=code,
|
code=code,
|
||||||
amount=amount,
|
amount=int(amount) if amount is not None else 0,
|
||||||
usage_limit=usage_limit,
|
usage_limit=usage_limit,
|
||||||
usage_count=0,
|
usage_count=0,
|
||||||
is_used=False,
|
is_used=False,
|
||||||
days=days,
|
days=days,
|
||||||
new_users_only=new_users_only,
|
new_users_only=new_users_only,
|
||||||
|
percent=percent,
|
||||||
|
max_discount_amount=max_discount_amount,
|
||||||
|
min_order_amount=min_order_amount,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
@@ -115,3 +136,23 @@ async def update_coupon_usage_count(session: AsyncSession, coupon_id: int):
|
|||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
logger.error(f"❌ Ошибка при обновлении купона {coupon_id}: {e}")
|
logger.error(f"❌ Ошибка при обновлении купона {coupon_id}: {e}")
|
||||||
await session.rollback()
|
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
@@ -16,7 +16,7 @@ from sqlalchemy import (
|
|||||||
String,
|
String,
|
||||||
Text,
|
Text,
|
||||||
UniqueConstraint,
|
UniqueConstraint,
|
||||||
text
|
text,
|
||||||
)
|
)
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
from sqlalchemy.orm import Mapped, declarative_base, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, declarative_base, mapped_column, relationship
|
||||||
@@ -186,6 +186,10 @@ class Coupon(DictLikeMixin, Base):
|
|||||||
days = Column(Integer, nullable=True)
|
days = Column(Integer, nullable=True)
|
||||||
new_users_only = Column(Boolean, nullable=False, server_default=text("false"))
|
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):
|
class CouponUsage(DictLikeMixin, Base):
|
||||||
__tablename__ = "coupon_usages"
|
__tablename__ = "coupon_usages"
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ from .keyboard import (
|
|||||||
format_coupons_list,
|
format_coupons_list,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@@ -37,6 +36,7 @@ class AdminCouponsState(StatesGroup):
|
|||||||
waiting_for_coupon_audience = State()
|
waiting_for_coupon_audience = State()
|
||||||
waiting_for_balance_data = State()
|
waiting_for_balance_data = State()
|
||||||
waiting_for_days_data = State()
|
waiting_for_days_data = State()
|
||||||
|
waiting_for_percent_data = State()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(
|
@router.callback_query(
|
||||||
@@ -56,6 +56,7 @@ async def handle_coupons_create(callback_query: CallbackQuery, state: FSMContext
|
|||||||
kb = InlineKeyboardBuilder()
|
kb = InlineKeyboardBuilder()
|
||||||
kb.button(text="💰 Баланс", callback_data="coupon_type_balance")
|
kb.button(text="💰 Баланс", callback_data="coupon_type_balance")
|
||||||
kb.button(text="⏳ Время", callback_data="coupon_type_days")
|
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.button(text=BACK, callback_data=AdminPanelCallback(action="coupons").pack())
|
||||||
kb.adjust(1)
|
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")
|
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())
|
@router.callback_query(F.data.in_(("coupon_audience_all", "coupon_audience_new")), IsAdminFilter())
|
||||||
async def handle_coupon_audience(callback_query: CallbackQuery, state: FSMContext):
|
async def handle_coupon_audience(callback_query: CallbackQuery, state: FSMContext):
|
||||||
data = await state.get_data()
|
data = await state.get_data()
|
||||||
coupon_type = data.get("coupon_type")
|
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)
|
await callback_query.answer("Ошибка: тип купона не найден", show_alert=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -105,19 +111,30 @@ async def handle_coupon_audience(callback_query: CallbackQuery, state: FSMContex
|
|||||||
text = (
|
text = (
|
||||||
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
|
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
|
||||||
"📝 <i>код</i> 💰 <i>сумма</i> 🔢 <i>лимит</i>\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 callback_query.message.edit_text(text=text, reply_markup=kb.as_markup())
|
||||||
await state.set_state(AdminCouponsState.waiting_for_balance_data)
|
await state.set_state(AdminCouponsState.waiting_for_balance_data)
|
||||||
return
|
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"
|
||||||
|
)
|
||||||
|
await callback_query.message.edit_text(text=text, reply_markup=kb.as_markup())
|
||||||
|
await state.set_state(AdminCouponsState.waiting_for_days_data)
|
||||||
|
return
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
|
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
|
||||||
"📝 <i>код</i> ⏳ <i>дни</i> 🔢 <i>лимит</i>\n\n"
|
"📝 <i>код</i> 📉 <i>процент</i> 🔢 <i>лимит</i>\n\n"
|
||||||
"Пример: <b>'DAYS10 10 50'</b> 👈\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 callback_query.message.edit_text(text=text, reply_markup=kb.as_markup())
|
||||||
await state.set_state(AdminCouponsState.waiting_for_days_data)
|
await state.set_state(AdminCouponsState.waiting_for_percent_data)
|
||||||
|
|
||||||
|
|
||||||
@router.message(AdminCouponsState.waiting_for_balance_data, IsAdminFilter())
|
@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:
|
if len(parts) != 3:
|
||||||
text = (
|
text = (
|
||||||
"❌ <b>Некорректный формат!</b> 📝 Пожалуйста, введите данные в формате:\n"
|
"❌ <b>Некорректный формат!</b>\n"
|
||||||
"🏷️ <b>код</b> 💰 <b>сумма</b> 🔢 <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())
|
await message.answer(text=text, reply_markup=kb.as_markup())
|
||||||
return
|
return
|
||||||
@@ -143,9 +160,11 @@ async def handle_balance_coupon_input(message: Message, state: FSMContext, sessi
|
|||||||
coupon_amount = int(parts[1])
|
coupon_amount = int(parts[1])
|
||||||
usage_limit = int(parts[2])
|
usage_limit = int(parts[2])
|
||||||
if coupon_amount <= 0:
|
if coupon_amount <= 0:
|
||||||
raise ValueError("Сумма должна быть больше 0")
|
raise ValueError
|
||||||
|
if usage_limit <= 0:
|
||||||
|
raise ValueError
|
||||||
except ValueError:
|
except ValueError:
|
||||||
text = "⚠️ <b>Проверьте правильность введенных данных!</b>\n💱 Сумма должна быть числом, а лимит — целым числом."
|
text = "⚠️ <b>Проверьте данные!</b>\nСумма и лимит должны быть целыми числами больше 0."
|
||||||
await message.answer(text=text, reply_markup=kb.as_markup())
|
await message.answer(text=text, reply_markup=kb.as_markup())
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -160,6 +179,7 @@ async def handle_balance_coupon_input(message: Message, state: FSMContext, sessi
|
|||||||
usage_limit,
|
usage_limit,
|
||||||
days=None,
|
days=None,
|
||||||
new_users_only=new_users_only,
|
new_users_only=new_users_only,
|
||||||
|
percent=None,
|
||||||
)
|
)
|
||||||
if not ok:
|
if not ok:
|
||||||
await message.answer("❌ Купон с таким кодом уже существует.", reply_markup=kb.as_markup())
|
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 "👤 Всем"
|
audience_txt = "🆕 Только новым" if new_users_only else "👤 Всем"
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
f"✅ Купон с кодом <b>{coupon_code}</b> успешно создан!\n"
|
f"✅ Купон <b>{coupon_code}</b> создан!\n"
|
||||||
f"💰 Сумма: <b>{coupon_amount} рублей</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>{audience_txt}</b>\n"
|
||||||
f"🔗 <b>Ссылка:</b> <code>{coupon_link}</code>\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 message.answer(text=text, reply_markup=kb.as_markup())
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при создании купона: {e}")
|
logger.error(f"Ошибка при создании купона: {e}")
|
||||||
await message.answer("❌ Произошла ошибка при создании купона.", reply_markup=kb.as_markup())
|
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:
|
if len(parts) != 3:
|
||||||
text = (
|
text = (
|
||||||
"❌ <b>Некорректный формат!</b> 📝 Пожалуйста, введите данные в формате:\n"
|
"❌ <b>Некорректный формат!</b>\n🏷️ <b>код</b> ⏳ <b>дни</b> 🔢 <b>лимит</b>\nПример: <b>'DAYS10 10 50'</b>"
|
||||||
"🏷️ <b>код</b> ⏳ <i>дни</i> 🔢 <b>лимит</b>\n"
|
|
||||||
"Пример: <b>'DAYS10 10 50'</b> 👈"
|
|
||||||
)
|
)
|
||||||
await message.answer(text=text, reply_markup=kb.as_markup())
|
await message.answer(text=text, reply_markup=kb.as_markup())
|
||||||
return
|
return
|
||||||
@@ -213,9 +230,11 @@ async def handle_days_coupon_input(message: Message, state: FSMContext, session:
|
|||||||
days = int(parts[1])
|
days = int(parts[1])
|
||||||
usage_limit = int(parts[2])
|
usage_limit = int(parts[2])
|
||||||
if days <= 0:
|
if days <= 0:
|
||||||
raise ValueError("Количество дней должно быть больше 0")
|
raise ValueError
|
||||||
|
if usage_limit <= 0:
|
||||||
|
raise ValueError
|
||||||
except ValueError:
|
except ValueError:
|
||||||
text = "⚠️ <b>Проверьте правильность введенных данных!</b>\n💱 Дни должны быть числом, а лимит — целым числом."
|
text = "⚠️ <b>Проверьте данные!</b>\nДни и лимит должны быть целыми числами больше 0."
|
||||||
await message.answer(text=text, reply_markup=kb.as_markup())
|
await message.answer(text=text, reply_markup=kb.as_markup())
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -230,6 +249,7 @@ async def handle_days_coupon_input(message: Message, state: FSMContext, session:
|
|||||||
usage_limit,
|
usage_limit,
|
||||||
days=days,
|
days=days,
|
||||||
new_users_only=new_users_only,
|
new_users_only=new_users_only,
|
||||||
|
percent=None,
|
||||||
)
|
)
|
||||||
if not ok:
|
if not ok:
|
||||||
await message.answer("❌ Купон с таким кодом уже существует.", reply_markup=kb.as_markup())
|
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 "👤 Всем"
|
audience_txt = "🆕 Только новым" if new_users_only else "👤 Всем"
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
f"✅ Купон с кодом <b>{coupon_code}</b> успешно создан!\n"
|
f"✅ Купон <b>{coupon_code}</b> создан!\n"
|
||||||
f"⏳ <b>{format_days(days)}</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>{audience_txt}</b>\n"
|
||||||
f"🔗 <b>Ссылка:</b> <code>{coupon_link}</code>\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 message.answer(text=text, reply_markup=kb.as_markup())
|
||||||
await state.clear()
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при создании купона: {e}")
|
logger.error(f"Ошибка при создании купона: {e}")
|
||||||
await message.answer("❌ Произошла ошибка при создании купона.", reply_markup=kb.as_markup())
|
await message.answer("❌ Произошла ошибка при создании купона.", reply_markup=kb.as_markup())
|
||||||
@@ -350,7 +437,6 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any):
|
|||||||
return
|
return
|
||||||
|
|
||||||
coupon_code = inline_query.query.split("coupon_")[1]
|
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)
|
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)
|
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
|
return
|
||||||
|
|
||||||
|
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']}"
|
title = f"Купон {coupon['code']}"
|
||||||
description = (
|
|
||||||
f"Получи {coupon['amount']} рублей!"
|
days_value = coupon.get("days")
|
||||||
if coupon["amount"] > 0
|
amount_value = coupon.get("amount") or 0
|
||||||
else f"Продли подписку на {format_days(coupon['days'])}!"
|
|
||||||
)
|
if days_value is not None and int(days_value) > 0:
|
||||||
message_text = (
|
days_int = int(days_value)
|
||||||
f"🎫 <b>Купон:</b> {coupon['code']}\n"
|
description = f"Продли подписку на {format_days(days_int)}!"
|
||||||
f"{'💰 <b>Бонус:</b> ' + str(coupon['amount']) + ' рублей' if coupon['amount'] > 0 else '⏳ <b>Продление:</b> ' + format_days(coupon['days'])}\n"
|
message_text = (
|
||||||
f"👇 Нажми, чтобы активировать!"
|
f"🎫 <b>Купон:</b> {coupon['code']}\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 = InlineKeyboardBuilder()
|
||||||
builder.button(text="Активировать купон", url=coupon_link)
|
builder.button(text="Активировать купон", url=coupon_link)
|
||||||
|
|||||||
@@ -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:
|
def format_coupons_list(coupons: list, username_bot: str) -> str:
|
||||||
coupon_list = "📜 Список всех купонов:\n\n"
|
text = "📜 <b>Список купонов</b>\n\n"
|
||||||
for coupon in coupons:
|
|
||||||
value_text = (
|
for i, coupon in enumerate(coupons, start=1):
|
||||||
f"💰 <b>Сумма:</b> {coupon['amount']} рублей"
|
percent_value = coupon.get("percent")
|
||||||
if coupon["amount"] > 0
|
days_value = coupon.get("days")
|
||||||
else f"⏳ <b>{format_days(coupon['days'])}</b>"
|
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"
|
return text
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from aiogram.exceptions import TelegramBadRequest
|
||||||
|
|
||||||
from aiogram import F, Router
|
from aiogram import F, Router
|
||||||
from aiogram.fsm.context import FSMContext
|
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 "—")
|
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"
|
||||||
"🧩 <b>Выбор конфигурации тарифа</b>\n\n"
|
f"📦 <b>Тариф:</b> {tariff.get('name', '—')}\n"
|
||||||
f"📦 <b>Тариф:</b> {tariff.get('name', '—')}\n"
|
f"📱 <b>Устройства:</b> {devices_label}\n"
|
||||||
f"📱 <b>Устройства:</b> {devices_label}\n"
|
f"📊 <b>Трафик:</b> {traffic_label}\n\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()
|
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} ГБ"
|
traffic_label = "Безлимит трафика" if selected_traffic_gb <= 0 else f"{selected_traffic_gb} ГБ"
|
||||||
|
|
||||||
await callback_query.message.edit_text(
|
text = (
|
||||||
text=(
|
"🧩 <b>Выбор конфигурации тарифа</b>\n\n"
|
||||||
"🧩 <b>Выбор конфигурации тарифа</b>\n\n"
|
f"📦 <b>Тариф:</b> {tariff.get('name', '—')}\n"
|
||||||
f"📦 <b>Тариф:</b> {tariff.get('name', '—')}\n"
|
f"📱 <b>Устройства:</b> {devices_label}\n"
|
||||||
f"📱 <b>Устройства:</b> {devices_label}\n"
|
f"📊 <b>Трафик:</b> {traffic_label}\n\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()
|
await callback_query.answer()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -296,6 +297,9 @@ async def handle_key_creation(
|
|||||||
target_message=target_message,
|
target_message=target_message,
|
||||||
text=SELECT_TARIFF_PLAN_MSG + discount_message,
|
text=SELECT_TARIFF_PLAN_MSG + discount_message,
|
||||||
reply_markup=builder.as_markup(),
|
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(
|
await state.update_data(
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ async def build_keys_response(records: list[Key] | None, session: AsyncSession,
|
|||||||
if getattr(record, "tariff_id", None):
|
if getattr(record, "tariff_id", None):
|
||||||
try:
|
try:
|
||||||
from handlers.tariffs.tariff_display import resolve_vless_enabled
|
from handlers.tariffs.tariff_display import resolve_vless_enabled
|
||||||
|
|
||||||
is_vless = await resolve_vless_enabled(session, int(record.tariff_id))
|
is_vless = await resolve_vless_enabled(session, int(record.tariff_id))
|
||||||
except Exception:
|
except Exception:
|
||||||
is_vless = False
|
is_vless = False
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
from math import ceil
|
||||||
|
|
||||||
from aiogram import F, Router
|
from aiogram import F, Router
|
||||||
from aiogram.fsm.context import FSMContext
|
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 aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
from config import USE_NEW_PAYMENT_FLOW, TRIBUTE_LINK
|
from config import USE_NEW_PAYMENT_FLOW, TRIBUTE_LINK
|
||||||
from core.bootstrap import PAYMENTS_CONFIG
|
from core.bootstrap import PAYMENTS_CONFIG
|
||||||
from core.settings.money_config import get_currency_mode
|
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 database.temporary_data import create_temporary_data
|
||||||
from handlers import buttons as btn
|
from handlers import buttons as btn
|
||||||
from handlers.payments.currency_flow import (
|
from handlers.payments.currency_flow import (
|
||||||
@@ -17,7 +28,7 @@ from handlers.payments.currency_flow import (
|
|||||||
currency_label,
|
currency_label,
|
||||||
)
|
)
|
||||||
from handlers.payments.providers import get_providers_with_hooks
|
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 handlers.utils import edit_or_send_message
|
||||||
from logger import logger
|
from logger import logger
|
||||||
|
|
||||||
@@ -25,6 +36,10 @@ from logger import logger
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
class FastFlowCouponState(StatesGroup):
|
||||||
|
waiting_for_coupon_code = State()
|
||||||
|
|
||||||
|
|
||||||
async def get_payment_providers_config() -> dict[str, bool]:
|
async def get_payment_providers_config() -> dict[str, bool]:
|
||||||
config = PAYMENTS_CONFIG or {}
|
config = PAYMENTS_CONFIG or {}
|
||||||
return dict(config)
|
return dict(config)
|
||||||
@@ -130,10 +145,30 @@ async def try_fast_payment_flow(
|
|||||||
if not providers and not show_tribute:
|
if not providers and not show_tribute:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
keyboard = build_currency_choice_kb(
|
keyboard_original = build_currency_choice_kb(show_stars=show_stars, show_tribute=show_tribute)
|
||||||
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(
|
lead_text = await shortfall_lead_text(
|
||||||
session,
|
session,
|
||||||
tg_id,
|
tg_id,
|
||||||
@@ -161,6 +196,7 @@ async def try_fast_payment_flow(
|
|||||||
currency = cfg.get("currency")
|
currency = cfg.get("currency")
|
||||||
if currency:
|
if currency:
|
||||||
await state.update_data(chosen_currency=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):
|
if await _run_provider_flow(single_provider, callback_query, session, state, required_amount):
|
||||||
return True
|
return True
|
||||||
return False
|
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"))
|
keyboard.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
|
||||||
|
|
||||||
lead_text = await shortfall_lead_text(
|
lead_text = await shortfall_lead_text(
|
||||||
@@ -209,6 +246,288 @@ async def try_fast_payment_flow(
|
|||||||
return True
|
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|"))
|
@router.callback_query(F.data.startswith("choose_payment_currency|"))
|
||||||
async def choose_payment_currency(callback_query: CallbackQuery, state: FSMContext, session: Any):
|
async def choose_payment_currency(callback_query: CallbackQuery, state: FSMContext, session: Any):
|
||||||
payment_config = await get_payment_providers_config()
|
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)
|
await state.update_data(chosen_currency=currency)
|
||||||
|
|
||||||
if not filtered:
|
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(
|
await edit_or_send_message(
|
||||||
target_message=callback_query.message,
|
target_message=callback_query.message,
|
||||||
text="Для выбранной валюты нет доступных касс. Выберите другую валюту или вернитесь в меню.",
|
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"))
|
keyboard.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
|
||||||
|
|
||||||
lead_text = await shortfall_lead_text(
|
lead_text = await shortfall_lead_text(
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 460 KiB |
+1
-1
@@ -92,4 +92,4 @@ def get_git_commit_number() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def get_version() -> 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()}"
|
||||||
|
|||||||
Reference in New Issue
Block a user