231 lines
8.2 KiB
Python
231 lines
8.2 KiB
Python
"""
|
||
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']}"
|