You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
167 lines
6.6 KiB
167 lines
6.6 KiB
from paymentwall import Pingback
|
|
from polymorphic.models import PolymorphicModel
|
|
|
|
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 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 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)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
update_at = models.DateTimeField(auto_now=True)
|
|
|
|
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='Дни недели')
|
|
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'),
|
|
)
|
|
month_price_sum = aggregate.get('month_price__sum', 0)
|
|
if 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)
|
|
|