diff --git a/bot/handlers/client/menus.py b/bot/handlers/client/menus.py index cb7179f..a0e6690 100644 --- a/bot/handlers/client/menus.py +++ b/bot/handlers/client/menus.py @@ -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( diff --git a/bot/keyboards/client.py b/bot/keyboards/client.py index 26c03ea..7dd64e0 100644 --- a/bot/keyboards/client.py +++ b/bot/keyboards/client.py @@ -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() diff --git a/bot/texts.py b/bot/texts.py index c232df0..d0983c7 100644 --- a/bot/texts.py +++ b/bot/texts.py @@ -239,7 +239,7 @@ AUTORENEWAL_INSUFFICIENT_FUNDS = ( ) AUTORENEWAL_SUCCESS = ( - f"{MALENIA_CARD} Подписка продлена автоматически. Списано {{price}}₽." + f"{MALENIA_CARD} Подписка продлена автоматически. Списано {{price}}₽.\n" "Новая дата окончания: {end_date}." ) diff --git a/repositories/subscriptions.py b/repositories/subscriptions.py index ad43b14..423b371 100644 --- a/repositories/subscriptions.py +++ b/repositories/subscriptions.py @@ -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 = ( diff --git a/services/admin_notifications.py b/services/admin_notifications.py index 667f2ad..5164b04 100644 --- a/services/admin_notifications.py +++ b/services/admin_notifications.py @@ -99,13 +99,13 @@ async def notify_subscription_renewal( bot, settings.admin_log_subscriptions_topic_id, ( - "Продление подписки", - f"Пользователь: {user_id}", - f"Стоимость: {amount}₽", - f"Баланс: {balance_before}₽ -> {balance_after}₽", - f"Устройства: {devices}", - f"Срок: {duration_days} дн.", - f"Whitelist: {'да' if whitelists else 'нет'}", - f"Новая дата окончания: {escape(end_date)}", + "Продление подписки\n\n" + f"Пользователь: {user_id}\n" + f"Стоимость: {amount}₽\n" + f"Баланс: {balance_before}₽ -> {balance_after}₽\n" + f"Устройства: {devices}\n" + f"Срок: {duration_days} дн.\n" + f"Whitelist: {'да' if whitelists else 'нет'}\n" + f"Новая дата окончания: {escape(end_date)}" ), ) diff --git a/services/renewal_service.py b/services/renewal_service.py index b13c275..38e0ffd 100644 --- a/services/renewal_service.py +++ b/services/renewal_service.py @@ -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) diff --git a/services/rw.py b/services/rw.py index ca29769..f269a49 100644 --- a/services/rw.py +++ b/services/rw.py @@ -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 diff --git a/services/user_service.py b/services/user_service.py index c67b7ea..643fab3 100644 --- a/services/user_service.py +++ b/services/user_service.py @@ -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, + ] + )