mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 19:32:10 +00:00
Добавлена опция "только для первой покупки" в промокоды
- models.py: добавлено поле first_purchase_only в PromoCode - universal_migration.py: миграция для добавления колонки first_purchase_only - promocodes.py: добавлен хендлер toggle_promocode_first_purchase, отображение статуса в управлении промокодом - promocode.py: обработка ошибки "not
This commit is contained in:
@@ -993,6 +993,7 @@ class PromoCode(Base):
|
|||||||
valid_until = Column(DateTime, nullable=True)
|
valid_until = Column(DateTime, nullable=True)
|
||||||
|
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
|
first_purchase_only = Column(Boolean, default=False) # Только для первой покупки
|
||||||
|
|
||||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
created_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||||
promo_group_id = Column(Integer, ForeignKey("promo_groups.id", ondelete="SET NULL"), nullable=True, index=True)
|
promo_group_id = Column(Integer, ForeignKey("promo_groups.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||||
|
|||||||
@@ -4630,6 +4630,38 @@ async def add_promocode_promo_group_column() -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def add_promocode_first_purchase_only_column() -> bool:
|
||||||
|
"""Добавляет колонку first_purchase_only в таблицу promocodes."""
|
||||||
|
column_exists = await check_column_exists('promocodes', 'first_purchase_only')
|
||||||
|
if column_exists:
|
||||||
|
logger.info("Колонка first_purchase_only уже существует в promocodes")
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
db_type = await get_database_type()
|
||||||
|
|
||||||
|
if db_type == 'sqlite':
|
||||||
|
await conn.execute(
|
||||||
|
text("ALTER TABLE promocodes ADD COLUMN first_purchase_only BOOLEAN DEFAULT 0")
|
||||||
|
)
|
||||||
|
elif db_type == 'postgresql':
|
||||||
|
await conn.execute(
|
||||||
|
text("ALTER TABLE promocodes ADD COLUMN first_purchase_only BOOLEAN DEFAULT FALSE")
|
||||||
|
)
|
||||||
|
elif db_type == 'mysql':
|
||||||
|
await conn.execute(
|
||||||
|
text("ALTER TABLE promocodes ADD COLUMN first_purchase_only BOOLEAN DEFAULT FALSE")
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("✅ Добавлена колонка first_purchase_only в promocodes")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as error:
|
||||||
|
logger.error(f"❌ Ошибка добавления first_purchase_only в promocodes: {error}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def migrate_contest_templates_prize_columns() -> bool:
|
async def migrate_contest_templates_prize_columns() -> bool:
|
||||||
"""Миграция contest_templates: prize_days -> prize_type + prize_value."""
|
"""Миграция contest_templates: prize_days -> prize_type + prize_value."""
|
||||||
try:
|
try:
|
||||||
@@ -5085,6 +5117,13 @@ async def run_universal_migration():
|
|||||||
else:
|
else:
|
||||||
logger.warning("⚠️ Проблемы с добавлением promo_group_id в promocodes")
|
logger.warning("⚠️ Проблемы с добавлением promo_group_id в promocodes")
|
||||||
|
|
||||||
|
logger.info("=== ДОБАВЛЕНИЕ FIRST_PURCHASE_ONLY В PROMOCODES ===")
|
||||||
|
first_purchase_ready = await add_promocode_first_purchase_only_column()
|
||||||
|
if first_purchase_ready:
|
||||||
|
logger.info("✅ Колонка first_purchase_only в promocodes готова")
|
||||||
|
else:
|
||||||
|
logger.warning("⚠️ Проблемы с добавлением first_purchase_only в promocodes")
|
||||||
|
|
||||||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ MAIN_MENU_BUTTONS ===")
|
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ MAIN_MENU_BUTTONS ===")
|
||||||
main_menu_buttons_created = await create_main_menu_buttons_table()
|
main_menu_buttons_created = await create_main_menu_buttons_table()
|
||||||
if main_menu_buttons_created:
|
if main_menu_buttons_created:
|
||||||
|
|||||||
@@ -187,27 +187,39 @@ async def show_promocode_management(
|
|||||||
|
|
||||||
if promo.valid_until:
|
if promo.valid_until:
|
||||||
text += f"⏰ <b>Действует до:</b> {format_datetime(promo.valid_until)}\n"
|
text += f"⏰ <b>Действует до:</b> {format_datetime(promo.valid_until)}\n"
|
||||||
|
|
||||||
|
first_purchase_only = getattr(promo, 'first_purchase_only', False)
|
||||||
|
first_purchase_emoji = "✅" if first_purchase_only else "❌"
|
||||||
|
text += f"🆕 <b>Только первая покупка:</b> {first_purchase_emoji}\n"
|
||||||
|
|
||||||
text += f"📅 <b>Создан:</b> {format_datetime(promo.created_at)}\n"
|
text += f"📅 <b>Создан:</b> {format_datetime(promo.created_at)}\n"
|
||||||
|
|
||||||
|
first_purchase_btn_text = "🆕 Первая покупка: ✅" if first_purchase_only else "🆕 Первая покупка: ❌"
|
||||||
|
|
||||||
keyboard = [
|
keyboard = [
|
||||||
[
|
[
|
||||||
types.InlineKeyboardButton(
|
types.InlineKeyboardButton(
|
||||||
text="✏️ Редактировать",
|
text="✏️ Редактировать",
|
||||||
callback_data=f"promo_edit_{promo.id}"
|
callback_data=f"promo_edit_{promo.id}"
|
||||||
),
|
),
|
||||||
types.InlineKeyboardButton(
|
types.InlineKeyboardButton(
|
||||||
text="🔄 Переключить статус",
|
text="🔄 Переключить статус",
|
||||||
callback_data=f"promo_toggle_{promo.id}"
|
callback_data=f"promo_toggle_{promo.id}"
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
types.InlineKeyboardButton(
|
types.InlineKeyboardButton(
|
||||||
text="📊 Статистика",
|
text=first_purchase_btn_text,
|
||||||
|
callback_data=f"promo_toggle_first_{promo.id}"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
types.InlineKeyboardButton(
|
||||||
|
text="📊 Статистика",
|
||||||
callback_data=f"promo_stats_{promo.id}"
|
callback_data=f"promo_stats_{promo.id}"
|
||||||
),
|
),
|
||||||
types.InlineKeyboardButton(
|
types.InlineKeyboardButton(
|
||||||
text="🗑️ Удалить",
|
text="🗑️ Удалить",
|
||||||
callback_data=f"promo_delete_{promo.id}"
|
callback_data=f"promo_delete_{promo.id}"
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -946,7 +958,31 @@ async def toggle_promocode_status(
|
|||||||
|
|
||||||
status_text = "активирован" if new_status else "деактивирован"
|
status_text = "активирован" if new_status else "деактивирован"
|
||||||
await callback.answer(f"✅ Промокод {status_text}", show_alert=True)
|
await callback.answer(f"✅ Промокод {status_text}", show_alert=True)
|
||||||
|
|
||||||
|
await show_promocode_management(callback, db_user, db)
|
||||||
|
|
||||||
|
|
||||||
|
@admin_required
|
||||||
|
@error_handler
|
||||||
|
async def toggle_promocode_first_purchase(
|
||||||
|
callback: types.CallbackQuery,
|
||||||
|
db_user: User,
|
||||||
|
db: AsyncSession
|
||||||
|
):
|
||||||
|
"""Переключает режим 'только для первой покупки'."""
|
||||||
|
promo_id = int(callback.data.split('_')[-1])
|
||||||
|
|
||||||
|
promo = await get_promocode_by_id(db, promo_id)
|
||||||
|
if not promo:
|
||||||
|
await callback.answer("❌ Промокод не найден", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
new_status = not getattr(promo, 'first_purchase_only', False)
|
||||||
|
await update_promocode(db, promo, first_purchase_only=new_status)
|
||||||
|
|
||||||
|
status_text = "включён" if new_status else "выключен"
|
||||||
|
await callback.answer(f"✅ Режим 'первая покупка' {status_text}", show_alert=True)
|
||||||
|
|
||||||
await show_promocode_management(callback, db_user, db)
|
await show_promocode_management(callback, db_user, db)
|
||||||
|
|
||||||
|
|
||||||
@@ -1124,6 +1160,7 @@ def register_handlers(dp: Dispatcher):
|
|||||||
dp.callback_query.register(process_promo_group_selection, F.data.startswith("promo_select_group_"))
|
dp.callback_query.register(process_promo_group_selection, F.data.startswith("promo_select_group_"))
|
||||||
|
|
||||||
dp.callback_query.register(show_promocode_management, F.data.startswith("promo_manage_"))
|
dp.callback_query.register(show_promocode_management, F.data.startswith("promo_manage_"))
|
||||||
|
dp.callback_query.register(toggle_promocode_first_purchase, F.data.startswith("promo_toggle_first_"))
|
||||||
dp.callback_query.register(toggle_promocode_status, F.data.startswith("promo_toggle_"))
|
dp.callback_query.register(toggle_promocode_status, F.data.startswith("promo_toggle_"))
|
||||||
dp.callback_query.register(show_promocode_stats, F.data.startswith("promo_stats_"))
|
dp.callback_query.register(show_promocode_stats, F.data.startswith("promo_stats_"))
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,10 @@ async def process_promocode(
|
|||||||
"expired": texts.PROMOCODE_EXPIRED,
|
"expired": texts.PROMOCODE_EXPIRED,
|
||||||
"used": texts.PROMOCODE_USED,
|
"used": texts.PROMOCODE_USED,
|
||||||
"already_used_by_user": texts.PROMOCODE_USED,
|
"already_used_by_user": texts.PROMOCODE_USED,
|
||||||
|
"not_first_purchase": texts.t(
|
||||||
|
"PROMOCODE_NOT_FIRST_PURCHASE",
|
||||||
|
"❌ Этот промокод доступен только для первой покупки"
|
||||||
|
),
|
||||||
"server_error": texts.ERROR
|
"server_error": texts.ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,13 @@ class PromoCodeService:
|
|||||||
existing_use = await check_user_promocode_usage(db, user_id, promocode.id)
|
existing_use = await check_user_promocode_usage(db, user_id, promocode.id)
|
||||||
if existing_use:
|
if existing_use:
|
||||||
return {"success": False, "error": "already_used_by_user"}
|
return {"success": False, "error": "already_used_by_user"}
|
||||||
|
|
||||||
|
# Проверка "только для первой покупки"
|
||||||
|
if getattr(promocode, 'first_purchase_only', False):
|
||||||
|
has_purchase = await self._user_has_paid_purchase(db, user_id)
|
||||||
|
if has_purchase:
|
||||||
|
return {"success": False, "error": "not_first_purchase"}
|
||||||
|
|
||||||
balance_before_kopeks = user.balance_kopeks
|
balance_before_kopeks = user.balance_kopeks
|
||||||
|
|
||||||
result_description = await self._apply_promocode_effects(db, user, promocode)
|
result_description = await self._apply_promocode_effects(db, user, promocode)
|
||||||
@@ -228,3 +234,20 @@ class PromoCodeService:
|
|||||||
effects.append("ℹ️ У вас уже есть активная подписка")
|
effects.append("ℹ️ У вас уже есть активная подписка")
|
||||||
|
|
||||||
return "\n".join(effects) if effects else "✅ Промокод активирован"
|
return "\n".join(effects) if effects else "✅ Промокод активирован"
|
||||||
|
|
||||||
|
async def _user_has_paid_purchase(self, db: AsyncSession, user_id: int) -> bool:
|
||||||
|
"""Проверяет была ли у пользователя хотя бы одна успешная платная покупка."""
|
||||||
|
from sqlalchemy import select, func
|
||||||
|
from app.database.models import Transaction
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(func.count(Transaction.id))
|
||||||
|
.where(
|
||||||
|
Transaction.user_id == user_id,
|
||||||
|
Transaction.status == "success",
|
||||||
|
Transaction.amount_kopeks > 0, # Платные транзакции
|
||||||
|
Transaction.type.in_(["subscription", "balance_topup", "renewal"])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
count = result.scalar()
|
||||||
|
return count > 0
|
||||||
|
|||||||
Reference in New Issue
Block a user