Merge dev into main (subscription expiry clock time)

This commit is contained in:
admin committed 2026-10-30 13:05:14 +02:00
commit b1bc011306
6 files changed
+52 -17

No files matched your search

+1 -1
View File
@@ -1 +1 @@
0.1.0+78
0.1.0+79
+6 -7
View File
@@ -436,11 +436,14 @@ async def confirm_development_payment(callback: CallbackQuery) -> None:
await callback.answer("Dev-оплата подтверждена. Срок подписки не изменён.")
return
await asyncio.to_thread(ensure_subscription, user_id)
days_left = await asyncio.to_thread(user_store.activate_subscription_days, user_id, days)
await asyncio.to_thread(user_store.activate_subscription_days, user_id, days)
sub = await asyncio.to_thread(subscription_row, user_id)
await callback.answer("Dev-оплата подтверждена.")
if isinstance(callback.message, Message):
await callback.message.edit_reply_markup(reply_markup=None)
await callback.message.edit_text(f"Подписка активна\nЗакончится через {days_left} дн.")
await callback.message.edit_text(
user_store.subscription_status_text(sub)
)
@router.message(CommandStart())
@@ -470,11 +473,7 @@ async def _show_subscription_info(
if not sub:
text = "Подписка закончилась"
else:
active, value, unit = user_store.subscription_time_left(sub)
text = (
f"Подписка активна\nЗакончится через {value} {unit}"
if active else "Подписка закончилась"
)
text = user_store.subscription_status_text(sub)
if mode == "subscription":
markup = mode_keyboard("subscription", subscription_purchased=bool(sub and sub["last_paid_at"]))
+5 -4
View File
@@ -23,6 +23,8 @@ from user_store import (
activate_subscription,
connect,
ensure_subscription,
subscription_row,
subscription_status_text,
)
logger = logging.getLogger(__name__)
@@ -155,7 +157,8 @@ async def handle_successful_payment(message: Message) -> None:
days = 1
await asyncio.to_thread(ensure_subscription, message.from_user.id)
days_left = await asyncio.to_thread(activate_subscription_days, message.from_user.id, days)
await asyncio.to_thread(activate_subscription_days, message.from_user.id, days)
sub = await asyncio.to_thread(subscription_row, message.from_user.id, db_path)
await asyncio.to_thread(
save_payment,
charge_id,
@@ -165,9 +168,7 @@ async def handle_successful_payment(message: Message) -> None:
payment.total_amount,
db_path,
)
await message.answer(
f"Подписка активна\nЗакончится через {days_left} дн."
)
await message.answer(subscription_status_text(sub))
async def _reject(message: Message, charge_id: str, reason: str) -> None:
+12
View File
@@ -250,6 +250,18 @@ class ModeRoutingTests(unittest.IsolatedAsyncioTestCase):
self.assertIn("Подписка активна", message.answers[0][0])
self.assertIsNotNone(markup)
async def test_subscription_status_shows_clock_time_when_under_a_day(self):
message = _StubMessage()
sub = {"last_paid_at": "2026-01-01T00:00:00", "is_active": 1}
with patch.object(
bot.user_store, "subscription_time_left", return_value=(True, "31.10.2026 12:51 (МСК)", "")
):
await bot._show_subscription_info(cast(Any, message), cast(Any, sub))
text = message.answers[0][0]
self.assertIn("Подписка активна", text)
self.assertIn("Закончится в 31.10.2026 12:51 (МСК)", text)
self.assertNotIn("ч.", text)
async def test_subscription_action_button_shows_inline_plan_choices(self):
message = _StubMessage(keyboards.BUY_SUBSCRIPTION_BUTTON)
with patch.object(bot, "notify_development_deploy", new=AsyncMock()):
+1 -1
View File
@@ -287,7 +287,7 @@ class SubscribeCommandTests(unittest.IsolatedAsyncioTestCase):
with (
patch.object(bot, "DEPLOY_ENV", "development"),
patch.dict(os.environ, {"BOT_DB_PATH": db}),
patch.object(bot.asyncio, "to_thread", new=AsyncMock(return_value=32)) as to_thread,
patch.object(bot.asyncio, "to_thread", new=_direct_thread_mock()) as to_thread,
):
await bot.confirm_development_payment(callback)
db_row = user_store.subscription_row(42, db)
+27 -4
View File
@@ -145,6 +145,8 @@ def init_db(db_path: str | None = None) -> None:
TRIAL_DAYS = 3
SUBSCRIPTION_MONTH_DAYS = 30 # Совместимость со старым dev-сценарием.
SUBSCRIPTION_PRICE_STARS = 450
# Время окончания подписки показываем по Москве (MSK = фиксированный UTC+3).
MSK_TIMEZONE = datetime.timezone(datetime.timedelta(hours=3))
SUBSCRIPTION_PLANS = {
0: 1,
1: 15,
@@ -183,7 +185,11 @@ def _days_until(sub: sqlite3.Row, now: datetime.datetime | None = None) -> int:
return delta.days if delta.total_seconds() > 0 else -1
def subscription_time_left(sub: sqlite3.Row, now: datetime.datetime | None = None) -> tuple[bool, int, str]:
def subscription_time_left(
sub: sqlite3.Row,
now: datetime.datetime | None = None,
display_tz: datetime.timezone | None = None,
) -> tuple[bool, int | str, str]:
expires = _subscription_expires_at(sub)
if expires is None:
return False, 0, ""
@@ -194,9 +200,26 @@ def subscription_time_left(sub: sqlite3.Row, now: datetime.datetime | None = Non
return False, 0, ""
if seconds >= 86400:
return True, seconds // 86400, "дн."
if seconds >= 3600:
return True, seconds // 3600, "ч."
return True, max(1, seconds // 60), "мин."
# Меньше суток — показываем конкретное время окончания вместо "N ч.".
display_tz = display_tz or MSK_TIMEZONE
stamp = (expires + datetime.timedelta(microseconds=1)).replace(tzinfo=datetime.timezone.utc)
return True, stamp.astimezone(display_tz).strftime("%d.%m.%Y %H:%M") + " (МСК)", ""
def subscription_status_text(
sub: sqlite3.Row | None,
now: datetime.datetime | None = None,
display_tz: datetime.timezone | None = None,
) -> str:
"""Единый текст статуса подписки: дни (>= суток) или конкретное время (< суток)."""
if not sub:
return "Подписка закончилась"
active, value, unit = subscription_time_left(sub, now=now, display_tz=display_tz)
if not active:
return "Подписка закончилась"
if unit == "":
return f"Подписка активна\nЗакончится в {value}"
return f"Подписка активна\nЗакончится через {value} {unit}"
def subscription_row(user_id: int, db_path: str | None = None) -> sqlite3.Row | None:
with closing(connect(db_path)) as connection: