Fix configurator price on tariff switch in renewal / Fix datetime naive vs aware across handlers / Redis connection stability

This commit is contained in:
Vladless
2026-04-15 17:42:12 +00:00
parent 995e2f8016
commit 9f140542d9
10 changed files with 62 additions and 32 deletions
+9 -1
View File
@@ -23,7 +23,15 @@ bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTM
RedisStorage = import_module("aiogram.fsm.storage.redis").RedisStorage
redis_from_url = import_module("redis.asyncio").from_url
redis = redis_from_url(REDIS_URL, encoding="utf-8", decode_responses=True)
redis = redis_from_url(
REDIS_URL,
encoding="utf-8",
decode_responses=True,
health_check_interval=30,
socket_connect_timeout=5,
socket_timeout=5,
retry_on_timeout=True,
)
storage = RedisStorage(redis=redis)
dp = Dispatcher(bot=bot, storage=storage)
+4
View File
@@ -49,6 +49,10 @@ async def _get_redis() -> Any | None:
encoding="utf-8",
decode_responses=True,
max_connections=64,
health_check_interval=30,
socket_connect_timeout=5,
socket_timeout=5,
retry_on_timeout=True,
)
await client.ping()
_REDIS_CLIENTS[client_key] = client
+4 -4
View File
@@ -1,7 +1,7 @@
import json
import re
from datetime import datetime
from datetime import datetime, timezone
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from sqlalchemy import and_, distinct, exists, func, not_, select
@@ -15,12 +15,12 @@ from logger import logger
def _not_banned(user_id_col):
return ~exists().where(BlockedUser.user_id == user_id_col) & ~exists().where(
ManualBan.user_id == user_id_col,
(ManualBan.until.is_(None)) | (ManualBan.until > datetime.utcnow()),
(ManualBan.until.is_(None)) | (ManualBan.until > datetime.now(timezone.utc)),
)
async def get_recipients(session: AsyncSession, send_to: str, cluster_name: str | None = None) -> tuple[list[int], int]:
now_ms = int(datetime.utcnow().timestamp() * 1000)
now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
query = None
@@ -54,7 +54,7 @@ async def get_recipients(session: AsyncSession, send_to: str, cluster_name: str
~exists().where(BlockedUser.user_id == unsub_base.c.uid),
~exists().where(
ManualBan.user_id == unsub_base.c.uid,
(ManualBan.until.is_(None)) | (ManualBan.until > datetime.utcnow()),
(ManualBan.until.is_(None)) | (ManualBan.until > datetime.now(timezone.utc)),
),
)
)
+17 -6
View File
@@ -1,5 +1,5 @@
from collections import defaultdict
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from math import ceil
from typing import Any
@@ -99,7 +99,7 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo
expiry_utc = datetime.utcfromtimestamp(expiry_time / 1000).replace(tzinfo=pytz.UTC)
available_from_utc = expiry_utc - timedelta(days=RENEW_BUTTON_BEFORE_DAYS)
now_utc = datetime.utcnow().replace(tzinfo=pytz.UTC)
now_utc = datetime.now(timezone.utc)
if now_utc < available_from_utc:
dt_msk = available_from_utc.astimezone(moscow_tz).strftime("%d.%m.%Y %H:%M")
@@ -434,7 +434,7 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, state: FSMC
discount_info = await check_hot_lead_discount(session, tg_id)
if tariff.get("group_code") in ["discounts", "discounts_max"]:
if not discount_info.get("available") or datetime.utcnow() >= discount_info["expires_at"]:
if not discount_info.get("available") or datetime.now(timezone.utc) >= discount_info["expires_at"]:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await callback_query.message.answer(
@@ -455,7 +455,7 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, state: FSMC
email = record["email"]
expiry_time_raw = record["expiry_time"]
expiry_time = normalize_expiry_ms(expiry_time_raw)
current_time = int(datetime.utcnow().timestamp() * 1000)
current_time = int(datetime.now(timezone.utc).timestamp() * 1000)
if expiry_time <= current_time:
new_expiry_time = int(current_time + timedelta(days=duration_days).total_seconds() * 1000)
@@ -596,12 +596,11 @@ async def handle_renew_config_confirm(callback_query: CallbackQuery, state: FSMC
client_id = data.get("renew_client_id")
email = data.get("renew_key_name")
new_expiry_time = data.get("renew_new_expiry_time")
tariff_id = data.get("renew_tariff_id")
selected_devices = data.get("config_selected_device_limit")
selected_traffic_gb = data.get("config_selected_traffic_gb")
if not client_id or not email or not new_expiry_time or not tariff_id:
if not client_id or not email or not tariff_id:
await callback_query.message.answer("❌ Данные для продления не найдены.")
return
@@ -612,6 +611,18 @@ async def handle_renew_config_confirm(callback_query: CallbackQuery, state: FSMC
await callback_query.message.answer("❌ Тариф не найден или не поддерживает настройку.")
return
duration_days = int(tariff.get("duration_days") or 30)
record = await get_key_details(session, email)
if not record:
await callback_query.message.answer("❌ Подписка не найдена.")
return
expiry_time = normalize_expiry_ms(record.get("expiry_time"))
current_time = int(datetime.now(timezone.utc).timestamp() * 1000)
if expiry_time <= current_time:
new_expiry_time = int(current_time + timedelta(days=duration_days).total_seconds() * 1000)
else:
new_expiry_time = int(expiry_time + timedelta(days=duration_days).total_seconds() * 1000)
final_price = calculate_config_price(
tariff=tariff,
selected_device_limit=int(selected_devices) if selected_devices is not None else None,
+3 -3
View File
@@ -3,7 +3,7 @@ import html
import os
import re
from datetime import datetime
from datetime import datetime, timezone
import pytz
@@ -328,8 +328,8 @@ async def build_key_view_payload(session: AsyncSession, tg_id: int, key_ref_or_e
expiry_time = record["expiry_time"]
server_name = record["server_id"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
now = datetime.utcnow()
expiry_date = datetime.fromtimestamp(expiry_time / 1000, tz=timezone.utc)
now = datetime.now(timezone.utc)
time_left = expiry_date - now
if time_left.total_seconds() <= 0:
@@ -1,7 +1,7 @@
import asyncio
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Any, Optional
import pytz
@@ -118,7 +118,7 @@ async def preload_notification_data(session: AsyncSession) -> dict[str, Any]:
~exists().where(BlockedUser.tg_id == Key.tg_id),
~exists().where(
ManualBan.tg_id == Key.tg_id,
or_(ManualBan.until.is_(None), ManualBan.until > datetime.utcnow()),
or_(ManualBan.until.is_(None), ManualBan.until > datetime.now(timezone.utc)),
),
)
)
@@ -391,8 +391,8 @@ async def try_auto_renew(ctx: NotificationContext, key) -> tuple[bool, dict | No
new_expiry_time = (
current_expiry
if current_expiry > datetime.utcnow().timestamp() * 1000
else datetime.utcnow().timestamp() * 1000
if current_expiry > datetime.now(timezone.utc).timestamp() * 1000
else datetime.now(timezone.utc).timestamp() * 1000
) + duration_days * 24 * 60 * 60 * 1000
logger.info(
@@ -609,7 +609,7 @@ async def _get_blocked_expired_keys(session: AsyncSession, current_time: int) ->
exists().where(BlockedUser.tg_id == Key.tg_id),
exists().where(
ManualBan.tg_id == Key.tg_id,
or_(ManualBan.until.is_(None), ManualBan.until > datetime.utcnow()),
or_(ManualBan.until.is_(None), ManualBan.until > datetime.now(timezone.utc)),
),
),
)
+7 -3
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from math import ceil
from typing import Any
@@ -606,7 +606,9 @@ async def start_user_tariff_configurator(
"config_tariff_id": tariff["id"],
"tariff_config": cfg_for_state,
}
if renew_mode != "renew":
if renew_mode == "renew":
update_payload["renew_tariff_id"] = tariff["id"]
else:
update_payload["config_selected_device_limit"] = None
update_payload["config_selected_traffic_gb"] = None
@@ -784,7 +786,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state:
discount_info = await check_hot_lead_discount(session, tg_id)
if tariff.get("group_code") in ["discounts", "discounts_max"]:
if not discount_info.get("available") or datetime.utcnow() >= discount_info["expires_at"]:
if not discount_info.get("available") or datetime.now(timezone.utc) >= discount_info["expires_at"]:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await edit_or_send_message(
@@ -820,6 +822,8 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state:
)
return
await state.update_data(renew_mode=None)
if tariff.get("configurable"):
logger.info(f"[TARIFF_CFG] select_tariff_plan configurable: tg_id={tg_id} tariff_id={tariff_id}")
try:
+4 -2
View File
@@ -4,7 +4,7 @@ import re
import secrets
import string
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
import aiofiles
@@ -456,7 +456,9 @@ def get_username(user) -> str:
def format_discount_time_left(last_time: datetime, discount_hours: int) -> str:
expires_at = last_time + timedelta(hours=discount_hours)
current_time = datetime.utcnow()
current_time = datetime.now(timezone.utc)
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
time_left = expires_at - current_time
if time_left.total_seconds() <= 0:
+6 -5
View File
@@ -1,10 +1,11 @@
from collections.abc import Awaitable, Callable
from datetime import datetime
from datetime import datetime, timezone
from typing import Any
import pytz
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, TelegramObject, Update
from pytz import timezone
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -16,7 +17,7 @@ from database.models import ManualBan, User
from logger import logger
TZ = timezone("Europe/Moscow")
TZ = pytz.timezone("Europe/Moscow")
_BAN_CACHE_TTL = BAN_CACHE_TTL_SEC
@@ -34,7 +35,7 @@ class BanCheckerMiddleware(BaseMiddleware):
.join(User, ManualBan.user_id == User.id)
.where(
User.tg_id == tg_id,
(ManualBan.until.is_(None)) | (ManualBan.until > datetime.utcnow()),
(ManualBan.until.is_(None)) | (ManualBan.until > datetime.now(timezone.utc)),
)
.limit(1)
)
@@ -87,7 +88,7 @@ class BanCheckerMiddleware(BaseMiddleware):
until_parsed = datetime.fromisoformat(until_raw)
except ValueError:
until_parsed = None
if until_parsed is not None and until_parsed < datetime.utcnow():
if until_parsed is not None and until_parsed < datetime.now(timezone.utc):
ban_info = None
await cache_delete(cache_key("ban_status", tg_id))
else:
+3 -3
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import uuid
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING
from pytz import timezone
@@ -98,7 +98,7 @@ async def redeem_gift(
if wu is None:
raise NotFoundError("Пользователь не найден")
if gift_info.expiry_time and gift_info.expiry_time < datetime.utcnow():
if gift_info.expiry_time and gift_info.expiry_time < datetime.now(timezone.utc):
raise ValidationError("Срок действия подарка истёк")
if gift_info.sender_user_id == wu.id:
@@ -209,7 +209,7 @@ async def create_gift(
await update_balance(session, sender_user_ref, -price_to_charge)
duration_days = int(tariff["duration_days"] or 0)
expiry_time = datetime.utcnow() + timedelta(days=duration_days)
expiry_time = datetime.now(timezone.utc) + timedelta(days=duration_days)
gift_id = uuid.uuid4().hex
gift_link = get_gift_link(sender_user_ref, gift_id)
site_gift_link = get_site_gift_link(gift_id)