feat: add traffic % warning check using user's threshold preference
This commit is contained in:
@@ -245,6 +245,7 @@ class MonitoringService:
|
||||
await self._check_trial_expiring_soon(db)
|
||||
await self._check_trial_channel_subscriptions(db)
|
||||
await self._check_expired_subscription_followups(db)
|
||||
await self._check_traffic_warnings(db)
|
||||
await self._check_low_balance_alerts(db)
|
||||
await self._retry_stuck_guest_purchases(db)
|
||||
await self._cleanup_inactive_users(db)
|
||||
@@ -518,17 +519,17 @@ class MonitoringService:
|
||||
|
||||
users_with_cards = await get_user_ids_with_active_payment_methods(db, autopay_user_ids)
|
||||
|
||||
from app.utils.notification_prefs import (
|
||||
get_subscription_expiry_days,
|
||||
is_subscription_expiry_enabled,
|
||||
)
|
||||
|
||||
for subscription in expiring_subscriptions:
|
||||
user = await get_user_by_id(db, subscription.user_id)
|
||||
if not user:
|
||||
continue
|
||||
|
||||
# Respect user notification preferences
|
||||
from app.utils.notification_prefs import (
|
||||
get_subscription_expiry_days,
|
||||
is_subscription_expiry_enabled,
|
||||
)
|
||||
|
||||
if not is_subscription_expiry_enabled(user):
|
||||
continue
|
||||
|
||||
@@ -1975,6 +1976,97 @@ class MonitoringService:
|
||||
except Exception:
|
||||
logger.error('Error retrying stuck PENDING_ACTIVATION guest purchases', exc_info=True)
|
||||
|
||||
async def _check_traffic_warnings(self, db: AsyncSession):
|
||||
"""Check subscriptions approaching traffic limit and notify users."""
|
||||
if not self.bot:
|
||||
return
|
||||
|
||||
try:
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.database.models import Subscription, User
|
||||
from app.utils.notification_prefs import get_traffic_warning_percent, is_traffic_warning_enabled
|
||||
|
||||
# Get active subscriptions with traffic limits (not unlimited)
|
||||
result = await db.execute(
|
||||
select(Subscription)
|
||||
.options(selectinload(Subscription.user))
|
||||
.where(
|
||||
Subscription.status.in_(['active', 'trial']),
|
||||
Subscription.traffic_limit_gb > 0,
|
||||
)
|
||||
)
|
||||
subscriptions = result.scalars().all()
|
||||
|
||||
sent_count = 0
|
||||
for subscription in subscriptions:
|
||||
user = subscription.user
|
||||
if not user or not user.telegram_id:
|
||||
continue
|
||||
|
||||
if not is_traffic_warning_enabled(user):
|
||||
continue
|
||||
|
||||
traffic_limit = subscription.traffic_limit_gb or 0
|
||||
traffic_used = subscription.traffic_used_gb or 0.0
|
||||
|
||||
if traffic_limit <= 0:
|
||||
continue
|
||||
|
||||
current_percent = (traffic_used / traffic_limit) * 100
|
||||
user_threshold = get_traffic_warning_percent(user)
|
||||
|
||||
if current_percent < user_threshold:
|
||||
continue
|
||||
|
||||
# Rate-limit: 1 notification per subscription per 24 hours
|
||||
cache_key_str = f'traffic_warn:{subscription.id}'
|
||||
try:
|
||||
already_sent = await cache.get(cache_key_str)
|
||||
if already_sent:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
language = getattr(user, 'language', 'ru') or 'ru'
|
||||
texts = get_texts(language)
|
||||
message = texts.get(
|
||||
'TRAFFIC_WARNING_ALERT',
|
||||
'⚠️ <b>Предупреждение о трафике</b>\n\n'
|
||||
'Использовано: {used:.1f} / {limit} ГБ ({percent:.0f}%)\n\n'
|
||||
'Ваш лимит трафика почти исчерпан.',
|
||||
)
|
||||
message = message.format(
|
||||
used=traffic_used,
|
||||
limit=traffic_limit,
|
||||
percent=current_percent,
|
||||
)
|
||||
await self.bot.send_message(
|
||||
user.telegram_id,
|
||||
message,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
try:
|
||||
await cache.set(cache_key_str, '1', expire=86400)
|
||||
except Exception:
|
||||
pass
|
||||
sent_count += 1
|
||||
except Exception as send_error:
|
||||
logger.debug(
|
||||
'Failed to send traffic warning',
|
||||
user_id=user.id,
|
||||
subscription_id=subscription.id,
|
||||
error=send_error,
|
||||
)
|
||||
|
||||
if sent_count > 0:
|
||||
logger.info('Traffic warnings sent', sent_count=sent_count)
|
||||
|
||||
except Exception as error:
|
||||
logger.error('Error checking traffic warnings', error=error)
|
||||
|
||||
async def _check_low_balance_alerts(self, db: AsyncSession):
|
||||
"""Check users with autopay enabled who have low balance and notify them."""
|
||||
if not self.bot:
|
||||
@@ -1983,7 +2075,6 @@ class MonitoringService:
|
||||
try:
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.database.crud.notification import notification_sent, record_notification
|
||||
from app.database.models import Subscription, User
|
||||
from app.utils.notification_prefs import get_balance_low_threshold, is_balance_low_enabled
|
||||
|
||||
@@ -2011,12 +2102,18 @@ class MonitoringService:
|
||||
if balance >= threshold:
|
||||
continue
|
||||
|
||||
# Don't spam — check if already notified today
|
||||
if await notification_sent(db, user.id, 0, 'low_balance'):
|
||||
continue
|
||||
# Rate-limit via Redis: max 1 notification per 24 hours per user
|
||||
cache_key_str = f'low_balance_alert:{user.id}'
|
||||
try:
|
||||
already_sent = await cache.get(cache_key_str)
|
||||
if already_sent:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
texts = get_texts(user.language)
|
||||
language = getattr(user, 'language', 'ru') or 'ru'
|
||||
texts = get_texts(language)
|
||||
threshold_rub = threshold / 100
|
||||
balance_rub = balance / 100
|
||||
message = texts.get(
|
||||
@@ -2035,7 +2132,11 @@ class MonitoringService:
|
||||
message,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
await record_notification(db, user.id, 0, 'low_balance')
|
||||
# Mark as sent for 24 hours
|
||||
try:
|
||||
await cache.set(cache_key_str, '1', expire=86400)
|
||||
except Exception:
|
||||
pass
|
||||
sent_count += 1
|
||||
except Exception as send_error:
|
||||
logger.debug('Failed to send low balance alert', user_id=user.id, error=send_error)
|
||||
|
||||
Reference in New Issue
Block a user