Add auto-renewal and cancellation handling for tribute subscriptions
- Implemented a new method to handle expired tribute subscriptions, automatically renewing them if no cancellation was received. - Added functionality to manage tribute subscription cancellations, setting a grace period and notifying users accordingly. - Updated localization files to include messages for auto-renewal and cancellation notifications in both English and Russian, enhancing user communication.
This commit is contained in:
@@ -41,6 +41,70 @@ class PanelWebhookService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to send notification to {user_id}: {e}")
|
logging.error(f"Failed to send notification to {user_id}: {e}")
|
||||||
|
|
||||||
|
async def _handle_expired_subscription(self, session, user_id: int, user_payload: dict,
|
||||||
|
lang: str, markup, first_name: str):
|
||||||
|
"""Handle expired subscription - auto-renew tribute users if no cancellation was received"""
|
||||||
|
from db.dal import subscription_dal, payment_dal
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if user has tribute subscriptions that weren't cancelled
|
||||||
|
user_subs = await subscription_dal.get_active_subscriptions_for_user(session, user_id)
|
||||||
|
|
||||||
|
for sub in user_subs:
|
||||||
|
# Check if this subscription was marked as cancelled (from tribute cancellation webhook)
|
||||||
|
if sub.status_from_panel == 'CANCELLED':
|
||||||
|
logging.info(f"Subscription {sub.subscription_id} for user {user_id} was cancelled, skipping auto-renewal")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if this user has tribute payments
|
||||||
|
last_tribute_duration = await payment_dal.get_last_tribute_payment_duration(session, user_id)
|
||||||
|
|
||||||
|
if last_tribute_duration is not None:
|
||||||
|
# This user has tribute payments, auto-renew for the same duration
|
||||||
|
logging.info(f"Auto-renewing tribute subscription for user {user_id} for {last_tribute_duration} months")
|
||||||
|
|
||||||
|
# Extend subscription by the last payment duration
|
||||||
|
new_end_date = datetime.now(timezone.utc) + timedelta(days=last_tribute_duration * 30)
|
||||||
|
|
||||||
|
await subscription_dal.update_subscription(
|
||||||
|
session,
|
||||||
|
sub.subscription_id,
|
||||||
|
{
|
||||||
|
'end_date': new_end_date,
|
||||||
|
'status_from_panel': 'ACTIVE',
|
||||||
|
'is_active': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send auto-renewal notification
|
||||||
|
_ = lambda k, **kw: self.i18n.gettext(lang, k, **kw) if self.i18n else k
|
||||||
|
auto_renewal_msg = _(
|
||||||
|
"tribute_auto_renewal",
|
||||||
|
default="🔄 <b>Подписка автоматически продлена</b>\n\n"
|
||||||
|
"Ваша подписка Tribute была автоматически продлена на {months} мес.\n"
|
||||||
|
"Новая дата окончания: {end_date}",
|
||||||
|
user_name=first_name,
|
||||||
|
months=last_tribute_duration,
|
||||||
|
end_date=new_end_date.strftime('%Y-%m-%d')
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.bot.send_message(
|
||||||
|
user_id,
|
||||||
|
auto_renewal_msg,
|
||||||
|
reply_markup=markup,
|
||||||
|
parse_mode="HTML"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to send auto-renewal notification to user {user_id}: {e}")
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error handling expired subscription for user {user_id}: {e}")
|
||||||
|
await session.rollback()
|
||||||
|
|
||||||
async def handle_event(self, event_name: str, user_payload: dict):
|
async def handle_event(self, event_name: str, user_payload: dict):
|
||||||
telegram_id = user_payload.get("telegramId")
|
telegram_id = user_payload.get("telegramId")
|
||||||
if not telegram_id:
|
if not telegram_id:
|
||||||
@@ -70,6 +134,9 @@ class PanelWebhookService:
|
|||||||
end_date=user_payload.get("expireAt", "")[:10],
|
end_date=user_payload.get("expireAt", "")[:10],
|
||||||
)
|
)
|
||||||
elif event_name == "user.expired" and self.settings.SUBSCRIPTION_NOTIFY_ON_EXPIRE:
|
elif event_name == "user.expired" and self.settings.SUBSCRIPTION_NOTIFY_ON_EXPIRE:
|
||||||
|
# Check if this is a tribute user that should be auto-renewed
|
||||||
|
await self._handle_expired_subscription(session, user_id, user_payload, lang, markup, first_name)
|
||||||
|
|
||||||
await self._send_message(
|
await self._send_message(
|
||||||
user_id,
|
user_id,
|
||||||
lang,
|
lang,
|
||||||
|
|||||||
@@ -201,10 +201,75 @@ class TributeService:
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to send tribute payment notification: {e}")
|
logging.error(f"Failed to send tribute payment notification: {e}")
|
||||||
|
|
||||||
|
elif event_name == 'subscription_cancelled':
|
||||||
|
# Handle tribute subscription cancellation
|
||||||
|
await self._handle_tribute_cancellation(session, user_id, bot, i18n)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return web.Response(status=200, text="ok")
|
return web.Response(status=200, text="ok")
|
||||||
|
|
||||||
|
async def _handle_tribute_cancellation(self, session, user_id: int, bot: Bot, i18n: JsonI18n):
|
||||||
|
"""Handle tribute subscription cancellation - set subscription to 1 day grace period"""
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from db.dal import subscription_dal, user_dal
|
||||||
|
from bot.keyboards.inline.user_keyboards import get_subscribe_only_markup
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set all user's subscriptions to expire in 1 day (grace period)
|
||||||
|
grace_end_date = datetime.now(timezone.utc) + timedelta(days=1)
|
||||||
|
|
||||||
|
# Get all active subscriptions for the user
|
||||||
|
user_subs = await subscription_dal.get_active_subscriptions_for_user(session, user_id)
|
||||||
|
|
||||||
|
for sub in user_subs:
|
||||||
|
await subscription_dal.update_subscription(
|
||||||
|
session,
|
||||||
|
sub.subscription_id,
|
||||||
|
{
|
||||||
|
'end_date': grace_end_date,
|
||||||
|
'status_from_panel': 'CANCELLED',
|
||||||
|
'skip_notifications': True # Skip future notifications for cancelled subs
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
# Send notification about cancellation if enabled
|
||||||
|
if not self.settings.TRIBUTE_SKIP_CANCELLATION_NOTIFICATIONS:
|
||||||
|
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||||
|
lang = db_user.language_code if db_user and db_user.language_code else self.settings.DEFAULT_LANGUAGE
|
||||||
|
first_name = db_user.first_name or f"User {user_id}" if db_user else f"User {user_id}"
|
||||||
|
|
||||||
|
_ = lambda k, **kw: i18n.gettext(lang, k, **kw) if i18n else k
|
||||||
|
markup = get_subscribe_only_markup(lang, i18n)
|
||||||
|
|
||||||
|
cancellation_msg = _(
|
||||||
|
"tribute_subscription_cancelled",
|
||||||
|
default="🚨 <b>Подписка отменена</b>\n\n"
|
||||||
|
"Ваша подписка Tribute была отменена. У вас есть 24 часа для восстановления доступа, "
|
||||||
|
"после чего подписка будет заблокирована.\n\n"
|
||||||
|
"Для продления подписки нажмите кнопку ниже.",
|
||||||
|
user_name=first_name
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.send_message(
|
||||||
|
user_id,
|
||||||
|
cancellation_msg,
|
||||||
|
reply_markup=markup,
|
||||||
|
parse_mode="HTML"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to send tribute cancellation notification to user {user_id}: {e}")
|
||||||
|
|
||||||
|
logging.info(f"Tribute subscription cancelled for user {user_id}, grace period set to 1 day")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error handling tribute cancellation for user {user_id}: {e}")
|
||||||
|
await session.rollback()
|
||||||
|
|
||||||
|
|
||||||
async def tribute_webhook_route(request: web.Request):
|
async def tribute_webhook_route(request: web.Request):
|
||||||
"""AIOHTTP route handler for Tribute webhook calls."""
|
"""AIOHTTP route handler for Tribute webhook calls."""
|
||||||
|
|||||||
@@ -170,3 +170,17 @@ async def get_financial_statistics(session: AsyncSession) -> Dict[str, Any]:
|
|||||||
"all_time_revenue": float(all_amount),
|
"all_time_revenue": float(all_amount),
|
||||||
"today_payments_count": today_payments_count
|
"today_payments_count": today_payments_count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_last_tribute_payment_duration(session: AsyncSession, user_id: int) -> Optional[int]:
|
||||||
|
"""Get duration in months from the last successful tribute payment for a user."""
|
||||||
|
stmt = select(Payment.subscription_duration_months).where(
|
||||||
|
and_(
|
||||||
|
Payment.user_id == user_id,
|
||||||
|
Payment.provider == 'tribute',
|
||||||
|
Payment.status == 'succeeded'
|
||||||
|
)
|
||||||
|
).order_by(Payment.created_at.desc()).limit(1)
|
||||||
|
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ async def get_subscription_by_panel_subscription_uuid(
|
|||||||
return result.scalar_one_or_none()
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_active_subscriptions_for_user(session: AsyncSession, user_id: int) -> List[Subscription]:
|
||||||
|
"""Get all active subscriptions for a user."""
|
||||||
|
stmt = select(Subscription).where(
|
||||||
|
Subscription.user_id == user_id,
|
||||||
|
Subscription.is_active == True
|
||||||
|
).order_by(Subscription.end_date.desc())
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
async def update_subscription(
|
async def update_subscription(
|
||||||
session: AsyncSession, subscription_id: int,
|
session: AsyncSession, subscription_id: int,
|
||||||
update_data: Dict[str, Any]) -> Optional[Subscription]:
|
update_data: Dict[str, Any]) -> Optional[Subscription]:
|
||||||
|
|||||||
@@ -232,6 +232,8 @@
|
|||||||
"subscription_24h_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expires in 1 day — {end_date}.\n\nPlease renew it using the button below.",
|
"subscription_24h_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expires in 1 day — {end_date}.\n\nPlease renew it using the button below.",
|
||||||
"subscription_expired_notification": "👋 Hi, {user_name}!\n\n⛔ Your VPN subscription expired on {end_date}.\n\nPlease renew it using the button below.",
|
"subscription_expired_notification": "👋 Hi, {user_name}!\n\n⛔ Your VPN subscription expired on {end_date}.\n\nPlease renew it using the button below.",
|
||||||
"subscription_expired_yesterday_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expired yesterday ({end_date}).\n\nPlease renew it using the button below.",
|
"subscription_expired_yesterday_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expired yesterday ({end_date}).\n\nPlease renew it using the button below.",
|
||||||
|
"tribute_subscription_cancelled": "🚨 <b>Subscription Cancelled</b>\n\nYour Tribute subscription has been cancelled. You have 24 hours to restore access, after which the subscription will be blocked.\n\nTo renew your subscription, press the button below.",
|
||||||
|
"tribute_auto_renewal": "🔄 <b>Subscription Auto-Renewed</b>\n\nYour Tribute subscription has been automatically renewed for {months} months.\nNew expiration date: {end_date}",
|
||||||
|
|
||||||
"admin_new_trial_notification": "\ud83c\udf21 User {user_id} activated a free trial until {end_date}.",
|
"admin_new_trial_notification": "\ud83c\udf21 User {user_id} activated a free trial until {end_date}.",
|
||||||
|
|
||||||
|
|||||||
@@ -232,6 +232,8 @@
|
|||||||
"subscription_24h_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекает через 1 день — {end_date}.\n\nПродлите её по кнопке ниже.",
|
"subscription_24h_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекает через 1 день — {end_date}.\n\nПродлите её по кнопке ниже.",
|
||||||
"subscription_expired_notification": "👋 Привет, {user_name}!\n\n⛔ Срок вашей подписки на VPN истек ({end_date}).\n\nПродлите её по кнопке ниже.",
|
"subscription_expired_notification": "👋 Привет, {user_name}!\n\n⛔ Срок вашей подписки на VPN истек ({end_date}).\n\nПродлите её по кнопке ниже.",
|
||||||
"subscription_expired_yesterday_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекла сутки назад ({end_date}).\n\nПродлите её по кнопке ниже.",
|
"subscription_expired_yesterday_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекла сутки назад ({end_date}).\n\nПродлите её по кнопке ниже.",
|
||||||
|
"tribute_subscription_cancelled": "🚨 <b>Подписка отменена</b>\n\nВаша подписка Tribute была отменена. У вас есть 24 часа для восстановления доступа, после чего подписка будет заблокирована.\n\nДля продления подписки нажмите кнопку ниже.",
|
||||||
|
"tribute_auto_renewal": "🔄 <b>Подписка автоматически продлена</b>\n\nВаша подписка Tribute была автоматически продлена на {months} мес.\nНовая дата окончания: {end_date}",
|
||||||
|
|
||||||
"admin_new_trial_notification": "\ud83c\udf21 Пользователь {user_id} активировал пробный период до {end_date}.",
|
"admin_new_trial_notification": "\ud83c\udf21 Пользователь {user_id} активировал пробный период до {end_date}.",
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user