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,
+ ]
+ )