feat(subscription): improve subscription sync and renewal logic.
This commit is contained in:
@@ -25,9 +25,18 @@ from bot.texts import (
|
||||
)
|
||||
from db.models.users import User
|
||||
from misc import utils
|
||||
from misc.utils import build_main_menu_text, format_subscription_link
|
||||
from misc.utils import (
|
||||
build_main_menu_text,
|
||||
convert_duration_to_int,
|
||||
format_subscription_link,
|
||||
)
|
||||
from schemas.di import DependenciesDTO
|
||||
from schemas.subscriptions import InternalSquads, SubscriptionPlan
|
||||
from schemas.subscriptions import (
|
||||
InternalSquads,
|
||||
SubscriptionDuration,
|
||||
SubscriptionPlan,
|
||||
SubscriptionStatus,
|
||||
)
|
||||
from services.rw import delete_hwid, get_hwid_list, get_user_by_telegram_id
|
||||
|
||||
router = Router()
|
||||
@@ -140,25 +149,40 @@ async def sub_info(
|
||||
await cb.answer(CALLBACK_FALLBACK)
|
||||
return
|
||||
|
||||
dto = SubscriptionPlan(
|
||||
devices=max(rw_user.hwid_device_limit, 3),
|
||||
whitelists=bool(rw_user.active_squads.count(InternalSquads.WHITELISTS)),
|
||||
)
|
||||
if rw_user and not sub:
|
||||
try:
|
||||
dto = SubscriptionPlan(
|
||||
devices=rw_user.hwid_device_limit,
|
||||
whitelists=bool(rw_user.active_squads.count(InternalSquads.WHITELISTS)),
|
||||
discount=0,
|
||||
)
|
||||
sub = await deps.subscriptions_repository.create_subscription(
|
||||
session,
|
||||
user_id=user.id,
|
||||
end_date=rw_user.expire_at,
|
||||
devices=rw_user.hwid_device_limit,
|
||||
duration_days=30,
|
||||
devices=dto.devices,
|
||||
duration_days=convert_duration_to_int(SubscriptionDuration.MONTH) * 30,
|
||||
whitelists=dto.whitelists,
|
||||
plan_price_paid=utils.calculate_price(dto),
|
||||
)
|
||||
except Exception:
|
||||
await cb.answer(CALLBACK_FALLBACK)
|
||||
return
|
||||
else:
|
||||
await deps.subscriptions_repository.update_sub(
|
||||
session,
|
||||
cb.from_user.id,
|
||||
status=(
|
||||
SubscriptionStatus.ACTIVE
|
||||
if rw_user.status == "ACTIVE"
|
||||
else SubscriptionStatus.EXPIRED
|
||||
),
|
||||
end_date=rw_user.expire_at,
|
||||
devices=dto.devices,
|
||||
whitelists=dto.whitelists,
|
||||
duration_days=convert_duration_to_int(SubscriptionDuration.MONTH) * 30,
|
||||
plan_price_paid=utils.calculate_price(dto),
|
||||
autorenew=sub.autorenew
|
||||
)
|
||||
|
||||
await cb.message.edit_text(
|
||||
deps.user_service.format_subscription_message(
|
||||
|
||||
@@ -64,7 +64,9 @@ def subscription_controls(
|
||||
)
|
||||
builder.row(InlineKeyboardButton(text=autorenewal_text, callback_data="toggle:autorenewal"))
|
||||
|
||||
builder.row(InlineKeyboardButton(text="💻 Управление устройствами", callback_data="hwid_control"))
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="💻 Управление устройствами", callback_data="hwid_control")
|
||||
)
|
||||
builder.row(_return_to_menu_btn)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ AUTORENEWAL_INSUFFICIENT_FUNDS = (
|
||||
)
|
||||
|
||||
AUTORENEWAL_SUCCESS = (
|
||||
f"<b>{MALENIA_CARD} Подписка продлена автоматически. Списано <code>{{price}}₽</code>.</b>"
|
||||
f"<b>{MALENIA_CARD} Подписка продлена автоматически. Списано <code>{{price}}₽</code>.</b>\n"
|
||||
"Новая дата окончания: <code>{end_date}</code>."
|
||||
)
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ class SubscriptionRepository:
|
||||
select(Subscription)
|
||||
.where(
|
||||
Subscription.autorenew == True, # noqa: E712
|
||||
Subscription.status == SubscriptionStatus.ACTIVE,
|
||||
Subscription.end_date <= threshold,
|
||||
)
|
||||
.with_for_update()
|
||||
@@ -69,7 +68,7 @@ class SubscriptionRepository:
|
||||
whitelists: bool,
|
||||
duration_days: int,
|
||||
plan_price_paid: int,
|
||||
autorenew: bool = False,
|
||||
autorenew: bool,
|
||||
status: SubscriptionStatus = SubscriptionStatus.ACTIVE,
|
||||
) -> Subscription | None:
|
||||
stmt = (
|
||||
|
||||
@@ -99,13 +99,13 @@ async def notify_subscription_renewal(
|
||||
bot,
|
||||
settings.admin_log_subscriptions_topic_id,
|
||||
(
|
||||
"<b>Продление подписки</b>",
|
||||
f"Пользователь: <code>{user_id}</code>",
|
||||
f"Стоимость: <b>{amount}₽</b>",
|
||||
f"Баланс: {balance_before}₽ -> {balance_after}₽",
|
||||
f"Устройства: {devices}",
|
||||
f"Срок: {duration_days} дн.",
|
||||
f"Whitelist: {'да' if whitelists else 'нет'}",
|
||||
f"Новая дата окончания: {escape(end_date)}",
|
||||
"<b>Продление подписки</b>\n\n"
|
||||
f"Пользователь: <code>{user_id}</code>\n"
|
||||
f"Стоимость: <b>{amount}₽</b>\n"
|
||||
f"Баланс: {balance_before}₽ -> {balance_after}₽\n"
|
||||
f"Устройства: {devices}\n"
|
||||
f"Срок: {duration_days} дн.\n"
|
||||
f"Whitelist: {'да' if whitelists else 'нет'}\n"
|
||||
f"Новая дата окончания: {escape(end_date)}"
|
||||
),
|
||||
)
|
||||
|
||||
@@ -49,7 +49,9 @@ class RenewalService:
|
||||
)
|
||||
return utils.calculate_price(plan)
|
||||
|
||||
async def _renew_subscription_in_rw(self, subscription: Subscription) -> None:
|
||||
async def _renew_subscription_in_rw(
|
||||
self, subscription: Subscription, expire_at: datetime.datetime
|
||||
) -> None:
|
||||
rw_user = await rw_service.get_user_by_telegram_id(self.rw_sdk, subscription.user_id)
|
||||
if not rw_user:
|
||||
raise RenewalServiceError(f"RW user not found for user_id={subscription.user_id}")
|
||||
@@ -58,7 +60,7 @@ class RenewalService:
|
||||
if subscription.whitelists:
|
||||
squads.append(InternalSquads.WHITELISTS)
|
||||
|
||||
await rw_service.add_days(self.rw_sdk, rw_user.uuid, subscription.duration_days)
|
||||
await rw_service.update_expire_at(self.rw_sdk, rw_user.uuid, expire_at)
|
||||
await rw_service.update_squads(self.rw_sdk, rw_user.uuid, squads)
|
||||
await rw_service.set_hwid_limit(self.rw_sdk, rw_user.uuid, subscription.devices)
|
||||
|
||||
@@ -89,7 +91,9 @@ class RenewalService:
|
||||
await self.subscription_repository.update_subscription_autorenew(
|
||||
session, user_id, False
|
||||
)
|
||||
await self._notify_user(user_id, AUTORENEWAL_INSUFFICIENT_FUNDS)
|
||||
await self._notify_user(
|
||||
user_id, AUTORENEWAL_INSUFFICIENT_FUNDS.format(price=price, balance=user.balance)
|
||||
)
|
||||
return False
|
||||
|
||||
renewed_user = await self.user_repository.decrease_user_balance(
|
||||
@@ -104,8 +108,14 @@ class RenewalService:
|
||||
logger.error("Failed to decrease balance for user %d", user_id)
|
||||
return False
|
||||
|
||||
now_utc = datetime.datetime.now(datetime.UTC)
|
||||
if subscription.end_date > now_utc:
|
||||
new_expire = subscription.end_date + datetime.timedelta(days=subscription.duration_days)
|
||||
else:
|
||||
new_expire = now_utc + datetime.timedelta(days=subscription.duration_days)
|
||||
|
||||
try:
|
||||
await self._renew_subscription_in_rw(subscription)
|
||||
await self._renew_subscription_in_rw(subscription, new_expire)
|
||||
except RenewalServiceError:
|
||||
logger.exception("Failed to renew subscription in RW for user %d", user_id)
|
||||
await self.user_repository.increase_user_balance(
|
||||
@@ -117,11 +127,10 @@ class RenewalService:
|
||||
)
|
||||
return False
|
||||
|
||||
new_end_date = subscription.end_date + datetime.timedelta(days=subscription.duration_days)
|
||||
await self.subscription_repository.update_sub(
|
||||
session,
|
||||
user_id,
|
||||
end_date=new_end_date,
|
||||
end_date=new_expire,
|
||||
autorenew=True,
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
devices=subscription.devices,
|
||||
@@ -134,7 +143,7 @@ class RenewalService:
|
||||
user_id,
|
||||
AUTORENEWAL_SUCCESS.format(
|
||||
price=price,
|
||||
end_date=new_end_date.strftime("%d.%m.%Y"),
|
||||
end_date=new_expire.strftime("%d.%m.%Y"),
|
||||
),
|
||||
)
|
||||
await notify_subscription_renewal(
|
||||
@@ -146,7 +155,7 @@ class RenewalService:
|
||||
devices=subscription.devices,
|
||||
duration_days=subscription.duration_days,
|
||||
whitelists=subscription.whitelists,
|
||||
end_date=new_end_date.strftime("%d.%m.%Y"),
|
||||
end_date=new_expire.strftime("%d.%m.%Y"),
|
||||
)
|
||||
|
||||
logger.info("Successfully renewed subscription for user %d", user_id)
|
||||
|
||||
@@ -372,5 +372,15 @@ async def delete_hwid(sdk: RemnawaveSDK, user_uuid: str, hwid: str) -> bool:
|
||||
resp = await sdk.hwid.delete_hwid_to_user(body)
|
||||
return isinstance(resp, DeleteUserHwidDeviceResponseDto)
|
||||
except Exception:
|
||||
logger.exception("failed to fetch hwid list for user=%s", user_uuid)
|
||||
logger.exception("failed to delete hwid=%s for user=%s", hwid, user_uuid)
|
||||
return False
|
||||
|
||||
|
||||
async def update_expire_at(sdk: RemnawaveSDK, user_uuid: str, expire_at: datetime):
|
||||
dto = UpdateUserRequestDto(uuid=user_uuid, expire_at=expire_at) # type: ignore
|
||||
try:
|
||||
await sdk.users.update_user(body=dto)
|
||||
return True
|
||||
except Exception:
|
||||
logger.exception("failed to update expire_at=%s for user=%s", str(expire_at), user_uuid)
|
||||
return False
|
||||
|
||||
@@ -8,6 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from bot.texts import NO_SUBSCRIPTION, NOTHING_PLACEHOLDER, SUBSCRIPTION_INFORMATION
|
||||
from config import settings
|
||||
from db.models import Subscription
|
||||
from db.models.users import User
|
||||
from misc import utils
|
||||
from repositories import UserRepository
|
||||
@@ -122,7 +123,7 @@ class UserService:
|
||||
session,
|
||||
user.id,
|
||||
end_date=rw_user.expire_at + duration,
|
||||
autorenew=False,
|
||||
autorenew=db_subscription.autorenew,
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
devices=int(subscription.devices),
|
||||
whitelists=subscription.whitelists,
|
||||
@@ -179,7 +180,7 @@ class UserService:
|
||||
session,
|
||||
user.id,
|
||||
end_date=expire,
|
||||
autorenew=False,
|
||||
autorenew=db_subscription.autorenew,
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
devices=int(subscription.devices),
|
||||
whitelists=subscription.whitelists,
|
||||
@@ -239,3 +240,14 @@ class UserService:
|
||||
logger.exception("caught an exception while launching promo, skipping user...")
|
||||
|
||||
return sent
|
||||
|
||||
def validate_db_subscription(self, subscription: Subscription, rw_user: rw.RWUserInfo):
|
||||
return all(
|
||||
[
|
||||
(rw_user.status != "ACTIVE" and subscription.status == SubscriptionStatus.EXPIRED)
|
||||
or (
|
||||
rw_user.status == "ACTIVE" and subscription.status == SubscriptionStatus.ACTIVE
|
||||
),
|
||||
rw_user.expire_at == subscription.end_date,
|
||||
]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user