import arrow from paymentwall import Pingback from polymorphic.models import PolymorphicModel from polymorphic.managers import PolymorphicManager from django.db import models from django.contrib.auth import get_user_model from django.contrib.postgres.fields import ArrayField, JSONField from django.core.validators import RegexValidator from django.utils.timezone import now from django.conf import settings from project.utils import weekday_in_date_range from apps.course.models import Course from apps.config.models import Config from apps.school.models import SchoolSchedule from apps.notification.utils import send_email config = Config.load() User = get_user_model() CREDIT_CARD_RE = r'^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\d{11})$' class AuthorBalance(models.Model): IN = 0 OUT = 1 TYPE_CHOICES = ( (IN, 'In'), (OUT, 'Out'), ) PENDING = 0 ACCEPTED = 1 DECLINED = 2 STATUS_CHOICES = ( (PENDING, 'Pending'), (ACCEPTED, 'Accepted'), (DECLINED, 'Declined'), ) author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='Автор', null=True, blank=True, related_name='balances') type = models.PositiveSmallIntegerField('Тип', choices=TYPE_CHOICES, default=0) amount = models.DecimalField('Итого', max_digits=8, decimal_places=2, default=0) commission = models.DecimalField('Комиссия', max_digits=8, decimal_places=2, default=0) status = models.PositiveSmallIntegerField('Статус', choices=STATUS_CHOICES, default=0) payment = models.OneToOneField('Payment', on_delete=models.CASCADE, null=True, blank=True, verbose_name='Платёж') cause = models.TextField('Причина отказа', null=True, blank=True) card = models.CharField(max_length=20, validators=[RegexValidator(regex=CREDIT_CARD_RE)], null=True, blank=True) declined_send_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = 'Баланс' verbose_name_plural = 'Балансы' def save(self, *args, **kwargs): self.commission = self.calc_commission() if self.type == self.OUT: if self.status == self.DECLINED and not self.declined_send_at: send_email( 'Отказ вывода средств', self.author.email, 'notification/email/decline_withdrawal.html', author_balance=self, ) self.declined_send_at = now() super().save(*args, **kwargs) def calc_commission(self): return self.amount * config.SERVICE_COMMISSION / 100 class PaymentManger(PolymorphicManager): def all(self): return self.filter(status__isnull=False) class Payment(PolymorphicModel): PW_STATUS_CHOICES = ( (Pingback.PINGBACK_TYPE_REGULAR, 'regular',), (Pingback.PINGBACK_TYPE_GOODWILL, 'goodwill',), (Pingback.PINGBACK_TYPE_NEGATIVE, 'negative',), (Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW, 'risk under review',), (Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, 'risk reviewed accepted',), (Pingback.PINGBACK_TYPE_RISK_REVIEWED_DECLINED, 'risk reviewed declined',), (Pingback.PINGBACK_TYPE_RISK_AUTHORIZATION_VOIDED, 'risk authorization voided',), (Pingback.PINGBACK_TYPE_SUBSCRIPTION_CANCELLATION, 'subscription cancelation',), (Pingback.PINGBACK_TYPE_SUBSCRIPTION_EXPIRED, 'subscription expired',), (Pingback.PINGBACK_TYPE_SUBSCRIPTION_PAYMENT_FAILED, 'subscription payment failed',), ) user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='Пользователь', related_name='payments') amount = models.DecimalField('Итого', max_digits=8, decimal_places=2, default=0, editable=False) status = models.PositiveSmallIntegerField('Статус платежа', choices=PW_STATUS_CHOICES, null=True) data = JSONField('Данные платежа от провайдера', default={}, editable=False) roistat_visit = models.PositiveIntegerField('Номер визита Roistat', null=True, editable=False) created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) objects = PaymentManger() class Meta: verbose_name = 'Платеж' verbose_name_plural = 'Платежи' ordering = ('created_at',) def calc_commission(self): return self.amount * config.SERVICE_COMMISSION / 100 def is_deliverable(self): return self.status in [ Pingback.PINGBACK_TYPE_REGULAR, Pingback.PINGBACK_TYPE_GOODWILL, Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, ] def is_under_review(self): return self.status == Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW def is_cancelable(self): return self.status in [ Pingback.PINGBACK_TYPE_NEGATIVE, Pingback.PINGBACK_TYPE_RISK_REVIEWED_DECLINED, ] class CoursePayment(Payment): course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name='Курс', related_name='payments') class Meta: verbose_name = 'Платеж за курс' verbose_name_plural = 'Платежи за курсы' def save(self, *args, **kwargs): self.amount = self.course.price super().save(*args, **kwargs) author_balance = getattr(self, 'authorbalance', None) if not author_balance: AuthorBalance.objects.create( author=self.course.author, amount=self.amount, payment=self, ) else: author_balance.amount = self.amount author_balance.save() class SchoolPayment(Payment): weekdays = ArrayField(models.IntegerField(), size=7, verbose_name='Дни недели') add_days = models.BooleanField('Докупленные дни', default=False) date_start = models.DateField('Дата начала подписки', null=True, blank=True) date_end = models.DateField('Дата окончания подписки', null=True, blank=True) class Meta: verbose_name = 'Платеж за школу' verbose_name_plural = 'Платежи за школу' def __str__(self): days = ', '.join([ dict(SchoolSchedule.WEEKDAY_CHOICES).get(weekday, '') for weekday in sorted(self.weekdays) ]) return days def save(self, *args, **kwargs): aggregate = SchoolSchedule.objects.filter( weekday__in=self.weekdays, ).aggregate( models.Sum('month_price'), ) if self.add_days: _school_payment = SchoolPayment.objects.filter( add_days=False, date_start__lte=self.date_start, date_end__gte=self.date_end, status__in=[ Pingback.PINGBACK_TYPE_REGULAR, Pingback.PINGBACK_TYPE_GOODWILL, Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, ], ).last() weekday_count = weekday_in_date_range(self.date_start, self.date_end, self.weekdays[0]) all_weekday_count = weekday_in_date_range(_school_payment.date_start, _school_payment.date_end, self.weekdays[0]) month_price_sum = aggregate.get('month_price__sum', 0) * weekday_count // all_weekday_count else: month_price_sum = aggregate.get('month_price__sum', 0) if self.id is None and month_price_sum >= config.SERVICE_DISCOUNT_MIN_AMOUNT: discount = config.SERVICE_DISCOUNT else: discount = 0 self.amount = month_price_sum - discount super().save(*args, **kwargs) @property def date_end_humanize(self): return arrow.get(self.date_end, settings.TIME_ZONE).humanize(locale='ru')