Fix configurator price on tariff switch in renewal / Fix datetime naive vs aware across handlers / Redis connection stability
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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,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)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user