uchill/backend/apps/subscriptions/tasks.py

231 lines
8.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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']}"