842 lines
26 KiB
Python
842 lines
26 KiB
Python
"""
|
||
Модели реферальной системы и промокодов.
|
||
"""
|
||
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 ReferralInvitedEmail(models.Model):
|
||
"""
|
||
Бэклог email-адресов, которые уже были приглашены (защита от накрутки).
|
||
Один email может быть в списке только один раз.
|
||
"""
|
||
email = models.EmailField(
|
||
unique=True,
|
||
db_index=True,
|
||
verbose_name='Email приглашённого'
|
||
)
|
||
referrer = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.CASCADE,
|
||
related_name='invited_emails',
|
||
verbose_name='Реферер'
|
||
)
|
||
referred_user = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.CASCADE,
|
||
related_name='+',
|
||
verbose_name='Приглашённый пользователь'
|
||
)
|
||
created_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name='Дата'
|
||
)
|
||
|
||
class Meta:
|
||
db_table = 'referrals_invited_emails'
|
||
verbose_name = 'Приглашённый email'
|
||
verbose_name_plural = 'Бэклог приглашённых email'
|
||
ordering = ['-created_at']
|
||
|
||
def __str__(self):
|
||
return f"{self.email} (пригласил: {self.referrer.email})"
|
||
|
||
|
||
class UserActivityDay(models.Model):
|
||
"""
|
||
Учёт дней активности пользователя на платформе (один день — одна запись).
|
||
Используется для проверки «реферал был активен 20+ дней» перед начислением бонуса.
|
||
"""
|
||
user = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.CASCADE,
|
||
related_name='activity_days',
|
||
verbose_name='Пользователь'
|
||
)
|
||
date = models.DateField(
|
||
verbose_name='Дата',
|
||
db_index=True
|
||
)
|
||
created_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name='Создано'
|
||
)
|
||
|
||
class Meta:
|
||
db_table = 'referrals_user_activity_days'
|
||
verbose_name = 'День активности'
|
||
verbose_name_plural = 'Дни активности'
|
||
unique_together = [['user', 'date']]
|
||
ordering = ['-date']
|
||
indexes = [
|
||
models.Index(fields=['user', 'date']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"{self.user.email} — {self.date}"
|
||
|
||
|
||
class PendingReferralBonus(models.Model):
|
||
"""
|
||
Ожидающее начисление бонуса за реферала.
|
||
Начисляется после 30 дней при условии 20+ дней активности реферала,
|
||
либо при достижении рефералом 21 дня активности (если был менее активен).
|
||
"""
|
||
STATUS_PENDING = 'pending'
|
||
STATUS_PAID = 'paid'
|
||
STATUS_CANCELLED = 'cancelled'
|
||
STATUS_CHOICES = [
|
||
(STATUS_PENDING, 'Ожидает'),
|
||
(STATUS_PAID, 'Начислено'),
|
||
(STATUS_CANCELLED, 'Отменено'),
|
||
]
|
||
|
||
referrer = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.CASCADE,
|
||
related_name='pending_referral_bonuses',
|
||
verbose_name='Реферер'
|
||
)
|
||
referred_user = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.CASCADE,
|
||
related_name='pending_bonuses_for_me',
|
||
verbose_name='Реферал'
|
||
)
|
||
referred_at = models.DateTimeField(
|
||
verbose_name='Дата приглашения',
|
||
db_index=True
|
||
)
|
||
points = models.IntegerField(
|
||
validators=[MinValueValidator(0)],
|
||
verbose_name='Очки к начислению'
|
||
)
|
||
level = models.IntegerField(
|
||
default=1,
|
||
validators=[MinValueValidator(1), MaxValueValidator(2)],
|
||
verbose_name='Уровень (1 — прямой, 2 — непрямой)'
|
||
)
|
||
reason = models.CharField(
|
||
max_length=255,
|
||
blank=True,
|
||
verbose_name='Причина'
|
||
)
|
||
status = models.CharField(
|
||
max_length=20,
|
||
choices=STATUS_CHOICES,
|
||
default=STATUS_PENDING,
|
||
db_index=True,
|
||
verbose_name='Статус'
|
||
)
|
||
paid_at = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='Дата начисления'
|
||
)
|
||
created_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name='Создано'
|
||
)
|
||
|
||
class Meta:
|
||
db_table = 'referrals_pending_referral_bonus'
|
||
verbose_name = 'Ожидающий бонус за реферала'
|
||
verbose_name_plural = 'Ожидающие бонусы за рефералов'
|
||
ordering = ['referred_at']
|
||
indexes = [
|
||
models.Index(fields=['status', 'referred_at']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"{self.referrer.email} <- {self.referred_user.email}: {self.points} очков ({self.get_status_display()})"
|
||
|
||
|
||
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} ₽"
|
||
|