From 34cf929b605505ba949a2fe7dd56c027c7532a6a Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Sun, 4 Mar 2018 20:58:10 +0300 Subject: [PATCH] LIL-263,269,270,272,274 --- .../email/decline_withdrawal.html | 8 ++ .../migrations/0013_auto_20180304_1757.py | 36 +++++++++ apps/payment/models.py | 47 +++++++++++- apps/payment/views.py | 8 +- apps/user/fields.py | 25 +++++++ apps/user/forms.py | 7 ++ apps/user/templates/user/payment-history.html | 75 ++++++++++++++----- apps/user/views.py | 41 +++++++++- 8 files changed, 222 insertions(+), 25 deletions(-) create mode 100644 apps/notification/templates/notification/email/decline_withdrawal.html create mode 100644 apps/payment/migrations/0013_auto_20180304_1757.py create mode 100644 apps/user/fields.py diff --git a/apps/notification/templates/notification/email/decline_withdrawal.html b/apps/notification/templates/notification/email/decline_withdrawal.html new file mode 100644 index 00000000..814bc35d --- /dev/null +++ b/apps/notification/templates/notification/email/decline_withdrawal.html @@ -0,0 +1,8 @@ +{% extends "notification/email/_base.html" %} + +{% block content %} +

К сожалению вам отказано в выводе средств!

+
+

{{ author_balance.cause }}

+
+{% endblock content %} diff --git a/apps/payment/migrations/0013_auto_20180304_1757.py b/apps/payment/migrations/0013_auto_20180304_1757.py new file mode 100644 index 00000000..e0e16525 --- /dev/null +++ b/apps/payment/migrations/0013_auto_20180304_1757.py @@ -0,0 +1,36 @@ +# Generated by Django 2.0.2 on 2018-03-04 17:57 + +import django.core.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('payment', '0012_auto_20180302_0740'), + ] + + operations = [ + migrations.AddField( + model_name='authorbalance', + name='card', + field=models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(regex='^(?: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})$')]), + ), + migrations.AddField( + model_name='authorbalance', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='authorbalance', + name='declined_send_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='authorbalance', + name='update_at', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/apps/payment/models.py b/apps/payment/models.py index dc8426dd..6cf3402a 100644 --- a/apps/payment/models.py +++ b/apps/payment/models.py @@ -1,6 +1,7 @@ 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 constance import config from paymentwall import Pingback @@ -8,9 +9,12 @@ from polymorphic.models import PolymorphicModel from apps.course.models import Course from apps.school.models import SchoolSchedule +from apps.notification.utils import send_email 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 @@ -35,11 +39,32 @@ class AuthorBalance(models.Model): 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 = ( @@ -66,6 +91,22 @@ class Payment(PolymorphicModel): 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') @@ -83,11 +124,9 @@ class CoursePayment(Payment): author=self.course.author, amount=self.amount, payment=self, - commission=self.calc_commission(), ) else: author_balance.amount = self.amount - author_balance.commission = self.calc_commission() author_balance.save() @@ -100,6 +139,10 @@ class SchoolPayment(Payment): 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, diff --git a/apps/payment/views.py b/apps/payment/views.py index 40b193f4..4d195254 100644 --- a/apps/payment/views.py +++ b/apps/payment/views.py @@ -1,6 +1,8 @@ import logging import json +from datetime import timedelta + from django.contrib import messages from django.http import HttpResponse from django.shortcuts import redirect @@ -8,6 +10,7 @@ from django.views.generic import View, TemplateView from django.views.decorators.csrf import csrf_exempt from django.urls import reverse_lazy from django.utils.decorators import method_decorator +from django.utils.timezone import now from paymentwall import Pingback, Product, Widget @@ -123,10 +126,13 @@ class PaymentwallCallbackView(View): ) payment.status = pingback.get_type() payment.data = payment_raw_data + if pingback.is_deliverable() and product_type_name == 'school': + payment.date_start = now() + payment.date_end = now() + timedelta(days=30) payment.save() author_balance = getattr(payment, 'author_balance', None) - if author_balance: + if author_balance and author_balance.type == AuthorBalance.IN: if pingback.is_deliverable(): payment.author_balance.status = AuthorBalance.ACCEPTED elif pingback.is_under_review(): diff --git a/apps/user/fields.py b/apps/user/fields.py new file mode 100644 index 00000000..60063b86 --- /dev/null +++ b/apps/user/fields.py @@ -0,0 +1,25 @@ +import re + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +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 CreditCardField(forms.CharField): + """ + Form field that validates credit card numbers. + """ + + default_error_messages = { + 'required': _(u'Номер карты обязателен.'), + 'invalid': _(u'Неверный номер карты.'), + } + + def clean(self, value): + value = value.replace(' ', '').replace('-', '') + if self.required and not value: + raise forms.utils.ValidationError(self.error_messages['required']) + if value and not re.match(CREDIT_CARD_RE, value): + raise forms.utils.ValidationError(self.error_messages['invalid']) + return value diff --git a/apps/user/forms.py b/apps/user/forms.py index 669a0fa0..2f75ef68 100644 --- a/apps/user/forms.py +++ b/apps/user/forms.py @@ -1,6 +1,8 @@ from django import forms from django.contrib.auth import get_user_model +from .fields import CreditCardField + User = get_user_model() @@ -47,3 +49,8 @@ class UserEditForm(forms.ModelForm): 'vkontakte', 'photo', ) + + +class WithdrawalForm(forms.Form): + amount = forms.DecimalField(required=True, min_value=2000) + card = CreditCardField(required=True) diff --git a/apps/user/templates/user/payment-history.html b/apps/user/templates/user/payment-history.html index f1daeb23..de91e85b 100644 --- a/apps/user/templates/user/payment-history.html +++ b/apps/user/templates/user/payment-history.html @@ -22,59 +22,98 @@ {% endif %} +{% if messages %} +
+
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+
+{% endif %} +{% if request.user.role == 1 or request.user.role == 2 %}
Вывести деньги со счета
-
+
{% csrf_token %}
На вашем счету {{ request.user.balance }} руб.
-
+
СУММА
- +
Размер выводимой суммы не должно быть менее 2000 рублей.
+ {% if form.amount.errors %} +
{{ form.amount.errors }}
+ {% endif %}
-
+
НОМЕР КРЕДИТНОЙ КАРТЫ *
- +
+ {% if form.card.errors %} +
{{ form.card.errors }}
+ {% endif %}
-
+
+{% endif %}
История платежей
- {% if request.user.role == 1 or request.user.role == 2 %} - {% for balance in request.user.balances.all %} -
-
{{balance.payment.course.title}}
-
{{balance.amount}}
-
Получено
-
- {% endfor %} - {% else %} {% for payment in request.user.payments.all %}
-
{{payment.course.title}}
+ {% if payment.course %} +
Курс. {{payment.course.title}}
+ {% else %} +
+ Школа. {% if payment.date_start and payment.date_end %}{{ payment.date_start }} - {{ payment.date_end }}{% endif %} + {{ payment }} +
+ {% endif %} + {% if payment.balance %} +
{{payment.balance.amount}}
+ {% else %}
{{payment.amount}}
-
Получено
+ {% endif %} + {% if payment.balance.type == 1 %} +
+ {% if payment.balance.status == 0 %} + Ожидается подтверждение выплаты + {% elif payment.balance.status == 1 %} + Выплачено + {% else %} + Выплата отменена + Причина: "{{ payment.balance.cause }} + {% endif %} +
+ {% else %} +
+ {% if payment.is_deliverable %} + Получено + {% elif payment.is_under_review %} + Ожидается подтверждение оплаты + {% else %} + Ошибка оплаты + {% endif %} +
+ {% endif %}
{% endfor %} - {% endif %}
diff --git a/apps/user/views.py b/apps/user/views.py index 0cb5b6c1..6d959069 100644 --- a/apps/user/views.py +++ b/apps/user/views.py @@ -1,9 +1,12 @@ from io import BytesIO from PIL import Image from os.path import splitext +from datetime import timedelta + from django.contrib.auth import login +from django.core.exceptions import ValidationError from django.shortcuts import render, reverse, redirect -from django.views.generic import DetailView, UpdateView, TemplateView +from django.views.generic import DetailView, UpdateView, TemplateView, FormView from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required, permission_required @@ -11,13 +14,14 @@ from django.contrib.auth.hashers import check_password, make_password from django.http import Http404 from django.urls import reverse_lazy from django.utils.decorators import method_decorator +from django.utils.timezone import now from apps.auth.tokens import verification_email_token from apps.course.models import Course -from apps.payment.models import CoursePayment +from apps.payment.models import AuthorBalance, CoursePayment from apps.notification.utils import send_email -from .forms import UserEditForm +from .forms import UserEditForm, WithdrawalForm User = get_user_model() @@ -31,6 +35,7 @@ def resend_email_verify(request): return redirect('user-edit-profile', request.user.id) +@method_decorator(login_required, name='dispatch') class UserView(DetailView): model = User template_name = 'user/profile.html' @@ -52,6 +57,7 @@ class UserView(DetailView): return context +@method_decorator(login_required, name='dispatch') class NotificationEditView(TemplateView): template_name = 'user/notification-settings.html' @@ -59,13 +65,40 @@ class NotificationEditView(TemplateView): return super().get(request) -class PaymentHistoryView(TemplateView): +@method_decorator(login_required, name='dispatch') +class PaymentHistoryView(FormView): template_name = 'user/payment-history.html' + form_class = WithdrawalForm def get(self, request, pk=None): return super().get(request) + def post(self, request, pk=None): + form = self.get_form() + if AuthorBalance.objects.filter(created_at__gte=now() - timedelta(days=30)).exists(): + messages.error(request, 'Запрос на вывод средств можно сделать только один раз в 30 дней.') + return self.form_invalid(form) + if form.is_valid(): + if request.user.balance < form.cleaned_data['amount']: + form.errors['amount'] = 'Сумма для вывода не может быть меньше средств на счету' + return self.form_invalid(form) + AuthorBalance.objects.create( + author=request.user, + type=AuthorBalance.OUT, + amount=form.cleaned_data['amount'], + status=AuthorBalance.PENDING, + card=form.cleaned_data['amount'], + ) + return self.form_valid(form) + else: + return self.form_invalid(form) + + def get_success_url(self): + success_url = reverse_lazy('user-edit-payments', args=[self.request.user.id]) + return success_url + +@method_decorator(login_required, name='dispatch') class UserEditView(UpdateView): model = User template_name = 'user/profile-settings.html'