diff --git a/app/services/monitoring_service.py b/app/services/monitoring_service.py index 76d168f9..2e8abf24 100644 --- a/app/services/monitoring_service.py +++ b/app/services/monitoring_service.py @@ -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', + '⚠️ Предупреждение о трафике\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)