diff --git a/bot/handlers/admin/promo_codes.py b/bot/handlers/admin/promo_codes.py
index 9775ecf..b230ec8 100644
--- a/bot/handlers/admin/promo_codes.py
+++ b/bot/handlers/admin/promo_codes.py
@@ -35,7 +35,7 @@ async def create_promo_prompt_handler(callback: types.CallbackQuery,
# Step 1: Ask for promo code
prompt_text = _(
"admin_promo_step1_code",
- default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 1 ΠΈΠ· 5: ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ², ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ):"
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 1 ΠΈΠ· 4: ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ², ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ):"
)
try:
@@ -91,7 +91,7 @@ async def process_promo_code_handler(message: types.Message,
# 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):",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 2 ΠΈΠ· 4: ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ\n\nΠΠΎΠ΄: {code}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π±ΠΎΠ½ΡΡΠ½ΡΡ
Π΄Π½Π΅ΠΉ (1-365):",
code=code_str
)
@@ -137,7 +137,7 @@ async def process_promo_bonus_days_handler(message: types.Message,
# 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):",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 3 ΠΈΠ· 4: ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ (1-10000):",
code=data['promo_code'],
bonus_days=bonus_days
)
@@ -203,7 +203,7 @@ async def process_promo_max_activations_handler(message: types.Message,
prompt_text = _(
"admin_promo_step4_validity",
- default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 4 ΠΈΠ· 5: Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_activations}\n\nΠΡΠ±Π΅ΡΠΈΡΠ΅ ΡΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°:",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 4 ΠΈΠ· 4: Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_activations}\n\nΠΡΠ±Π΅ΡΠΈΡΠ΅ ΡΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°:",
code=data['promo_code'],
bonus_days=data['bonus_days'],
max_activations=max_activations
@@ -227,9 +227,10 @@ async def process_promo_max_activations_handler(message: types.Message,
async def process_promo_unlimited_validity(callback: types.CallbackQuery,
state: FSMContext,
i18n_data: dict,
- settings: Settings):
+ settings: Settings,
+ session: AsyncSession):
await state.update_data(validity_days=None)
- await ask_for_promo_description(callback, state, i18n_data, settings)
+ await create_promo_code_final(callback, state, i18n_data, settings, session)
@router.callback_query(F.data == "promo_set_validity", StateFilter(AdminStates.waiting_for_promo_validity_days))
async def process_promo_set_validity(callback: types.CallbackQuery,
@@ -293,7 +294,7 @@ async def process_promo_validity_days_handler(message: types.Message,
data="fake"
)
- await ask_for_promo_description(fake_callback, state, i18n_data, settings)
+ await create_promo_code_final(fake_callback, state, i18n_data, settings, session)
except ValueError:
await message.answer(_(
@@ -301,116 +302,7 @@ async def process_promo_validity_days_handler(message: types.Message,
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 callback_or_message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- 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)
+# Description handlers removed - not needed for this model
async def create_promo_code_final(callback_or_message,
state: FSMContext,
@@ -433,7 +325,6 @@ async def create_promo_code_final(callback_or_message,
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
@@ -468,7 +359,7 @@ async def create_promo_code_final(callback_or_message,
"current_activations": 0,
"is_active": True,
"valid_until": valid_until_date,
- "description": description
+ "created_by_admin_id": callback_or_message.from_user.id if hasattr(callback_or_message, 'from_user') and callback_or_message.from_user else 0
}
await promo_code_dal.create_promo_code(session, promo_data)
@@ -477,12 +368,11 @@ async def create_promo_code_final(callback_or_message,
# Send success message
success_text = _(
"admin_promo_created_success",
- default="β
ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ ΡΠΎΠ·Π΄Π°Π½!\n\nπ ΠΠΎΠ΄: {code}\nπ ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {days}\nπ’ ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_act}\nβ° ΠΠ΅ΠΉΡΡΠ²ΡΠ΅Ρ Π΄ΠΎ: {validity}\nπ ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅: {description}",
+ default="β
ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ ΡΠΎΠ·Π΄Π°Π½!\n\nπ ΠΠΎΠ΄: {code}\nπ ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {days}\nπ’ ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_act}\nβ° ΠΠ΅ΠΉΡΡΠ²ΡΠ΅Ρ Π΄ΠΎ: {validity}",
code=promo_code,
days=bonus_days,
max_act=max_activations,
- validity=valid_until_str_display,
- description=description or "ΠΠ΅ ΡΠΊΠ°Π·Π°Π½ΠΎ"
+ validity=valid_until_str_display
)
if hasattr(callback_or_message, 'message'):
@@ -721,9 +611,7 @@ async def promo_delete_handler(callback: types.CallbackQuery, i18n_data: dict,
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,
@@ -1108,7 +996,7 @@ async def create_bulk_promo_codes_final(callback_or_message,
"current_activations": 0,
"is_active": True,
"valid_until": valid_until_date,
- "description": f"Bulk created #{i+1}/{quantity}"
+ "created_by_admin_id": callback_or_message.from_user.id if hasattr(callback_or_message, 'from_user') and callback_or_message.from_user else 0
}
await promo_code_dal.create_promo_code(session, promo_data)
@@ -1177,177 +1065,7 @@ async def create_bulk_promo_codes_final(callback_or_message,
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,
- 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)
-
- 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_bulk_promo_invalid_format",
- default="β ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ΡΠΎΡΠΌΠ°Ρ. ΠΡΠΏΠΎΠ»ΡΠ·ΡΠΉΡΠ΅: <ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ> <Π±ΠΎΠ½ΡΡΠ½ΡΠ΅_Π΄Π½ΠΈ> <ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅_ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΠ΅> [Π΄Π½ΠΈ_Π΄Π΅ΠΉΡΡΠ²ΠΈΡ]"
- ))
- return
-
- try:
- count = int(parts[0])
- bonus_days = int(parts[1])
- max_activations = int(parts[2])
-
- if count <= 0 or count > 1000:
- raise ValueError(_("admin_bulk_promo_invalid_count", default="ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 1000"))
-
- if bonus_days <= 0 or max_activations <= 0:
- raise ValueError(_("admin_promo_invalid_bonus_or_activations"))
-
- 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'))
-
- except ValueError as e:
- await message.answer(_(
- "admin_bulk_promo_invalid_values",
- default="β ΠΠ΅Π²Π΅ΡΠ½ΡΠ΅ Π·Π½Π°ΡΠ΅Π½ΠΈΡ: {error}",
- error=str(e)
- ))
- return
- except Exception as e_parse:
- logging.error(f"Error parsing bulk 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
-
- # Generate unique codes and create promo codes
- created_codes = []
- failed_codes = []
-
- await message.answer(_(
- "admin_bulk_promo_creating",
- default="π Π‘ΠΎΠ·Π΄Π°Ρ {count} ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²...",
- count=count
- ))
-
- for i in range(count):
- try:
- # Generate unique code
- attempts = 0
- while attempts < 10: # Max 10 attempts to generate unique code
- code = generate_unique_promo_code()
-
- # Check if code already exists
- existing_promo = await promo_code_dal.get_promo_code_by_code(session, code)
- if not existing_promo:
- break
- attempts += 1
-
- if attempts >= 10:
- failed_codes.append(f"Failed to generate unique code #{i+1}")
- continue
-
- promo_data_to_create = {
- "code": code,
- "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
- }
-
- created_promo = await promo_code_dal.create_promo_code(
- session, promo_data_to_create)
-
- if created_promo:
- created_codes.append(created_promo.code)
- else:
- failed_codes.append(f"Failed to create code #{i+1}")
-
- except Exception as e:
- logging.error(f"Error creating bulk promo code #{i+1}: {e}")
- failed_codes.append(f"Error creating code #{i+1}: {str(e)}")
-
- try:
- await session.commit()
-
- # Prepare success message
- success_text_parts = [
- _(
- "admin_bulk_promo_created_success",
- default="β
ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ Π·Π°Π²Π΅ΡΡΠ΅Π½ΠΎ!\n\nπ¦ Π‘ΠΎΠ·Π΄Π°Π½ΠΎ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²: {created_count}\nπ ΠΠΎΠ½ΡΡΠ½ΡΡ
Π΄Π½Π΅ΠΉ: {bonus_days}\nπ ΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ: {max_activations}\nβ° ΠΠ΅ΠΉΡΡΠ²ΠΈΡΠ΅Π»ΡΠ½Ρ Π΄ΠΎ: {valid_until}",
- created_count=len(created_codes),
- bonus_days=bonus_days,
- max_activations=max_activations,
- valid_until=valid_until_str_display
- )
- ]
-
- if failed_codes:
- success_text_parts.append(f"\nβ ΠΡΠΈΠ±ΠΎΠΊ: {len(failed_codes)}")
-
- if created_codes:
- # Show first few codes as examples
- codes_to_show = created_codes[:10] # Show first 10
- success_text_parts.append(f"\nπ ΠΡΠΈΠΌΠ΅ΡΡ ΡΠΎΠ·Π΄Π°Π½Π½ΡΡ
ΠΊΠΎΠ΄ΠΎΠ²:")
- success_text_parts.append("\n".join([f"β’ {code}" for code in codes_to_show]))
-
- if len(created_codes) > 10:
- success_text_parts.append(f"... ΠΈ Π΅ΡΠ΅ {len(created_codes) - 10} ΠΊΠΎΠ΄ΠΎΠ²")
-
- # Send codes as a file if many were created
- if len(created_codes) > 20:
- try:
- codes_text = "\n".join(created_codes)
- codes_file = types.BufferedInputFile(
- codes_text.encode('utf-8'),
- filename=f"promo_codes_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
- )
- await message.answer_document(
- codes_file,
- caption=_(
- "admin_bulk_promo_codes_file",
- default="π Π€Π°ΠΉΠ» ΡΠΎ Π²ΡΠ΅ΠΌΠΈ ΡΠΎΠ·Π΄Π°Π½Π½ΡΠΌΠΈ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°ΠΌΠΈ"
- )
- )
- except Exception as e:
- logging.error(f"Failed to send codes file: {e}")
-
- final_text = "\n".join(success_text_parts)
- await message.answer(
- final_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
-
- except Exception as e_db_commit:
- await session.rollback()
- logging.error(f"Failed to commit bulk promo codes creation: {e_db_commit}", exc_info=True)
- await message.answer(_(
- "admin_bulk_promo_creation_failed",
- default="β ΠΡΠΈΠ±ΠΊΠ° ΠΏΡΠΈ ΡΠΎΡ
ΡΠ°Π½Π΅Π½ΠΈΠΈ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² Π² Π±Π°Π·Ρ Π΄Π°Π½Π½ΡΡ
"
- ))
-
- await state.clear()
+# Old bulk promo handler removed - now using step-by-step process
diff --git a/bot/handlers/admin/statistics.py b/bot/handlers/admin/statistics.py
index bcc7dce..d7c1af4 100644
--- a/bot/handlers/admin/statistics.py
+++ b/bot/handlers/admin/statistics.py
@@ -139,20 +139,27 @@ async def show_statistics_handler(callback: types.CallbackQuery,
stats_text_parts.append(f"\n{_('admin_sync_status_never_run')}")
# Panel Statistics
+ stats_text_parts.append(f"\nπ₯ {_('admin_panel_stats_header', default='Π‘ΡΠ°ΡΠΈΡΡΠΈΠΊΠ° ΠΏΠ°Π½Π΅Π»ΠΈ')}")
+
try:
panel_service = PanelApiService(settings)
# Get panel system statistics
+ logging.info("Fetching panel 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='Π‘ΡΠ°ΡΠΈΡΡΠΈΠΊΠ° ΠΏΠ°Π½Π΅Π»ΠΈ')}")
+ logging.info(f"Panel stats response: panel_stats={panel_stats}, nodes_stats={nodes_stats}, online_count={online_count}, users_activity={users_activity}")
+ # Online users
if online_count is not None:
stats_text_parts.append(f"π’ ΠΠ½Π»Π°ΠΉΠ½ ΡΠ΅ΠΉΡΠ°Ρ: {online_count}")
+ else:
+ stats_text_parts.append(f"π’ ΠΠ½Π»Π°ΠΉΠ½ ΡΠ΅ΠΉΡΠ°Ρ: N/A")
+ # Users activity
if users_activity:
today_connected = users_activity.get('today_connected', 'N/A')
week_connected = users_activity.get('week_connected', 'N/A')
@@ -160,12 +167,18 @@ async def show_statistics_handler(callback: types.CallbackQuery,
stats_text_parts.append(f"π
ΠΠΎΠ΄ΠΊΠ»ΡΡΠ°Π»ΠΈΡΡ ΡΠ΅Π³ΠΎΠ΄Π½Ρ: {today_connected}")
stats_text_parts.append(f"π
ΠΠΎΠ΄ΠΊΠ»ΡΡΠ°Π»ΠΈΡΡ Π·Π° Π½Π΅Π΄Π΅Π»Ρ: {week_connected}")
stats_text_parts.append(f"β ΠΠΈΠΊΠΎΠ³Π΄Π° Π½Π΅ ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ°Π»ΠΈΡΡ: {never_connected}")
+ else:
+ stats_text_parts.append(f"π
ΠΠΊΡΠΈΠ²Π½ΠΎΡΡΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Π΅ΠΉ: N/A")
+ # Nodes statistics
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}")
+ else:
+ stats_text_parts.append(f"π ΠΠΎΠ΄Ρ: N/A")
+ # System statistics
if panel_stats:
system_info = panel_stats.get('system', {})
if system_info:
@@ -173,13 +186,17 @@ async def show_statistics_handler(callback: types.CallbackQuery,
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}%")
+ else:
+ stats_text_parts.append(f"πΎ Π‘ΠΈΡΡΠ΅ΠΌΠ½Π°Ρ ΠΈΠ½ΡΠΎΡΠΌΠ°ΡΠΈΡ: N/A")
+ else:
+ stats_text_parts.append(f"πΎ Π‘ΠΈΡΡΠ΅ΠΌΠ½Π°Ρ ΡΡΠ°ΡΠΈΡΡΠΈΠΊΠ°: N/A")
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("β ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΠΏΠΎΠ»ΡΡΠΈΡΡ Π΄Π°Π½Π½ΡΠ΅ Ρ ΠΏΠ°Π½Π΅Π»ΠΈ")
+ logging.error(f"Failed to fetch panel statistics: {e}", exc_info=True)
+ stats_text_parts.append("β ΠΡΠΈΠ±ΠΊΠ° ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΡ Π΄Π°Π½Π½ΡΡ
Ρ ΠΏΠ°Π½Π΅Π»ΠΈ")
+ stats_text_parts.append(f"β οΈ ΠΠ΅ΡΠ°Π»ΠΈ: {str(e)}")
final_text = "\n".join(stats_text_parts)
diff --git a/bot/handlers/admin/user_management.py b/bot/handlers/admin/user_management.py
index 21032f5..65daa9d 100644
--- a/bot/handlers/admin/user_management.py
+++ b/bot/handlers/admin/user_management.py
@@ -363,8 +363,10 @@ async def handle_toggle_ban(callback: types.CallbackQuery, user: User,
# Refresh user card with updated ban status
user.is_banned = new_ban_status # Update local object
from config.settings import Settings
+ from bot.services.panel_api_service import PanelApiService
settings = Settings()
- subscription_service = SubscriptionService(settings)
+ panel_service = PanelApiService(settings)
+ subscription_service = SubscriptionService(settings, panel_service)
await handle_refresh_user_card(callback, user, subscription_service, session, i18n_instance, lang)
except Exception as e:
@@ -628,7 +630,9 @@ async def process_direct_message_handler(message: types.Message, state: FSMConte
))
# Show user card again
- subscription_service = SubscriptionService(settings)
+ from bot.services.panel_api_service import PanelApiService
+ panel_service = PanelApiService(settings)
+ subscription_service = SubscriptionService(settings, panel_service)
user_card_text = await format_user_card(target_user, session, subscription_service, i18n, current_lang)
keyboard = get_user_card_keyboard(target_user.user_id, i18n, current_lang)
diff --git a/bot/states/admin_states.py b/bot/states/admin_states.py
index 21ee390..3b8af20 100644
--- a/bot/states/admin_states.py
+++ b/bot/states/admin_states.py
@@ -10,9 +10,7 @@ class AdminStates(StatesGroup):
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()
diff --git a/locales/en.json b/locales/en.json
index f02392b..0705c2d 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -344,11 +344,10 @@
"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_step1_code": "π Create Promo Code\n\nStep 1 of 4: 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 4: 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 4: 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 4: Validity Period\n\nCode: {code}\nBonus days: {bonus_days}\nMax activations: {max_activations}\n\nChoose the validity period for the promo code:",
"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",
@@ -359,6 +358,9 @@
"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}"
+ "admin_promo_created_success": "β
Promo code created!\n\nπ Code: {code}\nπ Bonus days: {days}\nπ’ Max activations: {max_act}\nβ° Valid until: {validity}",
+ "admin_promo_creation_failed": "β Error creating promo code: {error}",
+
+ "admin_panel_back_button": "β¬
οΈ Back",
+ "admin_bulk_promo_creation_failed": "β Error creating promo codes: {error}"
}
diff --git a/locales/ru.json b/locales/ru.json
index a0c6bbf..15211e8 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -344,11 +344,10 @@
"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_step1_code": "π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 1 ΠΈΠ· 4: ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ², ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ):",
+ "admin_promo_step2_bonus_days": "π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 2 ΠΈΠ· 4: ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ\n\nΠΠΎΠ΄: {code}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π±ΠΎΠ½ΡΡΠ½ΡΡ
Π΄Π½Π΅ΠΉ (1-365):",
+ "admin_promo_step3_max_activations": "π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 3 ΠΈΠ· 4: ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ (1-10000):",
+ "admin_promo_step4_validity": "π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 4 ΠΈΠ· 4: Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_activations}\n\nΠΡΠ±Π΅ΡΠΈΡΠ΅ ΡΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°:",
"admin_promo_invalid_code_format": "β ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΠΎΠ΄Π΅ΡΠΆΠ°ΡΡ 3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ² (ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ)",
"admin_promo_code_already_exists": "β ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ Ρ ΡΠ°ΠΊΠΈΠΌ ΠΊΠΎΠ΄ΠΎΠΌ ΡΠΆΠ΅ ΡΡΡΠ΅ΡΡΠ²ΡΠ΅Ρ",
@@ -359,6 +358,9 @@
"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}"
+ "admin_promo_created_success": "β
ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ ΡΠΎΠ·Π΄Π°Π½!\n\nπ ΠΠΎΠ΄: {code}\nπ ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {days}\nπ’ ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_act}\nβ° ΠΠ΅ΠΉΡΡΠ²ΡΠ΅Ρ Π΄ΠΎ: {validity}",
+ "admin_promo_creation_failed": "β ΠΡΠΈΠ±ΠΊΠ° ΡΠΎΠ·Π΄Π°Π½ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°: {error}",
+
+ "admin_panel_back_button": "β¬
οΈ ΠΠ°Π·Π°Π΄",
+ "admin_bulk_promo_creation_failed": "β ΠΡΠΈΠ±ΠΊΠ° ΡΠΎΠ·Π΄Π°Π½ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²: {error}"
}