Template
675 lines
23 KiB
Python
675 lines
23 KiB
Python
"""Покупка тарифа: баланс + создание/продление пользователя Remnawave."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from dataclasses import dataclass
|
||
from datetime import datetime, timedelta, timezone
|
||
|
||
from remnawave import RemnawaveSDK
|
||
from remnawave.enums import TrafficLimitStrategy, UserStatus
|
||
from remnawave.models import CreateUserRequestDto, UpdateUserRequestDto
|
||
|
||
from remnawave_client import friendly_remnawave_error
|
||
from services.db import Database
|
||
from services.devices import (
|
||
BASE_DEVICES,
|
||
DEVICE_PACKS,
|
||
EXTRA_DEVICE_PRICE_RUB,
|
||
DevicePack,
|
||
DeviceUsage,
|
||
)
|
||
from services.extras_billing import (
|
||
effective_extra_devices,
|
||
extras_billing_line,
|
||
on_extras_purchased,
|
||
process_user_extras_billing,
|
||
)
|
||
from services.tariffs import Tariff
|
||
from services.traffic import (
|
||
BASE_TRAFFIC_GB,
|
||
TrafficPack,
|
||
TrafficUsage,
|
||
ensure_base_limit_bytes,
|
||
format_gb,
|
||
gb_to_bytes,
|
||
traffic_from_panel_user,
|
||
)
|
||
|
||
|
||
from services.remnawave_users import (
|
||
device_limit_for_user as _device_limit_for_user,
|
||
find_panel_user as _find_panel_user,
|
||
sync_device_policy,
|
||
username_for,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _as_utc(dt: datetime) -> datetime:
|
||
if dt.tzinfo is None:
|
||
return dt.replace(tzinfo=timezone.utc)
|
||
return dt.astimezone(timezone.utc)
|
||
|
||
|
||
def _traffic_limit_for_update(existing, tariff: Tariff) -> float:
|
||
"""При продлении не затираем докупленный трафик, но не меньше базы тарифа."""
|
||
current = ensure_base_limit_bytes(getattr(existing, "traffic_limit_bytes", 0))
|
||
return max(current, float(tariff.traffic_bytes))
|
||
|
||
|
||
async def provision_subscription(
|
||
sdk: RemnawaveSDK,
|
||
*,
|
||
telegram_id: int,
|
||
tariff: Tariff,
|
||
login: str | None = None,
|
||
extra_devices: int = 0,
|
||
) -> tuple[str, str, datetime]:
|
||
"""Создать или продлить пользователя. Возвращает (uuid, sub_url, expire_at)."""
|
||
now = datetime.now(timezone.utc)
|
||
existing = await _find_panel_user(sdk, telegram_id, login=login)
|
||
|
||
panel_tg_id = telegram_id if telegram_id > 0 else None
|
||
device_limit = _device_limit_for_user(extra_devices, soft_cap=True)
|
||
|
||
if existing is not None:
|
||
current_expire = _as_utc(existing.expire_at)
|
||
base = current_expire if current_expire > now else now
|
||
new_expire = base + timedelta(days=tariff.days)
|
||
body = UpdateUserRequestDto(
|
||
uuid=existing.uuid,
|
||
status=UserStatus.ACTIVE,
|
||
expire_at=new_expire,
|
||
telegram_id=panel_tg_id,
|
||
hwid_device_limit=device_limit,
|
||
traffic_limit_bytes=_traffic_limit_for_update(existing, tariff),
|
||
traffic_limit_strategy=TrafficLimitStrategy.NO_RESET,
|
||
active_internal_squads=list(tariff.squad_uuids) or None,
|
||
)
|
||
updated = await sdk.users.update_user(body)
|
||
return str(updated.uuid), updated.subscription_url, _as_utc(updated.expire_at)
|
||
|
||
new_expire = now + timedelta(days=tariff.days)
|
||
created = await sdk.users.create_user(
|
||
CreateUserRequestDto(
|
||
username=username_for(telegram_id, login=login),
|
||
expire_at=new_expire,
|
||
status=UserStatus.ACTIVE,
|
||
telegram_id=panel_tg_id,
|
||
hwid_device_limit=device_limit,
|
||
traffic_limit_bytes=float(tariff.traffic_bytes),
|
||
traffic_limit_strategy=TrafficLimitStrategy.NO_RESET,
|
||
active_internal_squads=list(tariff.squad_uuids) or None,
|
||
description=f"Bought via bot: {tariff.id}",
|
||
tag="BOT",
|
||
)
|
||
)
|
||
return str(created.uuid), created.subscription_url, _as_utc(created.expire_at)
|
||
|
||
|
||
async def purchase_tariff(
|
||
*,
|
||
db: Database,
|
||
sdk: RemnawaveSDK,
|
||
telegram_id: int,
|
||
tariff: Tariff,
|
||
charge_balance: bool = True,
|
||
payment_meta: str | None = None,
|
||
renew: bool = False,
|
||
promo_code: str | None = None,
|
||
) -> PurchaseResult:
|
||
bot_user = db.ensure_user(telegram_id)
|
||
if bot_user.banned:
|
||
return PurchaseResult(
|
||
ok=False,
|
||
message="Аккаунт заблокирован. Обратитесь в поддержку.",
|
||
balance=bot_user.balance,
|
||
)
|
||
balance = bot_user.balance
|
||
login = bot_user.login
|
||
|
||
if renew:
|
||
existing = await _find_panel_user(sdk, telegram_id, login=login)
|
||
if existing is None:
|
||
return PurchaseResult(
|
||
ok=False,
|
||
message=(
|
||
"Нет подписки для продления.\n"
|
||
"Сначала купи тариф в разделе «Тарифы»."
|
||
),
|
||
balance=balance,
|
||
)
|
||
|
||
charge_price = float(tariff.price)
|
||
bonus_days = 0
|
||
promo_row = None
|
||
if promo_code and charge_balance:
|
||
from services.db import AuthError
|
||
|
||
try:
|
||
charge_price, bonus_days, promo_row = db.apply_promo_to_price(
|
||
code=promo_code,
|
||
telegram_id=telegram_id,
|
||
tariff_id=tariff.id,
|
||
price=float(tariff.price),
|
||
days=tariff.days,
|
||
)
|
||
except AuthError as exc:
|
||
return PurchaseResult(ok=False, message=str(exc), balance=balance)
|
||
|
||
effective_tariff = tariff
|
||
if bonus_days > 0:
|
||
effective_tariff = Tariff(
|
||
id=tariff.id,
|
||
title=tariff.title,
|
||
price=tariff.price,
|
||
days=tariff.days + bonus_days,
|
||
traffic_gb=tariff.traffic_gb,
|
||
device_limit=tariff.device_limit,
|
||
squad_uuids=tariff.squad_uuids,
|
||
description=tariff.description,
|
||
)
|
||
|
||
if charge_balance:
|
||
if charge_price > 0:
|
||
ok, balance = db.try_charge(
|
||
telegram_id,
|
||
charge_price,
|
||
kind="renewal" if renew else "purchase",
|
||
meta=payment_meta
|
||
or (
|
||
f"promo:{promo_row['code']}:" if promo_row else ""
|
||
)
|
||
+ ("renew:" + tariff.id if renew else tariff.id),
|
||
)
|
||
if not ok:
|
||
return PurchaseResult(
|
||
ok=False,
|
||
message=(
|
||
f"Недостаточно средств.\n"
|
||
f"Нужно: <b>{charge_price:.0f} ₽</b>\n"
|
||
f"Баланс: <b>{balance:.0f} ₽</b>\n\n"
|
||
"Пополни баланс криптой или через админа."
|
||
),
|
||
balance=balance,
|
||
)
|
||
# charge_price == 0 — бесплатно по промо
|
||
|
||
try:
|
||
rw_uuid, sub_url, expire_at = await provision_subscription(
|
||
sdk,
|
||
telegram_id=telegram_id,
|
||
tariff=effective_tariff,
|
||
login=login,
|
||
extra_devices=effective_extra_devices(bot_user),
|
||
)
|
||
except Exception as exc: # noqa: BLE001
|
||
if charge_balance and charge_price > 0:
|
||
balance = db.add_balance(
|
||
telegram_id,
|
||
charge_price,
|
||
kind="refund",
|
||
meta=f"refund:{tariff.id}:{exc}",
|
||
)
|
||
return PurchaseResult(
|
||
ok=False,
|
||
message=(
|
||
"Не удалось продлить/выдать подписку. Средства возвращены.\n\n"
|
||
+ friendly_remnawave_error(exc)
|
||
),
|
||
balance=balance,
|
||
)
|
||
return PurchaseResult(
|
||
ok=False,
|
||
message=(
|
||
"Оплата прошла, но выдача подписки упала. Напиши админу.\n\n"
|
||
+ friendly_remnawave_error(exc)
|
||
),
|
||
balance=balance,
|
||
)
|
||
|
||
if promo_row and charge_balance:
|
||
saved = max(0.0, float(tariff.price) - charge_price)
|
||
try:
|
||
db.redeem_promo(
|
||
promo_id=int(promo_row["id"]),
|
||
telegram_id=telegram_id,
|
||
amount_saved=saved,
|
||
)
|
||
except Exception: # noqa: BLE001
|
||
logger.exception("promo redeem failed")
|
||
|
||
db.record_purchase(
|
||
telegram_id,
|
||
tariff_id=tariff.id,
|
||
price=charge_price if charge_balance else float(tariff.price),
|
||
remnawave_uuid=rw_uuid,
|
||
subscription_url=sub_url,
|
||
)
|
||
|
||
if not renew and charge_balance:
|
||
try:
|
||
ref_bonus = float(db.get_setting("referral_bonus_referrer") or "50")
|
||
welcome = float(db.get_setting("referral_bonus_referee") or "50")
|
||
db.grant_referral_rewards_on_first_purchase(
|
||
telegram_id,
|
||
referrer_bonus=max(0.0, ref_bonus),
|
||
referee_bonus=max(0.0, welcome),
|
||
)
|
||
except Exception: # noqa: BLE001
|
||
logger.exception("referral reward failed")
|
||
|
||
try:
|
||
from services.xui_wg import sync_wg_configs_limits
|
||
from services.awg_configs import sync_awg_configs_expiry
|
||
|
||
await sync_wg_configs_limits(
|
||
db=db,
|
||
telegram_id=telegram_id,
|
||
expire_at=expire_at,
|
||
restore_enabled=True,
|
||
)
|
||
await sync_awg_configs_expiry(
|
||
db=db,
|
||
sdk=sdk,
|
||
telegram_id=telegram_id,
|
||
login=login,
|
||
restore_enabled=True,
|
||
)
|
||
except Exception: # noqa: BLE001
|
||
logger.exception("WG/AWG expiry sync after tariff purchase failed")
|
||
|
||
expire_str = expire_at.strftime("%d.%m.%Y %H:%M UTC")
|
||
if renew:
|
||
paid_how = "с баланса" if charge_balance else "криптой"
|
||
return PurchaseResult(
|
||
ok=True,
|
||
message=(
|
||
f"✅ <b>Подписка продлена</b>\n\n"
|
||
f"Тариф: <b>{tariff.title}</b> (+{tariff.days} дн.)\n"
|
||
f"Списано: <b>{tariff.format_price()}</b> ({paid_how})\n"
|
||
f"Баланс: <b>{balance:.0f} ₽</b>\n"
|
||
f"Новый срок: <b>{expire_str}</b>\n\n"
|
||
f"🔗 Подписка:\n<code>{sub_url}</code>"
|
||
),
|
||
balance=balance,
|
||
subscription_url=sub_url,
|
||
expire_at=expire_at,
|
||
)
|
||
|
||
paid_how = "с баланса" if charge_balance else "криптой"
|
||
return PurchaseResult(
|
||
ok=True,
|
||
message=(
|
||
f"✅ <b>Тариф «{tariff.title}» активирован</b>\n\n"
|
||
f"Оплата: <b>{tariff.format_price()}</b> ({paid_how})\n"
|
||
f"Баланс: <b>{balance:.0f} ₽</b>\n"
|
||
f"Действует до: <b>{expire_str}</b>\n\n"
|
||
f"🔗 Подписка:\n<code>{sub_url}</code>"
|
||
),
|
||
balance=balance,
|
||
subscription_url=sub_url,
|
||
expire_at=expire_at,
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class PurchaseResult:
|
||
ok: bool
|
||
message: str
|
||
balance: float
|
||
subscription_url: str | None = None
|
||
expire_at: datetime | None = None
|
||
traffic: TrafficUsage | None = None
|
||
devices: DeviceUsage | None = None
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class SubscriptionInfo:
|
||
text: str
|
||
has_subscription: bool
|
||
expire_at: datetime | None = None
|
||
subscription_url: str | None = None
|
||
traffic: TrafficUsage | None = None
|
||
devices: DeviceUsage | None = None
|
||
|
||
|
||
async def get_my_subscription_info(
|
||
sdk: RemnawaveSDK,
|
||
telegram_id: int,
|
||
*,
|
||
login: str | None = None,
|
||
db: Database | None = None,
|
||
tariffs: list[Tariff] | None = None,
|
||
renewal_bot=None,
|
||
) -> SubscriptionInfo:
|
||
user = await _find_panel_user(sdk, telegram_id, login=login)
|
||
if user is None:
|
||
return SubscriptionInfo(
|
||
text=(
|
||
"<b>Моя подписка</b>\n\n"
|
||
"У тебя пока нет подписки.\n"
|
||
"Купи тариф в разделе «Тарифы».\n\n"
|
||
f"База: <b>{format_gb(BASE_TRAFFIC_GB, digits=0)}</b> · "
|
||
f"<b>{BASE_DEVICES} устройств</b>\n"
|
||
f"Докупка: +500 ГБ = 50 ₽/мес · +1 устройство = "
|
||
f"{int(EXTRA_DEVICE_PRICE_RUB)} ₽/мес\n"
|
||
"Доп. опции списываются с баланса каждые 30 дней."
|
||
),
|
||
has_subscription=False,
|
||
)
|
||
|
||
bot_user = db.ensure_user(telegram_id) if db is not None else None
|
||
if db is not None:
|
||
await process_user_extras_billing(
|
||
db, sdk, telegram_id, login=login
|
||
)
|
||
if tariffs:
|
||
from services.subscription_renewal import (
|
||
process_user_subscription_renewal,
|
||
)
|
||
|
||
await process_user_subscription_renewal(
|
||
db,
|
||
sdk,
|
||
tariffs,
|
||
telegram_id,
|
||
bot=renewal_bot,
|
||
login=login,
|
||
)
|
||
bot_user = db.get_user(telegram_id)
|
||
|
||
extra_paid = effective_extra_devices(bot_user) if bot_user else 0
|
||
device_blocked = bot_user.device_blocked if bot_user else False
|
||
|
||
# Старые «безлимит» (0) → база 1000 ГБ
|
||
if float(getattr(user, "traffic_limit_bytes", 0) or 0) <= 0:
|
||
try:
|
||
user = await sdk.users.update_user(
|
||
UpdateUserRequestDto(
|
||
uuid=user.uuid,
|
||
traffic_limit_bytes=gb_to_bytes(BASE_TRAFFIC_GB),
|
||
traffic_limit_strategy=TrafficLimitStrategy.NO_RESET,
|
||
)
|
||
)
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
devices: DeviceUsage | None = None
|
||
if db is not None:
|
||
user, devices = await sync_device_policy(
|
||
sdk,
|
||
user,
|
||
db=db,
|
||
telegram_id=telegram_id,
|
||
extra_paid=extra_paid,
|
||
device_blocked=device_blocked,
|
||
)
|
||
|
||
expire = _as_utc(user.expire_at)
|
||
now = datetime.now(timezone.utc)
|
||
if devices and devices.over_limit:
|
||
status = "отключена (превышение устройств)"
|
||
elif expire > now and user.status == UserStatus.ACTIVE:
|
||
status = "активна"
|
||
else:
|
||
status = "истекла / неактивна"
|
||
traffic = traffic_from_panel_user(user)
|
||
devices_line = devices.format_line() if devices else f"📱 Устройства: база <b>{BASE_DEVICES}</b>"
|
||
extras_line = extras_billing_line(bot_user) if bot_user else ""
|
||
text = (
|
||
f"<b>Моя подписка</b>\n\n"
|
||
f"Статус: <b>{status}</b>\n"
|
||
f"До: <b>{expire.strftime('%d.%m.%Y %H:%M UTC')}</b>\n"
|
||
f"{devices_line}\n"
|
||
f"{traffic.format_line()}\n"
|
||
f"{extras_line}\n\n"
|
||
f"🔗 <code>{user.subscription_url}</code>\n\n"
|
||
f"Докупка: трафик +500 ГБ / <b>50 ₽/мес</b> · устройство +1 / "
|
||
f"<b>{int(EXTRA_DEVICE_PRICE_RUB)} ₽/мес</b>"
|
||
)
|
||
return SubscriptionInfo(
|
||
text=text,
|
||
has_subscription=True,
|
||
expire_at=expire,
|
||
subscription_url=user.subscription_url,
|
||
traffic=traffic,
|
||
devices=devices,
|
||
)
|
||
|
||
|
||
async def purchase_device_pack(
|
||
*,
|
||
db: Database,
|
||
sdk: RemnawaveSDK,
|
||
telegram_id: int,
|
||
pack: DevicePack,
|
||
charge_balance: bool = True,
|
||
login: str | None = None,
|
||
payment_meta: str | None = None,
|
||
) -> PurchaseResult:
|
||
bot_user = db.ensure_user(telegram_id)
|
||
balance = bot_user.balance
|
||
login = login or bot_user.login
|
||
|
||
existing = await _find_panel_user(sdk, telegram_id, login=login)
|
||
if existing is None:
|
||
return PurchaseResult(
|
||
ok=False,
|
||
message="Нет подписки. Сначала купи тариф (база 10 устройств).",
|
||
balance=balance,
|
||
)
|
||
|
||
if charge_balance:
|
||
ok, balance = db.try_charge(
|
||
telegram_id,
|
||
pack.price,
|
||
kind="devices",
|
||
meta=payment_meta or f"devices:{pack.id}",
|
||
)
|
||
if not ok:
|
||
return PurchaseResult(
|
||
ok=False,
|
||
message=(
|
||
f"Недостаточно средств.\n"
|
||
f"Нужно: <b>{pack.format_price()}</b>\n"
|
||
f"Баланс: <b>{balance:.0f} ₽</b>"
|
||
),
|
||
balance=balance,
|
||
)
|
||
|
||
new_extra = db.add_extra_devices(telegram_id, pack.slots)
|
||
on_extras_purchased(db, telegram_id)
|
||
try:
|
||
updated, devices = await sync_device_policy(
|
||
sdk,
|
||
existing,
|
||
db=db,
|
||
telegram_id=telegram_id,
|
||
extra_paid=new_extra,
|
||
device_blocked=False,
|
||
)
|
||
except Exception as exc: # noqa: BLE001
|
||
if charge_balance:
|
||
# откат слотов и денег
|
||
with db.pool.connection() as conn:
|
||
conn.execute(
|
||
"""
|
||
UPDATE users
|
||
SET extra_devices = GREATEST(extra_devices - %s, 0)
|
||
WHERE telegram_id = %s
|
||
""",
|
||
(pack.slots, telegram_id),
|
||
)
|
||
conn.commit()
|
||
balance = db.add_balance(
|
||
telegram_id,
|
||
pack.price,
|
||
kind="refund",
|
||
meta=f"refund:devices:{pack.id}:{exc}",
|
||
)
|
||
return PurchaseResult(
|
||
ok=False,
|
||
message=(
|
||
"Не удалось обновить лимит устройств. Средства возвращены.\n\n"
|
||
+ friendly_remnawave_error(exc)
|
||
),
|
||
balance=balance,
|
||
)
|
||
return PurchaseResult(
|
||
ok=False,
|
||
message="Оплата прошла, но лимит не обновился. Напиши админу.",
|
||
balance=balance,
|
||
)
|
||
|
||
return PurchaseResult(
|
||
ok=True,
|
||
message=(
|
||
f"✅ <b>Лимит устройств увеличен</b>\n\n"
|
||
f"Добавлено: <b>+{pack.slots}</b>\n"
|
||
f"Списано: <b>{pack.format_price()}</b> (первый месяц)\n"
|
||
f"Далее — <b>{int(EXTRA_DEVICE_PRICE_RUB)} ₽/мес</b> за каждое доп. устройство с баланса.\n"
|
||
f"Баланс: <b>{balance:.0f} ₽</b>\n"
|
||
f"{devices.format_line()}"
|
||
),
|
||
balance=balance,
|
||
subscription_url=getattr(updated, "subscription_url", None),
|
||
devices=devices,
|
||
)
|
||
|
||
|
||
async def purchase_traffic_pack(
|
||
*,
|
||
db: Database,
|
||
sdk: RemnawaveSDK,
|
||
telegram_id: int,
|
||
pack: TrafficPack,
|
||
charge_balance: bool = True,
|
||
login: str | None = None,
|
||
payment_meta: str | None = None,
|
||
) -> PurchaseResult:
|
||
bot_user = db.ensure_user(telegram_id)
|
||
balance = bot_user.balance
|
||
login = login or bot_user.login
|
||
|
||
existing = await _find_panel_user(sdk, telegram_id, login=login)
|
||
if existing is None:
|
||
return PurchaseResult(
|
||
ok=False,
|
||
message=(
|
||
"Нет активной подписки.\n"
|
||
"Сначала купи тариф — база 1000 ГБ уже входит."
|
||
),
|
||
balance=balance,
|
||
)
|
||
|
||
if charge_balance:
|
||
ok, balance = db.try_charge(
|
||
telegram_id,
|
||
pack.price,
|
||
kind="traffic",
|
||
meta=payment_meta or f"traffic:{pack.id}",
|
||
)
|
||
if not ok:
|
||
return PurchaseResult(
|
||
ok=False,
|
||
message=(
|
||
f"Недостаточно средств.\n"
|
||
f"Нужно: <b>{pack.format_price()}</b> (первый месяц)\n"
|
||
f"Баланс: <b>{balance:.0f} ₽</b>"
|
||
),
|
||
balance=balance,
|
||
)
|
||
|
||
db.add_extra_traffic_packs(telegram_id, pack.packs)
|
||
on_extras_purchased(db, telegram_id)
|
||
|
||
current_limit = ensure_base_limit_bytes(existing.traffic_limit_bytes)
|
||
new_limit = current_limit + gb_to_bytes(pack.gb)
|
||
|
||
try:
|
||
updated = await sdk.users.update_user(
|
||
UpdateUserRequestDto(
|
||
uuid=existing.uuid,
|
||
traffic_limit_bytes=new_limit,
|
||
traffic_limit_strategy=TrafficLimitStrategy.NO_RESET,
|
||
status=UserStatus.ACTIVE,
|
||
)
|
||
)
|
||
except Exception as exc: # noqa: BLE001
|
||
with db.pool.connection() as conn:
|
||
conn.execute(
|
||
"""
|
||
UPDATE users
|
||
SET extra_traffic_packs = GREATEST(extra_traffic_packs - %s, 0)
|
||
WHERE telegram_id = %s
|
||
""",
|
||
(pack.packs, telegram_id),
|
||
)
|
||
conn.commit()
|
||
if charge_balance:
|
||
balance = db.add_balance(
|
||
telegram_id,
|
||
pack.price,
|
||
kind="refund",
|
||
meta=f"refund:traffic:{pack.id}:{exc}",
|
||
)
|
||
return PurchaseResult(
|
||
ok=False,
|
||
message=(
|
||
"Не удалось увеличить трафик. Средства возвращены.\n\n"
|
||
+ friendly_remnawave_error(exc)
|
||
),
|
||
balance=balance,
|
||
)
|
||
return PurchaseResult(
|
||
ok=False,
|
||
message="Оплата прошла, но лимит не обновился. Напиши админу.\n\n"
|
||
+ friendly_remnawave_error(exc),
|
||
balance=balance,
|
||
)
|
||
|
||
traffic = traffic_from_panel_user(updated)
|
||
try:
|
||
from services.xui_wg import sync_wg_configs_limits
|
||
from services.awg_configs import sync_awg_configs_expiry
|
||
|
||
await sync_wg_configs_limits(
|
||
db=db,
|
||
telegram_id=telegram_id,
|
||
traffic_bytes=int(new_limit),
|
||
expire_at=_as_utc(updated.expire_at) if updated.expire_at else None,
|
||
restore_enabled=True,
|
||
)
|
||
await sync_awg_configs_expiry(
|
||
db=db,
|
||
sdk=sdk,
|
||
telegram_id=telegram_id,
|
||
login=login,
|
||
restore_enabled=True,
|
||
)
|
||
except Exception: # noqa: BLE001
|
||
logger.exception("WG/AWG sync after pack purchase failed")
|
||
|
||
return PurchaseResult(
|
||
ok=True,
|
||
message=(
|
||
f"✅ <b>Трафик увеличен</b>\n\n"
|
||
f"Добавлено: <b>{format_gb(pack.gb, digits=0)}</b>\n"
|
||
f"Списано: <b>{pack.format_price()}</b> (первый месяц)\n"
|
||
f"Далее — <b>{int(pack.price / pack.packs)} ₽/мес</b> за каждые 500 ГБ с баланса.\n"
|
||
f"Баланс: <b>{balance:.0f} ₽</b>\n"
|
||
f"{traffic.format_line()}"
|
||
),
|
||
balance=balance,
|
||
subscription_url=updated.subscription_url,
|
||
expire_at=_as_utc(updated.expire_at) if updated.expire_at else None,
|
||
traffic=traffic,
|
||
)
|
||
|
||
|
||
async def get_my_subscription(sdk: RemnawaveSDK, telegram_id: int) -> str:
|
||
return (await get_my_subscription_info(sdk, telegram_id)).text
|