diff --git a/bot/handlers/admin/promo_codes.py b/bot/handlers/admin/promo_codes.py
index 227d239..9775ecf 100644
--- a/bot/handlers/admin/promo_codes.py
+++ b/bot/handlers/admin/promo_codes.py
@@ -32,29 +32,35 @@ async def create_promo_prompt_handler(callback: types.CallbackQuery,
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
- prompt_text = _("admin_promo_create_prompt",
- example_format="MYPROMO20 7 100 30")
+ # Step 1: Ask for promo code
+ prompt_text = _(
+ "admin_promo_step1_code",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 1 ΠΈΠ· 5: ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ², ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ):"
+ )
try:
await callback.message.edit_text(
prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML")
except Exception as e:
logging.warning(
f"Could not edit message for promo prompt: {e}. Sending new.")
await callback.message.answer(
prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML")
await callback.answer()
- await state.set_state(AdminStates.waiting_for_promo_details)
+ await state.set_state(AdminStates.waiting_for_promo_code)
-@router.message(AdminStates.waiting_for_promo_details, F.text)
-async def process_promo_code_details_handler(message: types.Message,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings,
- session: AsyncSession):
+# Step 1: Process promo code
+@router.message(AdminStates.waiting_for_promo_code, F.text)
+async def process_promo_code_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n:
@@ -62,87 +68,457 @@ async def process_promo_code_details_handler(message: types.Message,
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
- if not message.text:
- await message.answer(_("admin_promo_invalid_format"))
- return
-
- parts = message.text.strip().split()
- if not (3 <= len(parts) <= 4):
- await message.answer(_("admin_promo_invalid_format"))
- return
-
try:
- code_str = parts[0].upper()
+ code_str = message.text.strip().upper()
if not (3 <= len(code_str) <= 30 and code_str.isalnum()):
- raise ValueError(_("admin_promo_invalid_code_format"))
+ await message.answer(_(
+ "admin_promo_invalid_code_format",
+ default="β ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΠΎΠ΄Π΅ΡΠΆΠ°ΡΡ 3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ² (ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ)"
+ ))
+ return
+
+ # Check if code already exists
+ existing_promo = await promo_code_dal.get_promo_code_by_code(session, code_str)
+ if existing_promo:
+ await message.answer(_(
+ "admin_promo_code_already_exists",
+ default="β ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ Ρ ΡΠ°ΠΊΠΈΠΌ ΠΊΠΎΠ΄ΠΎΠΌ ΡΠΆΠ΅ ΡΡΡΠ΅ΡΡΠ²ΡΠ΅Ρ"
+ ))
+ return
+
+ await state.update_data(promo_code=code_str)
+
+ # Step 2: Ask for bonus days
+ prompt_text = _(
+ "admin_promo_step2_bonus_days",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 2 ΠΈΠ· 5: ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ\n\nΠΠΎΠ΄: {code}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π±ΠΎΠ½ΡΡΠ½ΡΡ
Π΄Π½Π΅ΠΉ (1-365):",
+ code=code_str
+ )
+
+ await message.answer(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ await state.set_state(AdminStates.waiting_for_promo_bonus_days)
+
+ except Exception as e:
+ logging.error(f"Error processing promo code: {e}")
+ await message.answer(_(
+ "admin_promo_processing_error",
+ default="β ΠΡΠΈΠ±ΠΊΠ° ΠΎΠ±ΡΠ°Π±ΠΎΡΠΊΠΈ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°"
+ ))
- bonus_days = int(parts[1])
- max_activations = int(parts[2])
-
- valid_until_date: Optional[datetime] = None
- valid_until_str_display = _("admin_promo_valid_indefinitely")
-
- if len(parts) == 4:
- valid_days_from_now = int(parts[3])
- if valid_days_from_now <= 0:
- raise ValueError(_("admin_promo_invalid_validity_days"))
- valid_until_date = datetime.now(
- timezone.utc) + timedelta(days=valid_days_from_now)
- valid_until_str_display = _(
- "admin_promo_valid_until_display",
- date=valid_until_date.strftime('%Y-%m-%d'))
-
- if bonus_days <= 0 or max_activations <= 0:
- raise ValueError(_("admin_promo_invalid_bonus_or_activations"))
-
- except ValueError as e:
- await message.answer(_("admin_promo_invalid_values", error=str(e)))
+# Step 2: Process bonus days
+@router.message(AdminStates.waiting_for_promo_bonus_days, F.text)
+async def process_promo_bonus_days_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
return
- except Exception as e_parse:
- logging.error(
- f"Error parsing promo details '{message.text}': {e_parse}")
- await message.answer(_("admin_promo_invalid_format_general"))
- return
-
- admin_id = message.from_user.id if message.from_user else 0
-
- promo_data_to_create = {
- "code": code_str,
- "bonus_days": bonus_days,
- "max_activations": max_activations,
- "created_by_admin_id": admin_id,
- "valid_until": valid_until_date,
- "is_active": True,
- "current_activations": 0
- }
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
try:
- created_promo = await promo_code_dal.create_promo_code(
- session, promo_data_to_create)
- await session.commit()
+ bonus_days = int(message.text.strip())
+ if bonus_days <= 0 or bonus_days > 365:
+ await message.answer(_(
+ "admin_promo_invalid_bonus_days",
+ default="β ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
+ ))
+ return
+
+ data = await state.get_data()
+ await state.update_data(bonus_days=bonus_days)
+
+ # Step 3: Ask for max activations
+ prompt_text = _(
+ "admin_promo_step3_max_activations",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 3 ΠΈΠ· 5: ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ (1-10000):",
+ code=data['promo_code'],
+ bonus_days=bonus_days
+ )
+
+ await message.answer(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ await state.set_state(AdminStates.waiting_for_promo_max_activations)
+
+ except ValueError:
+ await message.answer(_(
+ "admin_promo_invalid_bonus_days",
+ default="β ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
+ ))
- if created_promo:
- success_text = _("admin_promo_created_success",
- code=created_promo.code,
- bonus_days=created_promo.bonus_days,
- max_activations=created_promo.max_activations,
- valid_until_str=valid_until_str_display)
- await message.answer(success_text,
- reply_markup=get_back_to_admin_panel_keyboard(
- current_lang, i18n))
+# Step 3: Process max activations
+@router.message(AdminStates.waiting_for_promo_max_activations, F.text)
+async def process_promo_max_activations_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ max_activations = int(message.text.strip())
+ if max_activations <= 0 or max_activations > 10000:
+ await message.answer(_(
+ "admin_promo_invalid_max_activations",
+ default="β ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 10000"
+ ))
+ return
+
+ data = await state.get_data()
+ await state.update_data(max_activations=max_activations)
+
+ # Step 4: Ask for validity days (with buttons)
+ keyboard = InlineKeyboardBuilder()
+ keyboard.row(
+ InlineKeyboardButton(
+ text=_("admin_promo_unlimited_validity", default="βΎοΈ ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ"),
+ callback_data="promo_unlimited_validity"
+ )
+ )
+ keyboard.row(
+ InlineKeyboardButton(
+ text=_("admin_promo_set_validity", default="β° Π£ΡΡΠ°Π½ΠΎΠ²ΠΈΡΡ ΡΡΠΎΠΊ"),
+ callback_data="promo_set_validity"
+ )
+ )
+ keyboard.row(
+ InlineKeyboardButton(
+ text=_("admin_panel_back_button", default="β¬
οΈ ΠΠ°Π·Π°Π΄"),
+ callback_data="admin_panel_back"
+ )
+ )
+
+ prompt_text = _(
+ "admin_promo_step4_validity",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 4 ΠΈΠ· 5: Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_activations}\n\nΠΡΠ±Π΅ΡΠΈΡΠ΅ ΡΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°:",
+ code=data['promo_code'],
+ bonus_days=data['bonus_days'],
+ max_activations=max_activations
+ )
+
+ await message.answer(
+ prompt_text,
+ reply_markup=keyboard.as_markup(),
+ parse_mode="HTML"
+ )
+ await state.set_state(AdminStates.waiting_for_promo_validity_days)
+
+ except ValueError:
+ await message.answer(_(
+ "admin_promo_invalid_max_activations",
+ default="β ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 10000"
+ ))
+
+# Handle validity choice buttons
+@router.callback_query(F.data == "promo_unlimited_validity", StateFilter(AdminStates.waiting_for_promo_validity_days))
+async def process_promo_unlimited_validity(callback: types.CallbackQuery,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ await state.update_data(validity_days=None)
+ await ask_for_promo_description(callback, state, i18n_data, settings)
+
+@router.callback_query(F.data == "promo_set_validity", StateFilter(AdminStates.waiting_for_promo_validity_days))
+async def process_promo_set_validity(callback: types.CallbackQuery,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await callback.answer("Language service error.", show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ prompt_text = _(
+ "admin_promo_enter_validity_days",
+ default="β° ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄Π½Π΅ΠΉ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (1-365):"
+ )
+
+ try:
+ await callback.message.edit_text(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
+ )
+ except Exception as e:
+ await callback.message.answer(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
+ )
+ await callback.answer()
+
+# Step 4: Process validity days (when user chooses to set)
+@router.message(AdminStates.waiting_for_promo_validity_days, F.text)
+async def process_promo_validity_days_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ validity_days = int(message.text.strip())
+ if validity_days <= 0 or validity_days > 365:
+ await message.answer(_(
+ "admin_promo_invalid_validity_days",
+ default="β ΠΠ½ΠΈ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
+ ))
+ return
+
+ await state.update_data(validity_days=validity_days)
+
+ # Create a fake callback for consistency
+ fake_callback = types.CallbackQuery(
+ id="fake",
+ from_user=message.from_user,
+ chat_instance="fake",
+ message=message,
+ data="fake"
+ )
+
+ await ask_for_promo_description(fake_callback, state, i18n_data, settings)
+
+ except ValueError:
+ await message.answer(_(
+ "admin_promo_invalid_validity_days",
+ default="β ΠΠ½ΠΈ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
+ ))
+
+async def ask_for_promo_description(callback_or_message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ """Ask for promo description (final step)"""
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ if hasattr(callback_or_message, 'answer'):
+ await callback_or_message.answer("Language service error.", show_alert=True)
else:
- await message.answer(
- _("admin_promo_creation_failed_duplicate", code=code_str))
+ await callback_or_message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
- except Exception as e_db_create:
+ data = await state.get_data()
+ validity_days = data.get('validity_days')
+ validity_text = "ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ" if validity_days is None else f"{validity_days} Π΄Π½."
+
+ # Step 5: Ask for description (optional)
+ keyboard = InlineKeyboardBuilder()
+ keyboard.row(
+ InlineKeyboardButton(
+ text=_("admin_promo_skip_description", default="βοΈ ΠΡΠΎΠΏΡΡΡΠΈΡΡ"),
+ callback_data="promo_skip_description"
+ )
+ )
+ keyboard.row(
+ InlineKeyboardButton(
+ text=_("admin_panel_back_button", default="β¬
οΈ ΠΠ°Π·Π°Π΄"),
+ callback_data="admin_panel_back"
+ )
+ )
+
+ prompt_text = _(
+ "admin_promo_step5_description",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 5 ΠΈΠ· 5: ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ (ΠΎΠΏΡΠΈΠΎΠ½Π°Π»ΡΠ½ΠΎ)\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_activations}\nΠΠ΅ΠΉΡΡΠ²ΡΠ΅Ρ: {validity}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΎΠΏΠΈΡΠ°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° ΠΈΠ»ΠΈ Π½Π°ΠΆΠΌΠΈΡΠ΅ \"ΠΡΠΎΠΏΡΡΡΠΈΡΡ\":",
+ code=data['promo_code'],
+ bonus_days=data['bonus_days'],
+ max_activations=data['max_activations'],
+ validity=validity_text
+ )
+
+ if hasattr(callback_or_message, 'message'):
+ try:
+ await callback_or_message.message.edit_text(
+ prompt_text,
+ reply_markup=keyboard.as_markup(),
+ parse_mode="HTML"
+ )
+ except:
+ await callback_or_message.message.answer(
+ prompt_text,
+ reply_markup=keyboard.as_markup(),
+ parse_mode="HTML"
+ )
+ if hasattr(callback_or_message, 'answer'):
+ await callback_or_message.answer()
+ else:
+ await callback_or_message.answer(
+ prompt_text,
+ reply_markup=keyboard.as_markup(),
+ parse_mode="HTML"
+ )
+
+ await state.set_state(AdminStates.waiting_for_promo_description)
+
+# Handle skip description button
+@router.callback_query(F.data == "promo_skip_description", StateFilter(AdminStates.waiting_for_promo_description))
+async def process_promo_skip_description(callback: types.CallbackQuery,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ await state.update_data(description=None)
+ await create_promo_code_final(callback, state, i18n_data, settings, session)
+
+# Step 5: Process description (optional)
+@router.message(AdminStates.waiting_for_promo_description, F.text)
+async def process_promo_description_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ description = message.text.strip()
+ if len(description) > 200:
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if i18n:
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+ await message.answer(_(
+ "admin_promo_description_too_long",
+ default="β ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ Π½Π΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ ΠΏΡΠ΅Π²ΡΡΠ°ΡΡ 200 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ²"
+ ))
+ else:
+ await message.answer("Description too long")
+ return
+
+ await state.update_data(description=description)
+
+ # Create a fake callback for consistency
+ fake_callback = types.CallbackQuery(
+ id="fake",
+ from_user=message.from_user,
+ chat_instance="fake",
+ message=message,
+ data="fake"
+ )
+
+ await create_promo_code_final(fake_callback, state, i18n_data, settings, session)
+
+async def create_promo_code_final(callback_or_message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ """Final step to create the promo code"""
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ if hasattr(callback_or_message, 'answer'):
+ await callback_or_message.answer("Language service error.", show_alert=True)
+ else:
+ await callback_or_message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ data = await state.get_data()
+ promo_code = data['promo_code']
+ bonus_days = data['bonus_days']
+ max_activations = data['max_activations']
+ validity_days = data.get('validity_days')
+ description = data.get('description')
+
+ # Create validity date
+ valid_until_date: Optional[datetime] = None
+ valid_until_str_display = _("admin_promo_valid_indefinitely", default="ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ")
+
+ if validity_days:
+ valid_until_date = datetime.now(timezone.utc) + timedelta(days=validity_days)
+ valid_until_str_display = valid_until_date.strftime('%Y-%m-%d')
+
+ # Show creating message
+ creating_text = _(
+ "admin_promo_creating",
+ default="β³ Π‘ΠΎΠ·Π΄Π°Ρ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄..."
+ )
+
+ if hasattr(callback_or_message, 'message'):
+ try:
+ await callback_or_message.message.edit_text(creating_text)
+ except:
+ await callback_or_message.message.answer(creating_text)
+ if hasattr(callback_or_message, 'answer'):
+ await callback_or_message.answer()
+ else:
+ await callback_or_message.answer(creating_text)
+
+ # Create promo code
+ try:
+ promo_data = {
+ "code": promo_code,
+ "bonus_days": bonus_days,
+ "max_activations": max_activations,
+ "current_activations": 0,
+ "is_active": True,
+ "valid_until": valid_until_date,
+ "description": description
+ }
+
+ await promo_code_dal.create_promo_code(session, promo_data)
+ await session.commit()
+
+ # Send success message
+ success_text = _(
+ "admin_promo_created_success",
+ default="β
ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ ΡΠΎΠ·Π΄Π°Π½!\n\nπ ΠΠΎΠ΄: {code}\nπ ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {days}\nπ’ ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_act}\nβ° ΠΠ΅ΠΉΡΡΠ²ΡΠ΅Ρ Π΄ΠΎ: {validity}\nπ ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅: {description}",
+ code=promo_code,
+ days=bonus_days,
+ max_act=max_activations,
+ validity=valid_until_str_display,
+ description=description or "ΠΠ΅ ΡΠΊΠ°Π·Π°Π½ΠΎ"
+ )
+
+ if hasattr(callback_or_message, 'message'):
+ target_message = callback_or_message.message
+ else:
+ target_message = callback_or_message
+
+ await target_message.answer(
+ success_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+
+ except Exception as e:
+ logging.error(f"Error creating promo code: {e}")
await session.rollback()
- logging.error(
- f"Failed to create promo code '{code_str}' in DB: {e_db_create}",
- exc_info=True)
- await message.answer(_("admin_promo_creation_failed"))
-
+ error_text = _(
+ "admin_promo_creation_failed",
+ default="β ΠΡΠΈΠ±ΠΊΠ° ΡΠΎΠ·Π΄Π°Π½ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°: {error}",
+ error=str(e)
+ )
+
+ if hasattr(callback_or_message, 'message'):
+ target_message = callback_or_message.message
+ else:
+ target_message = callback_or_message
+
+ await target_message.answer(
+ error_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
+ )
+
await state.clear()
+# Legacy handler removed - using step-by-step creation now
+
async def view_promo_codes_handler(callback: types.CallbackQuery,
i18n_data: dict, settings: Settings,
@@ -341,8 +717,17 @@ async def promo_delete_handler(callback: types.CallbackQuery, i18n_data: dict,
F.data == "admin_action:main",
StateFilter(
AdminStates.waiting_for_promo_details,
+ AdminStates.waiting_for_promo_code,
+ AdminStates.waiting_for_promo_bonus_days,
+ AdminStates.waiting_for_promo_max_activations,
+ AdminStates.waiting_for_promo_validity_days,
+ AdminStates.waiting_for_promo_description,
AdminStates.waiting_for_promo_edit_details,
AdminStates.waiting_for_bulk_promo_details,
+ AdminStates.waiting_for_bulk_promo_quantity,
+ AdminStates.waiting_for_bulk_promo_bonus_days,
+ AdminStates.waiting_for_bulk_promo_max_activations,
+ AdminStates.waiting_for_bulk_promo_validity_days,
),
)
async def cancel_promo_creation_state_to_menu(callback: types.CallbackQuery,
@@ -382,24 +767,26 @@ async def create_bulk_promo_prompt_handler(callback: types.CallbackQuery,
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+ # Step 1: Ask for quantity
prompt_text = _(
- "admin_bulk_promo_create_prompt",
- default="π¦ ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ Π΄Π°Π½Π½ΡΠ΅ Π² ΡΠΎΡΠΌΠ°ΡΠ΅:\n<ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ> <Π±ΠΎΠ½ΡΡΠ½ΡΠ΅_Π΄Π½ΠΈ> <ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅_ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΠ΅> [Π΄Π½ΠΈ_Π΄Π΅ΠΉΡΡΠ²ΠΈΡ]\n\nΠΡΠΈΠΌΠ΅Ρ: 50 7 100 30\n(ΡΠΎΠ·Π΄Π°ΡΡ 50 ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² Π½Π° 7 Π΄Π½Π΅ΠΉ Ρ Π»ΠΈΠΌΠΈΡΠΎΠΌ 100 Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ ΠΊΠ°ΠΆΠ΄ΡΠΉ, Π΄Π΅ΠΉΡΡΠ²ΡΡΡΠΈΡ
30 Π΄Π½Π΅ΠΉ)",
- example_format="50 7 100 30"
+ "admin_bulk_promo_step1_quantity",
+ default="π¦ ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 1 ΠΈΠ· 4: ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² Π΄Π»Ρ ΡΠΎΠ·Π΄Π°Π½ΠΈΡ (1-1000):"
)
try:
await callback.message.edit_text(
prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML")
except Exception as e:
logging.warning(
f"Could not edit message for bulk promo prompt: {e}. Sending new.")
await callback.message.answer(
prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML")
await callback.answer()
- await state.set_state(AdminStates.waiting_for_bulk_promo_details)
+ await state.set_state(AdminStates.waiting_for_bulk_promo_quantity)
def generate_unique_promo_code(length: int = 8) -> str:
@@ -410,6 +797,386 @@ def generate_unique_promo_code(length: int = 8) -> str:
return ''.join(random.choice(characters) for _ in range(length))
+# Step 1: Process quantity
+@router.message(AdminStates.waiting_for_bulk_promo_quantity, F.text)
+async def process_bulk_promo_quantity_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ quantity = int(message.text.strip())
+ if quantity <= 0 or quantity > 1000:
+ await message.answer(_(
+ "admin_bulk_promo_invalid_quantity",
+ default="β ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 1000"
+ ))
+ return
+
+ await state.update_data(quantity=quantity)
+
+ # Step 2: Ask for bonus days
+ prompt_text = _(
+ "admin_bulk_promo_step2_bonus_days",
+ default="π¦ ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 2 ΠΈΠ· 4: ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ\n\nΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ: {quantity}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π±ΠΎΠ½ΡΡΠ½ΡΡ
Π΄Π½Π΅ΠΉ (1-365):",
+ quantity=quantity
+ )
+
+ await message.answer(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ await state.set_state(AdminStates.waiting_for_bulk_promo_bonus_days)
+
+ except ValueError:
+ await message.answer(_(
+ "admin_bulk_promo_invalid_quantity",
+ default="β ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 1000"
+ ))
+
+# Step 2: Process bonus days
+@router.message(AdminStates.waiting_for_bulk_promo_bonus_days, F.text)
+async def process_bulk_promo_bonus_days_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ bonus_days = int(message.text.strip())
+ if bonus_days <= 0 or bonus_days > 365:
+ await message.answer(_(
+ "admin_bulk_promo_invalid_bonus_days",
+ default="β ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
+ ))
+ return
+
+ data = await state.get_data()
+ await state.update_data(bonus_days=bonus_days)
+
+ # Step 3: Ask for max activations
+ prompt_text = _(
+ "admin_bulk_promo_step3_max_activations",
+ default="π¦ ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 3 ΠΈΠ· 4: ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ\n\nΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ: {quantity}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ Π΄Π»Ρ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (1-10000):",
+ quantity=data['quantity'],
+ bonus_days=bonus_days
+ )
+
+ await message.answer(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ await state.set_state(AdminStates.waiting_for_bulk_promo_max_activations)
+
+ except ValueError:
+ await message.answer(_(
+ "admin_bulk_promo_invalid_bonus_days",
+ default="β ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
+ ))
+
+# Step 3: Process max activations
+@router.message(AdminStates.waiting_for_bulk_promo_max_activations, F.text)
+async def process_bulk_promo_max_activations_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ max_activations = int(message.text.strip())
+ if max_activations <= 0 or max_activations > 10000:
+ await message.answer(_(
+ "admin_bulk_promo_invalid_max_activations",
+ default="β ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 10000"
+ ))
+ return
+
+ data = await state.get_data()
+ await state.update_data(max_activations=max_activations)
+
+ # Step 4: Ask for validity days (optional)
+ keyboard = InlineKeyboardBuilder()
+ keyboard.row(
+ InlineKeyboardButton(
+ text=_("admin_bulk_promo_unlimited_validity", default="βΎοΈ ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ"),
+ callback_data="bulk_promo_unlimited_validity"
+ )
+ )
+ keyboard.row(
+ InlineKeyboardButton(
+ text=_("admin_bulk_promo_set_validity", default="β° Π£ΡΡΠ°Π½ΠΎΠ²ΠΈΡΡ ΡΡΠΎΠΊ"),
+ callback_data="bulk_promo_set_validity"
+ )
+ )
+ keyboard.row(
+ InlineKeyboardButton(
+ text=_("admin_panel_back_button", default="β¬
οΈ ΠΠ°Π·Π°Π΄"),
+ callback_data="admin_panel_back"
+ )
+ )
+
+ prompt_text = _(
+ "admin_bulk_promo_step4_validity",
+ default="π¦ ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 4 ΠΈΠ· 4: Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ\n\nΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ: {quantity}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_activations}\n\nΠΡΠ±Π΅ΡΠΈΡΠ΅ ΡΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²:",
+ quantity=data['quantity'],
+ bonus_days=data['bonus_days'],
+ max_activations=max_activations
+ )
+
+ await message.answer(
+ prompt_text,
+ reply_markup=keyboard.as_markup(),
+ parse_mode="HTML"
+ )
+ await state.set_state(AdminStates.waiting_for_bulk_promo_validity_days)
+
+ except ValueError:
+ await message.answer(_(
+ "admin_bulk_promo_invalid_max_activations",
+ default="β ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 10000"
+ ))
+
+# Handle validity choice buttons
+@router.callback_query(F.data == "bulk_promo_unlimited_validity", StateFilter(AdminStates.waiting_for_bulk_promo_validity_days))
+async def process_bulk_promo_unlimited_validity(callback: types.CallbackQuery,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ await state.update_data(validity_days=None)
+ await create_bulk_promo_codes_final(callback, state, i18n_data, settings, session)
+
+@router.callback_query(F.data == "bulk_promo_set_validity", StateFilter(AdminStates.waiting_for_bulk_promo_validity_days))
+async def process_bulk_promo_set_validity(callback: types.CallbackQuery,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await callback.answer("Language service error.", show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ prompt_text = _(
+ "admin_bulk_promo_enter_validity_days",
+ default="β° ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄Π½Π΅ΠΉ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² (1-365):"
+ )
+
+ try:
+ await callback.message.edit_text(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
+ )
+ except Exception as e:
+ await callback.message.answer(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
+ )
+ await callback.answer()
+
+# Step 4: Process validity days (when user chooses to set)
+@router.message(AdminStates.waiting_for_bulk_promo_validity_days, F.text)
+async def process_bulk_promo_validity_days_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ validity_days = int(message.text.strip())
+ if validity_days <= 0 or validity_days > 365:
+ await message.answer(_(
+ "admin_bulk_promo_invalid_validity_days",
+ default="β ΠΠ½ΠΈ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
+ ))
+ return
+
+ await state.update_data(validity_days=validity_days)
+
+ # Create a fake callback for consistency
+ fake_callback = types.CallbackQuery(
+ id="fake",
+ from_user=message.from_user,
+ chat_instance="fake",
+ message=message,
+ data="fake"
+ )
+
+ await create_bulk_promo_codes_final(fake_callback, state, i18n_data, settings, session)
+
+ except ValueError:
+ await message.answer(_(
+ "admin_bulk_promo_invalid_validity_days",
+ default="β ΠΠ½ΠΈ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
+ ))
+
+async def create_bulk_promo_codes_final(callback_or_message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ """Final step to create all promo codes"""
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ if hasattr(callback_or_message, 'answer'):
+ await callback_or_message.answer("Language service error.", show_alert=True)
+ else:
+ await callback_or_message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ data = await state.get_data()
+ quantity = data['quantity']
+ bonus_days = data['bonus_days']
+ max_activations = data['max_activations']
+ validity_days = data.get('validity_days')
+
+ # Create validity date
+ valid_until_date: Optional[datetime] = None
+ valid_until_str_display = _("admin_promo_valid_indefinitely", default="ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ")
+
+ if validity_days:
+ valid_until_date = datetime.now(timezone.utc) + timedelta(days=validity_days)
+ valid_until_str_display = valid_until_date.strftime('%Y-%m-%d')
+
+ # Show creating message
+ creating_text = _(
+ "admin_bulk_promo_creating",
+ default="β³ Π‘ΠΎΠ·Π΄Π°Ρ {quantity} ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²...",
+ quantity=quantity
+ )
+
+ if hasattr(callback_or_message, 'message'):
+ try:
+ await callback_or_message.message.edit_text(creating_text)
+ except:
+ await callback_or_message.message.answer(creating_text)
+ if hasattr(callback_or_message, 'answer'):
+ await callback_or_message.answer()
+ else:
+ await callback_or_message.answer(creating_text)
+
+ # Generate and create promo codes
+ created_codes = []
+ try:
+ for i in range(quantity):
+ # Generate unique code
+ promo_code = generate_unique_promo_code()
+
+ # Ensure uniqueness
+ existing_promo = await promo_code_dal.get_promo_code_by_code(session, promo_code)
+ retries = 0
+ while existing_promo and retries < 10:
+ promo_code = generate_unique_promo_code()
+ existing_promo = await promo_code_dal.get_promo_code_by_code(session, promo_code)
+ retries += 1
+
+ if retries >= 10:
+ raise Exception("ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΡΠΎΠ·Π΄Π°ΡΡ ΡΠ½ΠΈΠΊΠ°Π»ΡΠ½ΡΠΉ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄")
+
+ # Create promo code
+ promo_data = {
+ "code": promo_code,
+ "bonus_days": bonus_days,
+ "max_activations": max_activations,
+ "current_activations": 0,
+ "is_active": True,
+ "valid_until": valid_until_date,
+ "description": f"Bulk created #{i+1}/{quantity}"
+ }
+
+ await promo_code_dal.create_promo_code(session, promo_data)
+ created_codes.append(promo_code)
+
+ await session.commit()
+
+ # Send success message
+ codes_text = " ".join(created_codes)
+ success_text = _(
+ "admin_bulk_promo_created_success",
+ default="β
Π‘ΠΎΠ·Π΄Π°Π½ΠΎ {count} ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² Π½Π° {days} Π΄Π½Π΅ΠΉ!\n\nΠΠ΅ΠΉΡΡΠ²ΡΡΡ Π΄ΠΎ: {validity}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_act}\n\nΠΡΠΎΠΌΠΎΠΊΠΎΠ΄Ρ:\n{codes}",
+ count=quantity,
+ days=bonus_days,
+ validity=valid_until_str_display,
+ max_act=max_activations,
+ codes=codes_text[:3500] + "..." if len(codes_text) > 3500 else codes_text
+ )
+
+ if hasattr(callback_or_message, 'message'):
+ target_message = callback_or_message.message
+ else:
+ target_message = callback_or_message
+
+ await target_message.answer(
+ success_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+
+ # Send codes as file if too many
+ if len(codes_text) > 3500:
+ codes_file_content = "\n".join(created_codes)
+ codes_file = types.BufferedInputFile(
+ codes_file_content.encode('utf-8'),
+ filename=f"bulk_promo_codes_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
+ )
+
+ await target_message.answer_document(
+ codes_file,
+ caption=_(
+ "admin_bulk_promo_codes_file",
+ default="π ΠΡΠ΅ ΡΠΎΠ·Π΄Π°Π½Π½ΡΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Ρ Π² ΡΠ°ΠΉΠ»Π΅",
+ count=quantity
+ )
+ )
+
+ except Exception as e:
+ logging.error(f"Error creating bulk promo codes: {e}")
+ await session.rollback()
+ error_text = _(
+ "admin_bulk_promo_creation_failed",
+ default="β ΠΡΠΈΠ±ΠΊΠ° ΡΠΎΠ·Π΄Π°Π½ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²: {error}",
+ error=str(e)
+ )
+
+ if hasattr(callback_or_message, 'message'):
+ target_message = callback_or_message.message
+ else:
+ target_message = callback_or_message
+
+ await target_message.answer(
+ error_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
+ )
+
+ await state.clear()
+
@router.message(AdminStates.waiting_for_bulk_promo_details, F.text)
async def process_bulk_promo_details_handler(message: types.Message,
state: FSMContext,
diff --git a/bot/handlers/admin/statistics.py b/bot/handlers/admin/statistics.py
index 93d7b32..bcc7dce 100644
--- a/bot/handlers/admin/statistics.py
+++ b/bot/handlers/admin/statistics.py
@@ -8,6 +8,7 @@ from config.settings import Settings
from db.dal import user_dal, payment_dal, panel_sync_dal
from db.models import Payment, PanelSyncStatus
+from bot.services.panel_api_service import PanelApiService
from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard
from bot.middlewares.i18n import JsonI18n
@@ -137,6 +138,49 @@ async def show_statistics_handler(callback: types.CallbackQuery,
else:
stats_text_parts.append(f"\n{_('admin_sync_status_never_run')}")
+ # Panel Statistics
+ try:
+ panel_service = PanelApiService(settings)
+
+ # Get panel system statistics
+ panel_stats = await panel_service.get_panel_statistics()
+ nodes_stats = await panel_service.get_nodes_statistics()
+ online_count = await panel_service.get_online_users_count()
+ users_activity = await panel_service.get_users_activity_stats()
+
+ stats_text_parts.append(f"\nπ₯ {_('admin_panel_stats_header', default='Π‘ΡΠ°ΡΠΈΡΡΠΈΠΊΠ° ΠΏΠ°Π½Π΅Π»ΠΈ')}")
+
+ if online_count is not None:
+ stats_text_parts.append(f"π’ ΠΠ½Π»Π°ΠΉΠ½ ΡΠ΅ΠΉΡΠ°Ρ: {online_count}")
+
+ if users_activity:
+ today_connected = users_activity.get('today_connected', 'N/A')
+ week_connected = users_activity.get('week_connected', 'N/A')
+ never_connected = users_activity.get('never_connected', 'N/A')
+ stats_text_parts.append(f"π
ΠΠΎΠ΄ΠΊΠ»ΡΡΠ°Π»ΠΈΡΡ ΡΠ΅Π³ΠΎΠ΄Π½Ρ: {today_connected}")
+ stats_text_parts.append(f"π
ΠΠΎΠ΄ΠΊΠ»ΡΡΠ°Π»ΠΈΡΡ Π·Π° Π½Π΅Π΄Π΅Π»Ρ: {week_connected}")
+ stats_text_parts.append(f"β ΠΠΈΠΊΠΎΠ³Π΄Π° Π½Π΅ ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ°Π»ΠΈΡΡ: {never_connected}")
+
+ if nodes_stats:
+ active_nodes = len([node for node in nodes_stats if node.get('status') == 'active'])
+ total_nodes = len(nodes_stats)
+ stats_text_parts.append(f"π ΠΠΊΡΠΈΠ²Π½ΡΡ
Π½ΠΎΠ΄: {active_nodes}/{total_nodes}")
+
+ if panel_stats:
+ system_info = panel_stats.get('system', {})
+ if system_info:
+ memory_usage = system_info.get('memory_usage_percent', 'N/A')
+ cpu_usage = system_info.get('cpu_usage_percent', 'N/A')
+ stats_text_parts.append(f"πΎ ΠΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΠ΅ RAM: {memory_usage}%")
+ stats_text_parts.append(f"π ΠΠ°Π³ΡΡΠ·ΠΊΠ° CPU: {cpu_usage}%")
+
+ await panel_service.close()
+
+ except Exception as e:
+ logging.warning(f"Failed to fetch panel statistics: {e}")
+ stats_text_parts.append(f"\nπ₯ Π‘ΡΠ°ΡΠΈΡΡΠΈΠΊΠ° ΠΏΠ°Π½Π΅Π»ΠΈ")
+ stats_text_parts.append("β ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΠΏΠΎΠ»ΡΡΠΈΡΡ Π΄Π°Π½Π½ΡΠ΅ Ρ ΠΏΠ°Π½Π΅Π»ΠΈ")
+
final_text = "\n".join(stats_text_parts)
try:
diff --git a/bot/services/panel_api_service.py b/bot/services/panel_api_service.py
index 9ffb6d7..a09f499 100644
--- a/bot/services/panel_api_service.py
+++ b/bot/services/panel_api_service.py
@@ -460,3 +460,43 @@ class PanelApiService:
async def get_bot_db_last_sync_status(
self, session: AsyncSession) -> Optional[PanelSyncStatus]:
return await panel_sync_dal.get_panel_sync_status(session)
+
+
+ async def get_panel_statistics(self) -> Optional[Dict[str, Any]]:
+ """Get general panel statistics"""
+ response_data = await self._request("GET", "/admin/stats", log_full_response=False)
+ if response_data and not response_data.get("error") and "response" in response_data:
+ return response_data.get("response")
+ return None
+
+
+ async def get_nodes_statistics(self) -> Optional[List[Dict[str, Any]]]:
+ """Get nodes statistics"""
+ response_data = await self._request("GET", "/admin/nodes/stats", log_full_response=False)
+ if response_data and not response_data.get("error") and "response" in response_data:
+ return response_data.get("response", {}).get("nodes", [])
+ return None
+
+
+ async def get_system_info(self) -> Optional[Dict[str, Any]]:
+ """Get system information"""
+ response_data = await self._request("GET", "/admin/system/info", log_full_response=False)
+ if response_data and not response_data.get("error") and "response" in response_data:
+ return response_data.get("response")
+ return None
+
+
+ async def get_online_users_count(self) -> Optional[int]:
+ """Get count of currently online users"""
+ response_data = await self._request("GET", "/admin/stats/online-users", log_full_response=False)
+ if response_data and not response_data.get("error") and "response" in response_data:
+ return response_data.get("response", {}).get("count", 0)
+ return None
+
+
+ async def get_users_activity_stats(self) -> Optional[Dict[str, Any]]:
+ """Get users activity statistics (today, week, never connected)"""
+ response_data = await self._request("GET", "/admin/stats/users-activity", log_full_response=False)
+ if response_data and not response_data.get("error") and "response" in response_data:
+ return response_data.get("response")
+ return None
\ No newline at end of file
diff --git a/bot/states/admin_states.py b/bot/states/admin_states.py
index e7b4c63..21ee390 100644
--- a/bot/states/admin_states.py
+++ b/bot/states/admin_states.py
@@ -6,8 +6,17 @@ class AdminStates(StatesGroup):
waiting_for_broadcast_message = State()
confirming_broadcast = State()
waiting_for_promo_details = State()
+ waiting_for_promo_code = State()
+ waiting_for_promo_bonus_days = State()
+ waiting_for_promo_max_activations = State()
+ waiting_for_promo_validity_days = State()
+ waiting_for_promo_description = State()
waiting_for_promo_edit_details = State()
waiting_for_bulk_promo_details = State()
+ waiting_for_bulk_promo_quantity = State()
+ waiting_for_bulk_promo_bonus_days = State()
+ waiting_for_bulk_promo_max_activations = State()
+ waiting_for_bulk_promo_validity_days = State()
waiting_for_user_id_to_ban = State()
waiting_for_user_id_to_unban = State()
diff --git a/locales/en.json b/locales/en.json
index 9290905..f02392b 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -323,5 +323,42 @@
"admin_csv_header_raw_update_preview": "Update Preview",
"admin_banned_users_empty": "π Banned Users\n\nList is empty",
- "admin_banned_users_list": "π Banned Users ({count}):\n\n{users}"
+ "admin_banned_users_list": "π Banned Users ({count}):\n\n{users}",
+
+ "admin_panel_stats_header": "Panel Statistics",
+ "admin_bulk_promo_invalid_values": "β Invalid values: {error}",
+
+ "admin_bulk_promo_step1_quantity": "π¦ Bulk Promo Code Creation\n\nStep 1 of 4: Quantity\n\nEnter the number of promo codes to create (1-1000):",
+ "admin_bulk_promo_step2_bonus_days": "π¦ Bulk Promo Code Creation\n\nStep 2 of 4: Bonus Days\n\nQuantity: {quantity}\n\nEnter the number of bonus days (1-365):",
+ "admin_bulk_promo_step3_max_activations": "π¦ Bulk Promo Code Creation\n\nStep 3 of 4: Max Activations\n\nQuantity: {quantity}\nBonus days: {bonus_days}\n\nEnter the maximum number of activations for each promo code (1-10000):",
+ "admin_bulk_promo_step4_validity": "π¦ Bulk Promo Code Creation\n\nStep 4 of 4: Validity Period\n\nQuantity: {quantity}\nBonus days: {bonus_days}\nMax activations: {max_activations}\n\nChoose the validity period for promo codes:",
+
+ "admin_bulk_promo_invalid_quantity": "β Quantity must be between 1 and 1000",
+ "admin_bulk_promo_invalid_bonus_days": "β Bonus days must be between 1 and 365",
+ "admin_bulk_promo_invalid_max_activations": "β Max activations must be between 1 and 10000",
+ "admin_bulk_promo_invalid_validity_days": "β Validity days must be between 1 and 365",
+
+ "admin_bulk_promo_unlimited_validity": "βΎοΈ Unlimited",
+ "admin_bulk_promo_set_validity": "β° Set Period",
+ "admin_bulk_promo_enter_validity_days": "β° Enter the number of validity days for promo codes (1-365):",
+
+ "admin_bulk_promo_creating": "β³ Creating {quantity} promo codes...",
+
+ "admin_promo_step1_code": "π Create Promo Code\n\nStep 1 of 5: Promo Code\n\nEnter promo code (3-30 characters, letters and numbers only):",
+ "admin_promo_step2_bonus_days": "π Create Promo Code\n\nStep 2 of 5: Bonus Days\n\nCode: {code}\n\nEnter the number of bonus days (1-365):",
+ "admin_promo_step3_max_activations": "π Create Promo Code\n\nStep 3 of 5: Max Activations\n\nCode: {code}\nBonus days: {bonus_days}\n\nEnter the maximum number of activations (1-10000):",
+ "admin_promo_step4_validity": "π Create Promo Code\n\nStep 4 of 5: Validity Period\n\nCode: {code}\nBonus days: {bonus_days}\nMax activations: {max_activations}\n\nChoose the validity period for the promo code:",
+ "admin_promo_step5_description": "π Create Promo Code\n\nStep 5 of 5: Description (optional)\n\nCode: {code}\nBonus days: {bonus_days}\nMax activations: {max_activations}\nValid until: {validity}\n\nEnter promo code description or click \"Skip\":",
+
+ "admin_promo_invalid_code_format": "β Promo code must contain 3-30 characters (letters and numbers only)",
+ "admin_promo_code_already_exists": "β A promo code with this code already exists",
+ "admin_promo_processing_error": "β Error processing promo code",
+ "admin_promo_unlimited_validity": "βΎοΈ Unlimited",
+ "admin_promo_set_validity": "β° Set Period",
+ "admin_promo_enter_validity_days": "β° Enter the number of validity days for the promo code (1-365):",
+ "admin_promo_skip_description": "βοΈ Skip",
+ "admin_promo_description_too_long": "β Description must not exceed 200 characters",
+ "admin_promo_creating": "β³ Creating promo code...",
+ "admin_promo_created_success": "β
Promo code created!\n\nπ Code: {code}\nπ Bonus days: {days}\nπ’ Max activations: {max_act}\nβ° Valid until: {validity}\nπ Description: {description}",
+ "admin_promo_creation_failed": "β Error creating promo code: {error}"
}
diff --git a/locales/ru.json b/locales/ru.json
index 220cf96..a0c6bbf 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -323,5 +323,42 @@
"admin_csv_header_raw_update_preview": "ΠΡΠ΅Π²ΡΡ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΡ",
"admin_banned_users_empty": "π ΠΠ°Π±Π»ΠΎΠΊΠΈΡΠΎΠ²Π°Π½Π½ΡΠ΅ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»ΠΈ\n\nΠ‘ΠΏΠΈΡΠΎΠΊ ΠΏΡΡΡ",
- "admin_banned_users_list": "π ΠΠ°Π±Π»ΠΎΠΊΠΈΡΠΎΠ²Π°Π½Π½ΡΠ΅ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»ΠΈ ({count}):\n\n{users}"
+ "admin_banned_users_list": "π ΠΠ°Π±Π»ΠΎΠΊΠΈΡΠΎΠ²Π°Π½Π½ΡΠ΅ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»ΠΈ ({count}):\n\n{users}",
+
+ "admin_panel_stats_header": "Π‘ΡΠ°ΡΠΈΡΡΠΈΠΊΠ° ΠΏΠ°Π½Π΅Π»ΠΈ",
+ "admin_bulk_promo_invalid_values": "β ΠΠ΅Π²Π΅ΡΠ½ΡΠ΅ Π·Π½Π°ΡΠ΅Π½ΠΈΡ: {error}",
+
+ "admin_bulk_promo_step1_quantity": "π¦ ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 1 ΠΈΠ· 4: ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² Π΄Π»Ρ ΡΠΎΠ·Π΄Π°Π½ΠΈΡ (1-1000):",
+ "admin_bulk_promo_step2_bonus_days": "π¦ ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 2 ΠΈΠ· 4: ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ\n\nΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ: {quantity}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π±ΠΎΠ½ΡΡΠ½ΡΡ
Π΄Π½Π΅ΠΉ (1-365):",
+ "admin_bulk_promo_step3_max_activations": "π¦ ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 3 ΠΈΠ· 4: ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ\n\nΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ: {quantity}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ Π΄Π»Ρ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (1-10000):",
+ "admin_bulk_promo_step4_validity": "π¦ ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 4 ΠΈΠ· 4: Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ\n\nΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ: {quantity}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_activations}\n\nΠΡΠ±Π΅ΡΠΈΡΠ΅ ΡΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²:",
+
+ "admin_bulk_promo_invalid_quantity": "β ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 1000",
+ "admin_bulk_promo_invalid_bonus_days": "β ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365",
+ "admin_bulk_promo_invalid_max_activations": "β ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 10000",
+ "admin_bulk_promo_invalid_validity_days": "β ΠΠ½ΠΈ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365",
+
+ "admin_bulk_promo_unlimited_validity": "βΎοΈ ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ",
+ "admin_bulk_promo_set_validity": "β° Π£ΡΡΠ°Π½ΠΎΠ²ΠΈΡΡ ΡΡΠΎΠΊ",
+ "admin_bulk_promo_enter_validity_days": "β° ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄Π½Π΅ΠΉ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² (1-365):",
+
+ "admin_bulk_promo_creating": "β³ Π‘ΠΎΠ·Π΄Π°Ρ {quantity} ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²...",
+
+ "admin_promo_step1_code": "π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 1 ΠΈΠ· 5: ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ², ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ):",
+ "admin_promo_step2_bonus_days": "π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 2 ΠΈΠ· 5: ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ\n\nΠΠΎΠ΄: {code}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π±ΠΎΠ½ΡΡΠ½ΡΡ
Π΄Π½Π΅ΠΉ (1-365):",
+ "admin_promo_step3_max_activations": "π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 3 ΠΈΠ· 5: ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ (1-10000):",
+ "admin_promo_step4_validity": "π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 4 ΠΈΠ· 5: Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_activations}\n\nΠΡΠ±Π΅ΡΠΈΡΠ΅ ΡΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°:",
+ "admin_promo_step5_description": "π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 5 ΠΈΠ· 5: ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ (ΠΎΠΏΡΠΈΠΎΠ½Π°Π»ΡΠ½ΠΎ)\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_activations}\nΠΠ΅ΠΉΡΡΠ²ΡΠ΅Ρ: {validity}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΎΠΏΠΈΡΠ°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° ΠΈΠ»ΠΈ Π½Π°ΠΆΠΌΠΈΡΠ΅ \"ΠΡΠΎΠΏΡΡΡΠΈΡΡ\":",
+
+ "admin_promo_invalid_code_format": "β ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΠΎΠ΄Π΅ΡΠΆΠ°ΡΡ 3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ² (ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ)",
+ "admin_promo_code_already_exists": "β ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ Ρ ΡΠ°ΠΊΠΈΠΌ ΠΊΠΎΠ΄ΠΎΠΌ ΡΠΆΠ΅ ΡΡΡΠ΅ΡΡΠ²ΡΠ΅Ρ",
+ "admin_promo_processing_error": "β ΠΡΠΈΠ±ΠΊΠ° ΠΎΠ±ΡΠ°Π±ΠΎΡΠΊΠΈ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°",
+ "admin_promo_unlimited_validity": "βΎοΈ ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ",
+ "admin_promo_set_validity": "β° Π£ΡΡΠ°Π½ΠΎΠ²ΠΈΡΡ ΡΡΠΎΠΊ",
+ "admin_promo_enter_validity_days": "β° ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄Π½Π΅ΠΉ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (1-365):",
+ "admin_promo_skip_description": "βοΈ ΠΡΠΎΠΏΡΡΡΠΈΡΡ",
+ "admin_promo_description_too_long": "β ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ Π½Π΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ ΠΏΡΠ΅Π²ΡΡΠ°ΡΡ 200 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ²",
+ "admin_promo_creating": "β³ Π‘ΠΎΠ·Π΄Π°Ρ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄...",
+ "admin_promo_created_success": "β
ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ ΡΠΎΠ·Π΄Π°Π½!\n\nπ ΠΠΎΠ΄: {code}\nπ ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {days}\nπ’ ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_act}\nβ° ΠΠ΅ΠΉΡΡΠ²ΡΠ΅Ρ Π΄ΠΎ: {validity}\nπ ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅: {description}",
+ "admin_promo_creation_failed": "β ΠΡΠΈΠ±ΠΊΠ° ΡΠΎΠ·Π΄Π°Π½ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°: {error}"
}