fix(subscription): show precise remaining time
This commit is contained in:
1 parent
2fd41d82b9
commit
57f4c6cb4f
4 files changed
+40
-22
No files matched your search
@@ -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":
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user