fix(subscription): show precise remaining time

This commit is contained in:
admin committed 2026-10-29 21:35:58 +02:00
1 parent 2fd41d82b9
commit 57f4c6cb4f
4 files changed
+40 -22

No files matched your search

+1 -1
View File
@@ -1 +1 @@
0.1.0+75
0.1.0+76
+3 -3
View File
@@ -467,10 +467,10 @@ async def _show_subscription_info(
if not sub:
text = "Подписка закончилась"
else:
days_left = user_store._days_until(sub)
active, value, unit = user_store.subscription_time_left(sub)
text = (
f"Подписка активна\nЗакончится через {days_left} дн."
if days_left > 0 else "Подписка закончилась"
f"Подписка активна\nЗакончится через {value} {unit}"
if active else "Подписка закончилась"
)
if mode == "subscription":
+1 -1
View File
@@ -244,7 +244,7 @@ class ModeRoutingTests(unittest.IsolatedAsyncioTestCase):
async def test_subscription_status_uses_renew_button_after_previous_payment(self):
message = _StubMessage()
sub = {"last_paid_at": "2026-01-01T00:00:00", "expires_at": "2026-02-01T00:00:00", "is_active": 1}
with patch.object(bot.user_store, "_days_until", return_value=2):
with patch.object(bot.user_store, "subscription_time_left", return_value=(True, 2, "дн.")):
await bot._show_subscription_info(cast(Any, message), cast(Any, sub))
markup = message.answers[0][1]["reply_markup"]
self.assertIn("Подписка активна", message.answers[0][0])
+35 -17
View File
@@ -153,33 +153,51 @@ SUBSCRIPTION_PLANS = {
}
def _subscription_expires_at(sub: sqlite3.Row) -> datetime.datetime | None:
if not sub or sub["is_active"] == 0:
return None
def utc(value: str) -> datetime.datetime:
parsed = datetime.datetime.fromisoformat(value.replace("Z", "+00:00"))
if parsed.tzinfo is not None:
parsed = parsed.astimezone(datetime.timezone.utc).replace(tzinfo=None)
return parsed
if sub["last_paid_at"] is None:
return utc(sub["trial_started_at"]) + datetime.timedelta(days=TRIAL_DAYS)
return utc(sub["last_paid_at"])
def _days_until(sub: sqlite3.Row, now: datetime.datetime | None = None) -> int:
"""Сколько дней осталось от оплаты (триал или подписка). Отрицательно = истекло/отключено."""
if not sub:
return -1
if sub["is_active"] == 0:
return -1
# SQLite CURRENT_TIMESTAMP возвращает UTC-нейтральный datetime.
def utc(value: str) -> datetime.datetime:
return datetime.datetime.fromisoformat(value.replace("Z", "+00:00"))
if sub["last_paid_at"] is None:
# Триал: trial_started_at + 3 дня
trial_start = utc(sub["trial_started_at"])
if trial_start.tzinfo is not None:
trial_start = trial_start.astimezone(datetime.timezone.utc).replace(tzinfo=None)
expires = trial_start + datetime.timedelta(days=TRIAL_DAYS)
else:
expires = utc(sub["last_paid_at"])
if expires.tzinfo is not None:
expires = expires.astimezone(datetime.timezone.utc).replace(tzinfo=None)
expires = _subscription_expires_at(sub)
if expires is None:
return -1
now = now or datetime.datetime.now(datetime.timezone.utc)
now = now.replace(tzinfo=None)
now = now.astimezone(datetime.timezone.utc).replace(tzinfo=None) if now.tzinfo else now
delta = expires - now
# Полные дни, пока expires > now. При истечении (expires <= now) считаем -1.
return delta.days if expires > now else -1
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]:
expires = _subscription_expires_at(sub)
if expires is None:
return False, 0, ""
now = now or datetime.datetime.now(datetime.timezone.utc)
now = now.astimezone(datetime.timezone.utc).replace(tzinfo=None) if now.tzinfo else now
seconds = int((expires - now).total_seconds())
if seconds <= 0:
return False, 0, ""
if seconds >= 86400:
return True, seconds // 86400, "дн."
if seconds >= 3600:
return True, seconds // 3600, "ч."
return True, max(1, seconds // 60), "мин."
def subscription_row(user_id: int, db_path: str | None = None) -> sqlite3.Row | None:
with closing(connect(db_path)) as connection:
return connection.execute(