""" Celery задачи для подписок. """ from celery import shared_task from django.utils import timezone from datetime import timedelta import logging logger = logging.getLogger(__name__) @shared_task def check_expired_subscriptions(): """ Проверка истекших подписок. Запускается каждый день в 00:00. """ from .models import Subscription subscriptions = Subscription.objects.filter( status__in=['trial', 'active'] ) # Оптимизация: используем list() для кеширования queryset subscriptions_list = list(subscriptions) expired_count = 0 for subscription in subscriptions_list: subscription.check_expiration() if subscription.status in ['expired', 'past_due']: expired_count += 1 logger.info(f"Checked {len(subscriptions_list)} subscriptions, {expired_count} expired") return f"Проверено {subscriptions.count()} подписок, истекло {expired_count}" @shared_task def send_expiration_warnings(): """ Отправка предупреждений об истечении подписок. Отправляет уведомления за 7, 3 и 1 день до истечения. Запускается каждый день в 10:00. """ from .models import Subscription from apps.notifications.services import NotificationService now = timezone.now() warning_days = [7, 3, 1] sent_count = 0 for days in warning_days: warning_date = now + timedelta(days=days) # Находим подписки, истекающие через N дней # Оптимизация: используем select_related для избежания N+1 запросов subscriptions = Subscription.objects.filter( status__in=['trial', 'active'], end_date__date=warning_date.date(), auto_renew=False ).select_related('user', 'plan') for subscription in subscriptions: # Отправляем уведомление NotificationService.create_notification_with_telegram( recipient=subscription.user, notification_type='subscription_expiring', title=f'Подписка истекает через {days} дн.', message=f'Ваша подписка "{subscription.plan.name}" истекает через {days} дней. Продлите подписку, чтобы не потерять доступ.', related_object=subscription ) sent_count += 1 logger.info(f"Sent {sent_count} expiration warnings") return f"Отправлено {sent_count} предупреждений" @shared_task def reset_monthly_usage(): """ Сброс месячного использования. Запускается 1-го числа каждого месяца в 00:00. """ from .models import Subscription subscriptions = Subscription.objects.filter( status__in=['trial', 'active'] ) # Оптимизация: используем bulk_update вместо цикла с save() subscriptions_to_update = [] for subscription in subscriptions: subscription.lessons_used = 0 subscription.video_minutes_used = 0 subscriptions_to_update.append(subscription) if subscriptions_to_update: Subscription.objects.bulk_update( subscriptions_to_update, ['lessons_used', 'video_minutes_used'], batch_size=100 ) logger.info(f"Reset usage for {len(subscriptions_to_update)} subscriptions") return f"Сброшено использование для {subscriptions.count()} подписок" @shared_task def auto_renew_subscriptions(): """ Автопродление подписок. Продлевает подписки с auto_renew=True, которые истекают сегодня. Запускается каждый день в 02:00. """ from .models import Subscription, Payment today = timezone.now().date() # Находим подписки для продления # Оптимизация: используем select_related для избежания N+1 запросов subscriptions = Subscription.objects.filter( status='active', auto_renew=True, end_date__date=today ).select_related('user', 'plan') renewed_count = 0 failed_count = 0 for subscription in subscriptions: try: # Создаем платеж для продления payment = Payment.objects.create( user=subscription.user, subscription=subscription, amount=subscription.plan.price, currency=subscription.plan.currency, payment_method='card', # Используем сохраненную карту description=f"Автопродление подписки {subscription.plan.name}" ) # Здесь должна быть логика списания с сохраненной карты # Пока просто помечаем как успешный payment.mark_as_succeeded() # Продлеваем подписку subscription.renew() renewed_count += 1 except Exception as e: logger.error(f"Failed to renew subscription {subscription.id}: {e}") subscription.status = 'past_due' subscription.save() failed_count += 1 logger.info(f"Renewed {renewed_count} subscriptions, {failed_count} failed") return f"Продлено {renewed_count} подписок, ошибок {failed_count}" @shared_task def cleanup_old_payment_history(): """ Очистка старой истории платежей. Удаляет записи старше 1 года. Запускается 1-го числа каждого месяца в 03:00. """ from .models import PaymentHistory cutoff_date = timezone.now() - timedelta(days=365) deleted = PaymentHistory.objects.filter( created_at__lt=cutoff_date ).delete()[0] logger.info(f"Deleted {deleted} old payment history records") return f"Удалено {deleted} старых записей" @shared_task def generate_subscription_reports(): """ Генерация отчетов по подпискам. Создает ежемесячные отчеты по подпискам. Запускается 1-го числа каждого месяца в 09:00. """ from .models import Subscription, Payment from django.db.models import Count, Sum # Статистика за прошлый месяц now = timezone.now() start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) start_of_prev_month = (start_of_month - timedelta(days=1)).replace(day=1) # Подписки new_subscriptions = Subscription.objects.filter( created_at__gte=start_of_prev_month, created_at__lt=start_of_month ).count() cancelled_subscriptions = Subscription.objects.filter( cancelled_at__gte=start_of_prev_month, cancelled_at__lt=start_of_month ).count() # Платежи payments_stats = Payment.objects.filter( created_at__gte=start_of_prev_month, created_at__lt=start_of_month, status='succeeded' ).aggregate( total_count=Count('id'), total_amount=Sum('amount') ) report = { 'period': f"{start_of_prev_month.strftime('%Y-%m')}", 'new_subscriptions': new_subscriptions, 'cancelled_subscriptions': cancelled_subscriptions, 'total_payments': payments_stats['total_count'] or 0, 'total_revenue': float(payments_stats['total_amount'] or 0) } logger.info(f"Generated subscription report: {report}") return f"Создан отчет за {report['period']}"