diff --git a/handlers/coupons.py b/handlers/coupons.py
index e6ca9baf..a7889662 100644
--- a/handlers/coupons.py
+++ b/handlers/coupons.py
@@ -74,8 +74,9 @@ async def activate_coupon(message: Message, state: FSMContext, session: Any, cou
coupon_record = await get_coupon_by_code(coupon_code, session)
if not coupon_record:
- await message.answer(COUPON_NOT_FOUND_MSG)
- await state.clear()
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="exit_coupon_input"))
+ await message.answer(COUPON_NOT_FOUND_MSG, reply_markup=builder.as_markup())
return
if coupon_record["usage_count"] >= coupon_record["usage_limit"] or coupon_record["is_used"]:
@@ -220,3 +221,10 @@ async def cancel_coupon_activation(callback_query: CallbackQuery, state: FSMCont
await callback_query.message.edit_text("⚠️ Активация купона отменена.")
await process_callback_view_profile(callback_query.message, state, admin)
await state.clear()
+
+
+@router.callback_query(F.data == "exit_coupon_input")
+async def handle_exit_coupon_input(callback_query: CallbackQuery, state: FSMContext):
+ await state.clear()
+ is_admin = callback_query.from_user.id in ADMIN_ID
+ await process_callback_view_profile(callback_query.message, state, admin=is_admin)
diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py
index de6f5f8d..4c0e6fe0 100644
--- a/handlers/keys/key_renew.py
+++ b/handlers/keys/key_renew.py
@@ -41,7 +41,7 @@ from handlers.texts import (
PLAN_SELECTION_MSG,
SUCCESS_RENEWAL_MSG,
)
-from handlers.utils import edit_or_send_message
+from handlers.utils import edit_or_send_message, format_months
from logger import logger
@@ -66,7 +66,7 @@ async def process_callback_renew_key(callback_query: CallbackQuery, session: Any
discount = DISCOUNTS.get(plan_id, 0) if isinstance(DISCOUNTS, dict) else 0
- button_text = f"📅 {months} месяц{'а' if months > 1 else ''} ({price} руб.)"
+ button_text = f"📅 {format_months(months)} ({price} руб.)"
if discount > 0:
button_text += f" {discount}% скидка"
diff --git a/handlers/keys/key_view.py b/handlers/keys/key_view.py
index fdf5411e..a809c2ab 100644
--- a/handlers/keys/key_view.py
+++ b/handlers/keys/key_view.py
@@ -48,7 +48,7 @@ from handlers.texts import (
NO_SUBSCRIPTIONS_MSG,
key_message,
)
-from handlers.utils import edit_or_send_message, handle_error, is_full_remnawave_cluster
+from handlers.utils import edit_or_send_message, handle_error, is_full_remnawave_cluster, format_days, format_hours, format_minutes
from logger import logger
@@ -215,13 +215,13 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any)
time_left = expiry_date - datetime.utcnow()
if time_left.total_seconds() <= 0:
- days_left_message = "🕒 Статус подписки:\n🔴 Истекла\nОсталось часов: 0\nОсталось минут: 0"
+ days_left_message = "🕒 Статус подписки:\n🔴 Истекла"
else:
total_seconds = int(time_left.total_seconds())
days = total_seconds // 86400
hours = (total_seconds % 86400) // 3600
minutes = (total_seconds % 3600) // 60
- days_left_message = f"Осталось: {days} дней, {hours} часов, {minutes} минут"
+ days_left_message = f"Осталось: {format_days(days)}, {format_hours(hours)}, {format_minutes(minutes)}"
formatted_expiry_date = expiry_date.strftime("%d %B %Y года")
response_message = key_message(
diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py
index 77951edb..a7098749 100644
--- a/handlers/notifications/general_notifications.py
+++ b/handlers/notifications/general_notifications.py
@@ -44,6 +44,7 @@ from handlers.texts import (
KEY_RENEWED,
KEY_RENEWED_TEMP_MSG,
)
+from handlers.utils import format_hours, format_minutes
from logger import logger
from .notify_utils import send_notification
@@ -137,8 +138,8 @@ async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: int,
continue
hours_left = int((expiry_timestamp - current_time) / (1000 * 3600))
- days_left_message = (
- f"⏳ Осталось времени: {hours_left} часов" if hours_left > 0 else "⏳ Последний день подписки!"
+ hours_left_formatted = (
+ f"⏳ Осталось времени: {format_hours(hours_left)}" if hours_left > 0 else "⏳ Последний день подписки!"
)
expiry_datetime = datetime.fromtimestamp(expiry_timestamp / 1000, tz=moscow_tz)
@@ -146,7 +147,7 @@ async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: int,
notification_text = KEY_EXPIRY_24H.format(
email=email,
- days_left_message=days_left_message,
+ hours_left_formatted=hours_left_formatted,
formatted_expiry_date=formatted_expiry_date,
)
@@ -192,8 +193,8 @@ async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: int,
continue
hours_left = int((expiry_timestamp - current_time) / (1000 * 3600))
- hours_left_message = (
- f"⏳ Осталось времени: {hours_left} часов" if hours_left > 0 else "⏳ Последний день подписки!"
+ hours_left_formatted = (
+ f"⏳ Осталось времени: {format_hours(hours_left)}" if hours_left > 0 else "⏳ Последний день подписки!"
)
expiry_datetime = datetime.fromtimestamp(expiry_timestamp / 1000, tz=moscow_tz)
@@ -201,7 +202,7 @@ async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: int,
notification_text = KEY_EXPIRY_10H.format(
email=email,
- hours_left_message=hours_left_message,
+ hours_left_formatted=hours_left_formatted,
formatted_expiry_date=formatted_expiry_date,
)
@@ -308,15 +309,21 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
if hours > 0:
if minutes > 0:
- hour_suffix = "час" if hours == 1 else "часа" if 2 <= hours <= 4 else "часов"
- time_str = f"{hours} {hour_suffix} и {minutes} минут"
- delay_message = KEY_EXPIRED_DELAY_HOURS_MINUTES_MSG.format(email=email, time_str=time_str)
+ delay_message = KEY_EXPIRED_DELAY_HOURS_MINUTES_MSG.format(
+ email=email,
+ hours_formatted=format_hours(hours),
+ minutes_formatted=format_minutes(minutes)
+ )
else:
- hour_suffix = "час" if hours == 1 else "часа" if 2 <= hours <= 4 else "часов"
- time_str = f"{hours} {hour_suffix}"
- delay_message = KEY_EXPIRED_DELAY_HOURS_MSG.format(email=email, time_str=time_str)
+ delay_message = KEY_EXPIRED_DELAY_HOURS_MSG.format(
+ email=email,
+ hours_formatted=format_hours(hours)
+ )
else:
- delay_message = KEY_EXPIRED_DELAY_MINUTES_MSG.format(email=email, minutes=NOTIFY_DELETE_DELAY)
+ delay_message = KEY_EXPIRED_DELAY_MINUTES_MSG.format(
+ email=email,
+ minutes_formatted=format_minutes(minutes)
+ )
else:
delay_message = KEY_EXPIRED_NO_DELAY_MSG.format(email=email)
diff --git a/handlers/notifications/special_notifications.py b/handlers/notifications/special_notifications.py
index 25286463..d257b5b2 100644
--- a/handlers/notifications/special_notifications.py
+++ b/handlers/notifications/special_notifications.py
@@ -18,6 +18,7 @@ from database import (
from handlers.buttons import MAIN_MENU
from handlers.keys.key_utils import get_user_traffic
from handlers.texts import TRIAL_INACTIVE_BONUS_MSG, TRIAL_INACTIVE_FIRST_MSG, ZERO_TRAFFIC_MSG
+from handlers.utils import format_days
from logger import logger
@@ -80,12 +81,15 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
total_days = NOTIFY_EXTRA_DAYS + TRIAL_TIME
message = TRIAL_INACTIVE_BONUS_MSG.format(
display_name=display_name,
- NOTIFY_EXTRA_DAYS=NOTIFY_EXTRA_DAYS,
- total_days=total_days,
+ extra_days_formatted=format_days(NOTIFY_EXTRA_DAYS),
+ total_days_formatted=format_days(total_days),
)
await conn.execute("UPDATE users SET trial = -1 WHERE tg_id = $1", tg_id)
else:
- message = TRIAL_INACTIVE_FIRST_MSG.format(display_name=display_name, TRIAL_TIME=TRIAL_TIME)
+ message = TRIAL_INACTIVE_FIRST_MSG.format(
+ display_name=display_name,
+ trial_time_formatted=format_days(TRIAL_TIME)
+ )
try:
await bot.send_message(tg_id, message, reply_markup=keyboard)
diff --git a/handlers/utils.py b/handlers/utils.py
index 17e64200..dc97d1c9 100644
--- a/handlers/utils.py
+++ b/handlers/utils.py
@@ -115,64 +115,37 @@ async def handle_error(tg_id: int, callback_query: object | None = None, message
logger.error(f"Ошибка при обработке ошибки: {e}")
-def format_time_until_deletion(seconds: int) -> str:
- if seconds <= 0:
- return "0 минут"
-
- days = seconds // (3600 * 24)
- hours = (seconds % (3600 * 24)) // 3600
- minutes = (seconds % 3600 + 59) // 60
-
- parts = []
-
- if days > 0:
- if days == 1:
- parts.append(f"{days} день")
- elif 2 <= days <= 4:
- parts.append(f"{days} дня")
- else:
- parts.append(f"{days} дней")
-
- if hours > 0:
- if hours == 1:
- parts.append(f"{hours} час")
- elif 2 <= hours <= 4:
- parts.append(f"{hours} часа")
- else:
- parts.append(f"{hours} часов")
-
- if minutes > 0 and days == 0:
- if minutes == 1:
- parts.append("1 минута")
- elif 2 <= minutes <= 4:
- parts.append(f"{minutes} минуты")
- else:
- parts.append(f"{minutes} минут")
-
- return " и ".join(parts) if parts else "менее минуты"
-
-
def get_plural_form(num: int, form1: str, form2: str, form3: str) -> str:
+ """Универсальная функция для получения правильной формы множественного числа"""
n = abs(num) % 100
if 10 < n < 20:
return form3
return {1: form1, 2: form2, 3: form2, 4: form2}.get(n % 10, form3)
+def format_months(months: int) -> str:
+ """Форматирует количество месяцев с правильным склонением"""
+ if months <= 0:
+ return "0 месяцев"
+ return f"{months} {get_plural_form(months, 'месяц', 'месяца', 'месяцев')}"
def format_days(days: int) -> str:
- """
- Форматирует количество дней с правильным склонением.
-
- Args:
- days (int): Количество дней.
-
- Returns:
- str: Строка с числом и склонённым словом "день/дня/дней".
- """
+ """Форматирует количество дней с правильным склонением"""
if days <= 0:
return "0 дней"
return f"{days} {get_plural_form(days, 'день', 'дня', 'дней')}"
+def format_hours(hours: int) -> str:
+ """Форматирует количество часов с правильным склонением"""
+ if hours <= 0:
+ return "0 часов"
+ return f"{hours} {get_plural_form(hours, 'час', 'часа', 'часов')}"
+
+def format_minutes(minutes: int) -> str:
+ """Форматирует количество минут с правильным склонением"""
+ if minutes <= 0:
+ return "0 минут"
+ return f"{minutes} {get_plural_form(minutes, 'минута', 'минуты', 'минут')}"
+
async def edit_or_send_message(
target_message: Message,