Files
malenia/services/renewal_service.py

196 lines
7.3 KiB
Python

import datetime
import logging
from aiogram import Bot
from remnawave import RemnawaveSDK
from sqlalchemy.ext.asyncio import AsyncSession
from bot.texts import AUTORENEWAL_INSUFFICIENT_FUNDS, AUTORENEWAL_SUCCESS
from config import settings
from db.models.subscriptions import Subscription
from db.models.transactions import BalanceTxType
from misc import utils
from repositories.subscriptions import SubscriptionRepository
from repositories.users import UserRepository
from schemas.subscriptions import InternalSquads, SubscriptionPlan, SubscriptionStatus
from services import rw as rw_service
from services.admin_notifications import notify_subscription_renewal
logger = logging.getLogger(__name__)
class RenewalServiceError(Exception):
"""Base exception for renewal service errors."""
class InsufficientBalanceError(RenewalServiceError):
"""Raised when user has insufficient balance for renewal."""
class RenewalService:
def __init__(
self,
subscription_repository: SubscriptionRepository,
user_repository: UserRepository,
rw_sdk: RemnawaveSDK,
bot: Bot,
) -> None:
self.subscription_repository = subscription_repository
self.user_repository = user_repository
self.rw_sdk = rw_sdk
self.bot = bot
async def _calculate_renewal_price(self, subscription: Subscription) -> int:
duration = utils.convert_int_to_duration(subscription.duration_days)
plan = SubscriptionPlan(
devices=subscription.devices,
duration=duration,
whitelists=subscription.whitelists,
)
return utils.calculate_price(plan)
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}")
squads = [InternalSquads.DEFAULT]
if subscription.whitelists:
squads.append(InternalSquads.WHITELISTS)
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)
async def _notify_user(self, user_id: int, message: str) -> None:
try:
await self.bot.send_message(user_id, message)
except Exception:
logger.warning("Failed to notify user %d about renewal", user_id, exc_info=True)
async def _process_single_renewal(
self, session: AsyncSession, subscription: Subscription
) -> bool:
user_id = subscription.user_id
price = await self._calculate_renewal_price(subscription)
user = await self.user_repository.get_user_by_id(session, user_id)
if not user:
logger.error("User %d not found for autorenewal", user_id)
return False
if user.balance < price:
logger.info(
"Insufficient balance for user %d: balance=%d, price=%d",
user_id,
user.balance,
price,
)
await self.subscription_repository.update_subscription_autorenew(
session, user_id, False
)
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(
session,
user_id,
price,
tx_type=BalanceTxType.AUTORENEW,
description=f"autorenewal: devices={subscription.devices}, duration={subscription.duration_days}d, whitelists={subscription.whitelists}",
)
if not renewed_user:
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, new_expire)
except RenewalServiceError:
logger.exception("Failed to renew subscription in RW for user %d", user_id)
await self.user_repository.increase_user_balance(
session,
user_id,
price,
tx_type=BalanceTxType.REFUND,
description=f"refund for failed autorenewal: devices={subscription.devices}, duration={subscription.duration_days}d",
)
return False
await self.subscription_repository.update_sub(
session,
user_id,
end_date=new_expire,
autorenew=True,
status=SubscriptionStatus.ACTIVE,
devices=subscription.devices,
whitelists=subscription.whitelists,
duration_days=subscription.duration_days,
plan_price_paid=price,
)
await self._notify_user(
user_id,
AUTORENEWAL_SUCCESS.format(
price=price,
end_date=new_expire.strftime("%d.%m.%Y"),
),
)
await notify_subscription_renewal(
self.bot,
user_id=user_id,
amount=price,
balance_before=renewed_user.balance + price,
balance_after=renewed_user.balance,
devices=subscription.devices,
duration_days=subscription.duration_days,
whitelists=subscription.whitelists,
end_date=new_expire.strftime("%d.%m.%Y"),
)
logger.info("Successfully renewed subscription for user %d", user_id)
return True
async def process_autorenewals(self, session: AsyncSession) -> dict:
subscriptions = await self.subscription_repository.get_subscriptions_for_autorenewal(
session, days_before_expiry=settings.autorenew_days_before
)
logger.info("Found %d subscriptions for autorenewal", len(subscriptions))
results = {"success": 0, "failed": 0, "insufficient_balance": 0}
for subscription in subscriptions:
try:
success = await self._process_single_renewal(session, subscription)
if success:
results["success"] += 1
else:
user = await self.user_repository.get_user_by_id(session, subscription.user_id)
if user and user.balance < await self._calculate_renewal_price(subscription):
results["insufficient_balance"] += 1
else:
results["failed"] += 1
except Exception:
logger.exception("Error processing autorenewal for user %d", subscription.user_id)
results["failed"] += 1
logger.info(
"Autorenewal completed: success=%d, failed=%d, insufficient_balance=%d",
results["success"],
results["failed"],
results["insufficient_balance"],
)
return results