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,