""" Модели реферальной системы и промокодов. """ import random import string from decimal import Decimal from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator from django.utils import timezone from django.conf import settings def generate_referral_code(): """Генерация уникального реферального кода.""" return ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) class ReferralSettings(models.Model): """ Настройки реферальной программы (singleton). """ # Проценты комиссии level1_commission = models.DecimalField( max_digits=5, decimal_places=2, default=10.00, validators=[MinValueValidator(0), MaxValueValidator(100)], verbose_name='Комиссия 1 уровня (%)', help_text='Процент от оплаты прямого реферала' ) level2_commission = models.DecimalField( max_digits=5, decimal_places=2, default=5.00, validators=[MinValueValidator(0), MaxValueValidator(100)], verbose_name='Комиссия 2 уровня (%)', help_text='Процент от оплаты реферала реферала' ) # Очки за рефералов points_direct_referral = models.IntegerField( default=5, validators=[MinValueValidator(0)], verbose_name='Очков за прямого реферала' ) points_indirect_referral = models.IntegerField( default=2, validators=[MinValueValidator(0)], verbose_name='Очков за реферала реферала' ) # Обновление updated_at = models.DateTimeField( auto_now=True, verbose_name='Обновлено' ) class Meta: verbose_name = 'Настройки реферальной программы' verbose_name_plural = 'Настройки реферальной программы' def __str__(self): return f"Настройки реферальной программы (обновлено: {self.updated_at})" def save(self, *args, **kwargs): """Гарантируем что существует только одна запись.""" self.pk = 1 super().save(*args, **kwargs) @classmethod def get_settings(cls): """Получить настройки (создать если не существует).""" obj, created = cls.objects.get_or_create(pk=1) return obj class ReferralLevel(models.Model): """ Уровни реферальной программы. """ level = models.IntegerField( unique=True, verbose_name='Уровень' ) name = models.CharField( max_length=100, verbose_name='Название' ) points_required = models.IntegerField( validators=[MinValueValidator(0)], verbose_name='Требуется очков', help_text='Минимальное количество очков для достижения уровня' ) bonus_payment_percent = models.IntegerField( validators=[MinValueValidator(0), MaxValueValidator(100)], verbose_name='% оплаты бонусами', help_text='Максимальный процент подписки, который можно оплатить бонусами' ) icon = models.CharField( max_length=50, blank=True, verbose_name='Иконка' ) class Meta: verbose_name = 'Уровень реферальной программы' verbose_name_plural = 'Уровни реферальной программы' ordering = ['level'] def __str__(self): return f"Уровень {self.level}: {self.name} ({self.points_required} очков)" class UserReferralProfile(models.Model): """ Реферальный профиль пользователя. """ user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='referral_profile', verbose_name='Пользователь' ) referral_code = models.CharField( max_length=20, unique=True, db_index=True, verbose_name='Реферальный код' ) referred_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='referrals', verbose_name='Пригласил' ) # Статистика total_points = models.IntegerField( default=0, validators=[MinValueValidator(0)], verbose_name='Всего очков' ) current_level = models.ForeignKey( ReferralLevel, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Текущий уровень' ) direct_referrals_count = models.IntegerField( default=0, validators=[MinValueValidator(0)], verbose_name='Прямых рефералов' ) indirect_referrals_count = models.IntegerField( default=0, validators=[MinValueValidator(0)], verbose_name='Непрямых рефералов' ) total_earned = models.DecimalField( max_digits=10, decimal_places=2, default=0, validators=[MinValueValidator(0)], verbose_name='Всего заработано (₽)' ) # Даты created_at = models.DateTimeField( auto_now_add=True, verbose_name='Создан' ) updated_at = models.DateTimeField( auto_now=True, verbose_name='Обновлен' ) class Meta: verbose_name = 'Реферальный профиль' verbose_name_plural = 'Реферальные профили' def __str__(self): return f"{self.user.email} - {self.referral_code}" def get_referral_link(self, base_url=''): """Получить реферальную ссылку.""" if not base_url: base_url = settings.FRONTEND_URL return f"{base_url}/register?ref={self.referral_code}" def update_level(self): """Обновить уровень на основе очков.""" new_level = ReferralLevel.objects.filter( points_required__lte=self.total_points ).order_by('-points_required').first() if new_level and new_level != self.current_level: self.current_level = new_level self.save(update_fields=['current_level', 'updated_at']) return True return False def add_points(self, points, reason=''): """Добавить очки.""" self.total_points += points self.save(update_fields=['total_points', 'updated_at']) self.update_level() # Создаем запись в истории PointsTransaction.objects.create( user=self.user, points=points, reason=reason, balance_after=self.total_points ) def get_max_bonus_payment_percent(self): """Получить максимальный процент оплаты бонусами.""" if self.current_level: return self.current_level.bonus_payment_percent # Уровень 1 по умолчанию level1 = ReferralLevel.objects.filter(level=1).first() return level1.bonus_payment_percent if level1 else 60 class BonusAccount(models.Model): """ Бонусный счет пользователя. """ user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='bonus_account', verbose_name='Пользователь' ) balance = models.DecimalField( max_digits=10, decimal_places=2, default=0, validators=[MinValueValidator(0)], verbose_name='Баланс (₽)' ) total_earned = models.DecimalField( max_digits=10, decimal_places=2, default=0, validators=[MinValueValidator(0)], verbose_name='Всего заработано (₽)' ) total_spent = models.DecimalField( max_digits=10, decimal_places=2, default=0, validators=[MinValueValidator(0)], verbose_name='Всего потрачено (₽)' ) created_at = models.DateTimeField( auto_now_add=True, verbose_name='Создан' ) updated_at = models.DateTimeField( auto_now=True, verbose_name='Обновлен' ) class Meta: verbose_name = 'Бонусный счет' verbose_name_plural = 'Бонусные счета' def __str__(self): return f"{self.user.email} - {self.balance} ₽" def add_bonus(self, amount, reason=''): """Добавить бонусы.""" amount = Decimal(str(amount)) self.balance += amount self.total_earned += amount self.save(update_fields=['balance', 'total_earned', 'updated_at']) # Создаем транзакцию BonusTransaction.objects.create( user=self.user, amount=amount, transaction_type='earn', reason=reason, balance_after=self.balance ) def spend_bonus(self, amount, reason=''): """Потратить бонусы.""" amount = Decimal(str(amount)) if amount > self.balance: raise ValueError('Недостаточно бонусов') self.balance -= amount self.total_spent += amount self.save(update_fields=['balance', 'total_spent', 'updated_at']) # Создаем транзакцию BonusTransaction.objects.create( user=self.user, amount=amount, transaction_type='spend', reason=reason, balance_after=self.balance ) class ReferralEarning(models.Model): """ История заработков с рефералов. """ referrer = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='referral_earnings', verbose_name='Реферер' ) referral = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='generated_earnings', verbose_name='Реферал' ) payment = models.ForeignKey( 'subscriptions.Payment', on_delete=models.CASCADE, verbose_name='Платеж' ) level = models.IntegerField( validators=[MinValueValidator(1), MaxValueValidator(2)], verbose_name='Уровень', help_text='1 - прямой реферал, 2 - реферал реферала' ) payment_amount = models.DecimalField( max_digits=10, decimal_places=2, validators=[MinValueValidator(0)], verbose_name='Сумма платежа (₽)' ) commission_percent = models.DecimalField( max_digits=5, decimal_places=2, validators=[MinValueValidator(0), MaxValueValidator(100)], verbose_name='Процент комиссии' ) earned_amount = models.DecimalField( max_digits=10, decimal_places=2, validators=[MinValueValidator(0)], verbose_name='Заработано (₽)' ) created_at = models.DateTimeField( auto_now_add=True, verbose_name='Создан' ) class Meta: verbose_name = 'Заработок с реферала' verbose_name_plural = 'Заработки с рефералов' ordering = ['-created_at'] def __str__(self): return f"{self.referrer.email} <- {self.referral.email}: {self.earned_amount} ₽" class PointsTransaction(models.Model): """ История начисления очков. """ user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='points_transactions', verbose_name='Пользователь' ) points = models.IntegerField( verbose_name='Очки' ) reason = models.CharField( max_length=255, verbose_name='Причина' ) balance_after = models.IntegerField( validators=[MinValueValidator(0)], verbose_name='Баланс после' ) created_at = models.DateTimeField( auto_now_add=True, verbose_name='Создан' ) class Meta: verbose_name = 'Транзакция очков' verbose_name_plural = 'Транзакции очков' ordering = ['-created_at'] def __str__(self): return f"{self.user.email}: {self.points:+d} очков" class BonusTransaction(models.Model): """ История транзакций бонусного счета. """ TRANSACTION_TYPES = [ ('earn', 'Начисление'), ('spend', 'Списание'), ] user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='bonus_transactions', verbose_name='Пользователь' ) amount = models.DecimalField( max_digits=10, decimal_places=2, validators=[MinValueValidator(0)], verbose_name='Сумма (₽)' ) transaction_type = models.CharField( max_length=10, choices=TRANSACTION_TYPES, verbose_name='Тип транзакции' ) reason = models.CharField( max_length=255, verbose_name='Причина' ) balance_after = models.DecimalField( max_digits=10, decimal_places=2, validators=[MinValueValidator(0)], verbose_name='Баланс после (₽)' ) created_at = models.DateTimeField( auto_now_add=True, verbose_name='Создан' ) class Meta: verbose_name = 'Транзакция бонусов' verbose_name_plural = 'Транзакции бонусов' ordering = ['-created_at'] def __str__(self): sign = '+' if self.transaction_type == 'earn' else '-' return f"{self.user.email}: {sign}{self.amount} ₽" class PromoCode(models.Model): """ Промокод для скидок на подписки. """ code = models.CharField( max_length=50, unique=True, db_index=True, verbose_name='Код' ) name = models.CharField( max_length=200, verbose_name='Название' ) description = models.TextField( blank=True, verbose_name='Описание' ) discount_type = models.CharField( max_length=10, choices=[ ('percent', 'Процент'), ('fixed', 'Фиксированная сумма'), ], default='percent', verbose_name='Тип скидки' ) discount_value = models.DecimalField( max_digits=10, decimal_places=2, validators=[MinValueValidator(0)], verbose_name='Размер скидки', help_text='Процент (0-100) или фиксированная сумма' ) # Применимость applicable_plans = models.ManyToManyField( 'subscriptions.SubscriptionPlan', blank=True, verbose_name='Применим к планам', help_text='Если не указано - применим ко всем планам' ) # Ограничения max_uses = models.IntegerField( null=True, blank=True, validators=[MinValueValidator(1)], verbose_name='Максимум использований', help_text='Оставьте пустым для неограниченного' ) current_uses = models.IntegerField( default=0, validators=[MinValueValidator(0)], verbose_name='Текущих использований' ) valid_until = models.DateTimeField( null=True, blank=True, verbose_name='Действителен до' ) # Статус is_active = models.BooleanField( default=True, verbose_name='Активен' ) # Статистика created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_promocodes', verbose_name='Создал' ) created_at = models.DateTimeField( auto_now_add=True, verbose_name='Создан' ) updated_at = models.DateTimeField( auto_now=True, verbose_name='Обновлен' ) class Meta: verbose_name = 'Промокод' verbose_name_plural = 'Промокоды' ordering = ['-created_at'] def __str__(self): return f"{self.code} - {self.name}" def is_valid(self): """Проверить валидность промокода.""" if not self.is_active: return False, 'Промокод неактивен' if self.valid_until and timezone.now() > self.valid_until: return False, 'Промокод истек' if self.max_uses and self.current_uses >= self.max_uses: return False, 'Промокод исчерпан' return True, 'OK' def can_apply_to_plan(self, plan): """Проверить применимость к плану.""" if not self.applicable_plans.exists(): return True # Применим ко всем return self.applicable_plans.filter(id=plan.id).exists() def calculate_discount(self, amount): """Рассчитать размер скидки.""" amount = Decimal(str(amount)) if self.discount_type == 'percent': discount = amount * (self.discount_value / 100) else: # fixed discount = min(self.discount_value, amount) return discount def use(self): """Использовать промокод.""" self.current_uses += 1 self.save(update_fields=['current_uses', 'updated_at']) class PromoCodeUsage(models.Model): """ История использования промокодов. """ user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='promocode_usages', verbose_name='Пользователь' ) promo_code = models.ForeignKey( PromoCode, on_delete=models.CASCADE, related_name='usages', verbose_name='Промокод' ) payment = models.ForeignKey( 'subscriptions.Payment', on_delete=models.CASCADE, null=True, blank=True, verbose_name='Платеж' ) original_amount = models.DecimalField( max_digits=10, decimal_places=2, validators=[MinValueValidator(0)], verbose_name='Исходная сумма (₽)' ) discount_amount = models.DecimalField( max_digits=10, decimal_places=2, validators=[MinValueValidator(0)], verbose_name='Размер скидки (₽)' ) final_amount = models.DecimalField( max_digits=10, decimal_places=2, validators=[MinValueValidator(0)], verbose_name='Итоговая сумма (₽)' ) created_at = models.DateTimeField( auto_now_add=True, verbose_name='Использован' ) class Meta: verbose_name = 'Использование промокода' verbose_name_plural = 'Использования промокодов' ordering = ['-created_at'] def __str__(self): return f"{self.user.email} - {self.promo_code.code}: -{self.discount_amount} ₽"