diff --git a/api/v1/views.py b/api/v1/views.py index f43ca18a..967dbb91 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -1,4 +1,3 @@ -import json from datetime import datetime from decimal import Decimal @@ -6,9 +5,8 @@ from django.contrib.auth import get_user_model from django.db.models import Q from rest_framework import status, views, viewsets, generics, mixins from rest_framework.decorators import (detail_route, list_route, action, - permission_classes, authentication_classes as auth_classes) + permission_classes) from rest_framework.response import Response -from rest_framework.settings import api_settings from . import ExtendedModelViewSet, BothListFormatMixin @@ -80,7 +78,9 @@ User = get_user_model() class AuthorBalanceViewSet(ExtendedModelViewSet): queryset = AuthorBalance.objects.filter( - Q(type=1) | Q(payment__status__in=Payment.PW_PAID_STATUSES), + Q(type=AuthorBalance.OUT) + | Q(payment__payment_platform=Payment.PAYMENT_PLATFORM_PAYMENTWALL, payment__status__in=Payment.PAID_STATUSES) + | Q(payment__payment_platform=Payment.PAYMENT_PLATFORM_CLOUDPAYMENTS, payment__status=Payment.STATUS_PAID), # author__role__in=[User.AUTHOR_ROLE, User.ADMIN_ROLE, User.TEACHER_ROLE], ) serializer_class = AuthorBalanceCreateSerializer diff --git a/apps/course/management/commands/fix_access_expire.py b/apps/course/management/commands/fix_access_expire.py index 2570e8e0..77c13e44 100644 --- a/apps/course/management/commands/fix_access_expire.py +++ b/apps/course/management/commands/fix_access_expire.py @@ -18,7 +18,7 @@ class Command(BaseCommand): course.access_duration = access_duration course.save() - for payment in CoursePayment.objects.filter(status__in=CoursePayment.PW_PAID_STATUSES): + for payment in CoursePayment.objects.paid(): payment.access_expire = payment.created_at.date() + timedelta(days=payment.course.access_duration) payment.save() diff --git a/apps/course/templates/course/course.html b/apps/course/templates/course/course.html index fb9c351f..dcba1856 100644 --- a/apps/course/templates/course/course.html +++ b/apps/course/templates/course/course.html @@ -22,6 +22,11 @@ {% block ogdescription %}{{ course.short_description | striptags }}{% endblock ogdescription %} +{% block head %} + {{ block.super }} + +{% endblock head %} + {% block content %}
diff --git a/apps/course/views.py b/apps/course/views.py index 1f0a101f..77597acc 100644 --- a/apps/course/views.py +++ b/apps/course/views.py @@ -1,6 +1,5 @@ from datetime import timedelta -from paymentwall import Pingback from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required from django.db.models import Q @@ -234,13 +233,8 @@ class CourseView(DetailView): if self.request.user.is_authenticated: context['next'] = self.request.GET.get('next', None) # берем последнюю оплату курса - payments = self.object.payments.filter( - user=self.request.user, - status__in=[ - Pingback.PINGBACK_TYPE_REGULAR, - Pingback.PINGBACK_TYPE_GOODWILL, - Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, - ]) + payments = self.object.payments.paid().filter( + user=self.request.user) payment = payments.filter(access_expire__gte=now().date()).order_by('-access_expire').first() context['payment'] = payment context['access_duration'] = ((payment.access_expire - now().date()).days + 1) if payment else self.object.access_duration @@ -249,7 +243,8 @@ class CourseView(DetailView): payments.filter(access_expire__lt=now().date()).exists()) context['pending'] = self.object.payments.filter( user=self.request.user, - status=Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW, + payment_platform=CoursePayment.PAYMENT_PLATFORM_PAYMENTWALL, + status=CoursePayment.STATUS_PINGBACK_TYPE_RISK_UNDER_REVIEW, ).exists() context['only_lessons'] = self.only_lessons if self.only_lessons: @@ -328,8 +323,7 @@ class CoursesView(ListView): else: context['age_name'] = '' if self.request.user.is_authenticated: - can_buy_again_courses = list(CoursePayment.objects.filter(user=self.request.user, - status__in=CoursePayment.PW_PAID_STATUSES, + can_buy_again_courses = list(CoursePayment.objects.paid().filter(user=self.request.user, access_expire__lte=now().date() + timedelta(7)).values_list('course_id', flat=True)) for course in context['course_items']: if course.id in can_buy_again_courses: @@ -350,13 +344,8 @@ class LessonView(DetailView): def get(self, request, *args, **kwargs): response = super().get(request, *args, **kwargs) - paid = request.user.is_authenticated and self.object.course.payments.filter( + paid = request.user.is_authenticated and self.object.course.payments.paid().filter( user=self.request.user, - status__in=[ - Pingback.PINGBACK_TYPE_REGULAR, - Pingback.PINGBACK_TYPE_GOODWILL, - Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, - ], access_expire__gte=now().date(), ).exists() # если это не автор или админ diff --git a/apps/notification/tasks.py b/apps/notification/tasks.py index 243be211..c41f16be 100644 --- a/apps/notification/tasks.py +++ b/apps/notification/tasks.py @@ -84,8 +84,7 @@ def sendgrid_update_recipients(): for user in users: last_course_purchase = course_payments.get(user.id) and course_payments.get(user.id).strftime(date_format) last_school_purchase = school_payments.get(user.id) and school_payments.get(user.id).strftime(date_format) - courses_purchased = CoursePayment.objects.filter(user=user, - status__in=CoursePayment.PW_PAID_STATUSES).values_list('course_id', flat=True) + courses_purchased = CoursePayment.objects.paid().filter(user=user,).values_list('course_id', flat=True) data.append({ 'first_name': user.first_name, 'last_name': user.last_name, diff --git a/apps/payment/management/commands/roistat_set_statuses.py b/apps/payment/management/commands/roistat_set_statuses.py index f3ac3e11..5990c921 100644 --- a/apps/payment/management/commands/roistat_set_statuses.py +++ b/apps/payment/management/commands/roistat_set_statuses.py @@ -1,10 +1,10 @@ import requests -from paymentwall import Pingback - from django.conf import settings from django.core.management.base import BaseCommand, CommandError +from apps.payment.models import Payment + class Command(BaseCommand): help = 'Update statuses on Roistat' @@ -12,32 +12,32 @@ class Command(BaseCommand): def handle(self, *args, **options): body = [ { - 'id': str(Pingback.PINGBACK_TYPE_REGULAR), + 'id': str(Payment.STATUS_PAID), 'name': 'PINGBACK_TYPE_REGULAR', 'type': 'paid', }, { - 'id': str(Pingback.PINGBACK_TYPE_GOODWILL), + 'id': str(Payment.STATUS_PINGBACK_TYPE_GOODWILL), 'name': 'PINGBACK_TYPE_GOODWILL', 'type': 'paid', }, { - 'id': str(Pingback.PINGBACK_TYPE_NEGATIVE), + 'id': str(Payment.STATUS_PINGBACK_TYPE_NEGATIVE), 'name': 'PINGBACK_TYPE_NEGATIVE', 'type': 'canceled', }, { - 'id': str(Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW), + 'id': str(Payment.STATUS_PINGBACK_TYPE_RISK_UNDER_REVIEW), 'name': 'PINGBACK_TYPE_RISK_UNDER_REVIEW', 'type': 'progress', }, { - 'id': str(Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED), + 'id': str(Payment.STATUS_PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED), 'name': 'PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED', 'type': 'paid', }, { - 'id': str(Pingback.PINGBACK_TYPE_RISK_REVIEWED_DECLINED), + 'id': str(Payment.STATUS_PINGBACK_TYPE_RISK_REVIEWED_DECLINED), 'name': 'PINGBACK_TYPE_RISK_REVIEWED_DECLINED', 'type': 'canceled', }, diff --git a/apps/payment/management/commands/send_transactions_to_roistat.py b/apps/payment/management/commands/send_transactions_to_roistat.py index f5569984..a2a93105 100644 --- a/apps/payment/management/commands/send_transactions_to_roistat.py +++ b/apps/payment/management/commands/send_transactions_to_roistat.py @@ -1,6 +1,4 @@ -from django.core.management.base import BaseCommand, CommandError -from django.utils.timezone import now -from paymentwall.pingback import Pingback +from django.core.management.base import BaseCommand from apps.payment.tasks import transaction_to_roistat from apps.payment.models import Payment @@ -22,7 +20,7 @@ class Command(BaseCommand): start_id = options.get('start_id') print('start_id=' + str(start_id)) - payments = Payment.objects.filter(id__gte=start_id, status__in=Payment.PW_PAID_STATUSES) + payments = Payment.objects.paid().filter(id__gte=start_id) for payment in payments: print('TRANSACTION: ' + str(payment.id)) diff --git a/apps/payment/migrations/0033_payment_payment_platform.py b/apps/payment/migrations/0033_payment_payment_platform.py new file mode 100644 index 00000000..e34e1300 --- /dev/null +++ b/apps/payment/migrations/0033_payment_payment_platform.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.7 on 2019-02-22 02:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('payment', '0032_auto_20190207_1233'), + ] + + operations = [ + migrations.AddField( + model_name='payment', + name='payment_platform', + field=models.PositiveSmallIntegerField(choices=[(1, 'PaymentWall'), (2, 'Cloudpayments')], null=True), + ), + ] diff --git a/apps/payment/migrations/0034_fill_payment_platform.py b/apps/payment/migrations/0034_fill_payment_platform.py new file mode 100644 index 00000000..b9b34c8f --- /dev/null +++ b/apps/payment/migrations/0034_fill_payment_platform.py @@ -0,0 +1,21 @@ +# Generated by Django 2.0.7 on 2019-02-22 02:14 + +from django.db import migrations + + +def forwards(apps, schema_editor): + Payment = apps.get_model('payment', 'Payment') + for payment in Payment.objects.filter(payment_platform__isnull=True): + payment.payment_platform = 1 # PAYMENT_PLATFORM_PAYMENTWALL + payment.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('payment', '0033_payment_payment_platform'), + ] + + operations = [ + migrations.RunPython(forwards), + ] diff --git a/apps/payment/migrations/0035_auto_20190222_0233.py b/apps/payment/migrations/0035_auto_20190222_0233.py new file mode 100644 index 00000000..fd3273fe --- /dev/null +++ b/apps/payment/migrations/0035_auto_20190222_0233.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.7 on 2019-02-22 02:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('payment', '0034_fill_payment_platform'), + ] + + operations = [ + migrations.AlterField( + model_name='payment', + name='payment_platform', + field=models.PositiveSmallIntegerField(choices=[(1, 'PaymentWall'), (2, 'Cloudpayments')], default=2), + ), + ] diff --git a/apps/payment/models.py b/apps/payment/models.py index 4e157d4a..fb064c9c 100644 --- a/apps/payment/models.py +++ b/apps/payment/models.py @@ -1,9 +1,7 @@ -from decimal import Decimal import arrow import short_url -from django.db.models import Func, F +from django.db.models import Func, F, Q -from paymentwall import Pingback from polymorphic.models import PolymorphicModel from polymorphic.managers import PolymorphicManager @@ -14,7 +12,6 @@ from django.core.validators import RegexValidator from django.utils.timezone import now from django.conf import settings -from apps.content.models import ImageObject from project.utils import weekdays_in_date_range from apps.course.models import Course @@ -85,24 +82,46 @@ class PaymentManger(PolymorphicManager): def all(self): return self.filter(status__isnull=False) + def paid(self): + return self.filter( + Q(payment_platform=Payment.PAYMENT_PLATFORM_PAYMENTWALL, status__in=Payment.PAID_STATUSES) + | Q(payment_platform=Payment.PAYMENT_PLATFORM_CLOUDPAYMENTS, status=Payment.STATUS_PAID)) + class Payment(PolymorphicModel): - PW_PAID_STATUSES = [ - Pingback.PINGBACK_TYPE_REGULAR, - Pingback.PINGBACK_TYPE_GOODWILL, - Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, + STATUS_PAID = 0 + STATUS_PINGBACK_TYPE_GOODWILL = 1 + STATUS_PINGBACK_TYPE_NEGATIVE = 2 + STATUS_PINGBACK_TYPE_RISK_UNDER_REVIEW = 200 + STATUS_PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED = 201 + STATUS_PINGBACK_TYPE_RISK_REVIEWED_DECLINED = 202 + STATUS_PINGBACK_TYPE_RISK_AUTHORIZATION_VOIDED = 203 + STATUS_PINGBACK_TYPE_SUBSCRIPTION_CANCELLATION = 12 + STATUS_PINGBACK_TYPE_SUBSCRIPTION_EXPIRED = 13 + STATUS_PINGBACK_TYPE_SUBSCRIPTION_PAYMENT_FAILED = 14 + + PAID_STATUSES = [ + STATUS_PAID, + STATUS_PINGBACK_TYPE_GOODWILL, + STATUS_PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, ] 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',), + (STATUS_PAID, 'regular',), + (STATUS_PINGBACK_TYPE_GOODWILL, 'goodwill',), + (STATUS_PINGBACK_TYPE_NEGATIVE, 'negative',), + (STATUS_PINGBACK_TYPE_RISK_UNDER_REVIEW, 'risk under review',), + (STATUS_PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, 'risk reviewed accepted',), + (STATUS_PINGBACK_TYPE_RISK_REVIEWED_DECLINED, 'risk reviewed declined',), + (STATUS_PINGBACK_TYPE_RISK_AUTHORIZATION_VOIDED, 'risk authorization voided',), + (STATUS_PINGBACK_TYPE_SUBSCRIPTION_CANCELLATION, 'subscription cancelation',), + (STATUS_PINGBACK_TYPE_SUBSCRIPTION_EXPIRED, 'subscription expired',), + (STATUS_PINGBACK_TYPE_SUBSCRIPTION_PAYMENT_FAILED, 'subscription payment failed',), + ) + PAYMENT_PLATFORM_PAYMENTWALL = 1 + PAYMENT_PLATFORM_CLOUDPAYMENTS = 2 + PAYMENT_PLATFORM_CHOICES = ( + (PAYMENT_PLATFORM_PAYMENTWALL, 'PaymentWall'), + (PAYMENT_PLATFORM_CLOUDPAYMENTS, 'Cloudpayments'), ) 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) @@ -112,6 +131,7 @@ class Payment(PolymorphicModel): created_at = models.DateTimeField(auto_now_add=True) update_at = models.DateTimeField(auto_now=True) bonus = models.ForeignKey('payment.UserBonus', null=True, on_delete=models.SET_NULL, related_name='purchase_payments') + payment_platform = models.PositiveSmallIntegerField(choices=PAYMENT_PLATFORM_CHOICES, default=PAYMENT_PLATFORM_CLOUDPAYMENTS) objects = PaymentManger() @@ -152,15 +172,14 @@ class Payment(PolymorphicModel): elif isinstance(payment, GiftCertificatePayment): price = payment.gift_certificate.price elif course: - paid_before = CoursePayment.objects.filter(user=user, course=course, status__in=Payment.PW_PAID_STATUSES).exists() + paid_before = CoursePayment.objects.paid().filter(user=user, course=course).exists() price = course.price / 2 if paid_before else course.price else: if user: - school_payments = SchoolPayment.objects.filter( + school_payments = SchoolPayment.objects.paid().filter( user=user, date_start__lte=date_start, date_end__gte=date_start, - status__in=Payment.PW_PAID_STATUSES, ) school_schedules_purchased = school_payments.annotate( joined_weekdays=Func(F('weekdays'), function='unnest', ) @@ -206,26 +225,26 @@ class Payment(PolymorphicModel): # TODO change to property def is_paid(self): - return self.status in self.PW_PAID_STATUSES + if self.payment_platform == Payment.PAYMENT_PLATFORM_PAYMENTWALL: + return self.status in self.PAID_STATUSES + if self.payment_platform == Payment.PAYMENT_PLATFORM_CLOUDPAYMENTS: + return self.status == Payment.STATUS_PAID # TODO? delete ? change to property def is_deliverable(self): - return self.status in [ - Pingback.PINGBACK_TYPE_REGULAR, - Pingback.PINGBACK_TYPE_GOODWILL, - Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, - ] + return self.is_paid() # TODO change to property def is_under_review(self): - return self.status == Pingback.PINGBACK_TYPE_RISK_UNDER_REVIEW + return (self.payment_platform == Payment.PAYMENT_PLATFORM_PAYMENTWALL + and self.status == self.STATUS_PINGBACK_TYPE_RISK_UNDER_REVIEW) # TODO change to property def is_cancelable(self): - return self.status in [ - Pingback.PINGBACK_TYPE_NEGATIVE, - Pingback.PINGBACK_TYPE_RISK_REVIEWED_DECLINED, - ] + return (self.payment_platform == Payment.PAYMENT_PLATFORM_PAYMENTWALL and self.status in [ + self.STATUS_PINGBACK_TYPE_NEGATIVE, + self.STATUS_PINGBACK_TYPE_RISK_REVIEWED_DECLINED, + ]) def save(self, *args, **kwargs): amount_data = Payment.calc_amount(payment=self) @@ -284,6 +303,10 @@ class SchoolPayment(Payment): ]) return days + @property + def weekdays_str(self): + return ', '.join([SchoolSchedule.WEEKDAY_SHORT_NAMES[wd - 1] for wd in self.weekdays]) + @property def date_end_humanize(self): return arrow.get(self.date_end, settings.TIME_ZONE).humanize(locale='ru') diff --git a/apps/payment/templates/payment/cloud_payments_test.html b/apps/payment/templates/payment/cloud_payments_test.html deleted file mode 100644 index c26a2345..00000000 --- a/apps/payment/templates/payment/cloud_payments_test.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "templates/lilcity/index.html" %} {% load static %} - -{% block head %} - -{% endblock head %} - -{% block content %} - -{% endblock content %} - -{% block foot_js %} - -{% endblock foot_js %} diff --git a/apps/payment/templates/payment/cloudpayments_widget.html b/apps/payment/templates/payment/cloudpayments_widget.html new file mode 100644 index 00000000..3142316d --- /dev/null +++ b/apps/payment/templates/payment/cloudpayments_widget.html @@ -0,0 +1,55 @@ +{% extends "templates/lilcity/index.html" %} {% load static settings %} + +{% block head %} + +{% endblock head %} + +{% block content %} + +{% endblock content %} + +{% block foot_js %} + +{% endblock foot_js %} diff --git a/apps/payment/views.py b/apps/payment/views.py index 50a7e4de..afa65d80 100644 --- a/apps/payment/views.py +++ b/apps/payment/views.py @@ -12,11 +12,11 @@ import random from cloudpayments import CloudPayments from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.http import HttpResponse, Http404 +from django.http import HttpResponse, Http404, JsonResponse from django.shortcuts import redirect, get_object_or_404 from django.views.generic import View, TemplateView, DetailView from django.views.decorators.csrf import csrf_exempt -from django.urls import reverse_lazy +from django.urls import reverse_lazy, reverse from django.utils.decorators import method_decorator from django.utils.timezone import now from django.conf import settings @@ -56,9 +56,10 @@ class SchoolBuySuccessView(TemplateView): @method_decorator(login_required, name='dispatch') class CourseBuyView(TemplateView): - template_name = 'payment/paymentwall_widget.html' + template_name = 'payment/cloudpayments_widget.html' def get(self, request, pk=None, *args, **kwargs): + #TODO: cloudpayments use_bonuses = request.GET.get('use_bonuses') host = urlsplit(self.request.META.get('HTTP_REFERER')) host = str(host[0]) + '://' + str(host[1]) @@ -67,8 +68,7 @@ class CourseBuyView(TemplateView): if request.user == course.author: messages.error(request, 'Вы не можете приобрести свой курс.') return redirect(reverse_lazy('course', args=[course.id])) - prev_payment = CoursePayment.objects.filter(user=request.user, course=course, - status__in=Payment.PW_PAID_STATUSES).order_by('-access_expire').first() + prev_payment = CoursePayment.objects.paid().filter(user=request.user, course=course).order_by('-access_expire').first() access_duration = course.access_duration or 90 access_expire = prev_payment.access_expire + timedelta(days=access_duration) if prev_payment \ else now().date() + timedelta(days=access_duration - 1) @@ -77,12 +77,13 @@ class CourseBuyView(TemplateView): course=course, access_expire=access_expire, roistat_visit=roistat_visit, + payment_platform=CoursePayment.PAYMENT_PLATFORM_CLOUDPAYMENTS, ) if use_bonuses: if request.user.bonus >= course_payment.amount: bonus = UserBonus.objects.create(amount= -course_payment.amount, user=request.user, payment=course_payment) course_payment.amount = 0 - course_payment.status = Pingback.PINGBACK_TYPE_REGULAR + course_payment.status = Payment.STATUS_PAID else: bonus = UserBonus.objects.create(amount= -request.user.bonus, user=request.user, payment=course_payment) @@ -91,31 +92,15 @@ class CourseBuyView(TemplateView): course_payment.save() if course_payment.is_paid(): return redirect(reverse_lazy('course_payment_success', args=[course.id])) - product = Product( - f'course_{course_payment.id}', - course_payment.amount, - 'RUB', - f'Курс "{course.title}"', - ) - widget = Widget( - str(request.user.id), - 'p1_1', - [product], - extra_params={ - 'lang': 'ru', - 'evaluation': 1, - 'demo': 1, - 'test_mode': 1, - 'success_url': host + str(reverse_lazy('course_payment_success', args=[course.id])), - 'failure_url': host + str(reverse_lazy('payment-error')), - } - ) - return self.render_to_response(context={'widget': widget.get_html_code()}) + context = { + 'payment': course_payment, + } + return self.render_to_response(context=context) @method_decorator(login_required, name='dispatch') class SchoolBuyView(TemplateView): - template_name = 'payment/paymentwall_widget.html' + template_name = 'payment/cloudpayments_widget.html' def get(self, request, *args, **kwargs): host = urlsplit(self.request.META.get('HTTP_REFERER')) @@ -126,23 +111,26 @@ class SchoolBuyView(TemplateView): date_start = request.GET.get('date_start') date_start = date_start and datetime.datetime.strptime(date_start, '%Y-%m-%d') or now().date() if not weekdays: - messages.error(request, 'Выберите несколько дней недели.') + messages.error(request, 'Выберите несколько дней недели') + if request.is_ajax(): + return JsonResponse({ + 'error': 'Выберите несколько дней недели' + }) return redirect('school:school') try: weekdays = [int(weekday) for weekday in weekdays] except ValueError: - messages.error(request, 'Ошибка выбора дней недели.') + messages.error(request, 'Ошибка выбора дней недели') + if request.is_ajax(): + return JsonResponse({ + 'error': 'Ошибка выбора дней недели' + }) return redirect('school:school') - prev_school_payment = SchoolPayment.objects.filter( + prev_school_payment = SchoolPayment.objects.paid().filter( user=request.user, date_start__lte=date_start, date_end__gte=date_start, add_days=False, - status__in=[ - Pingback.PINGBACK_TYPE_REGULAR, - Pingback.PINGBACK_TYPE_GOODWILL, - Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, - ], ).last() add_days = bool(prev_school_payment) if add_days: @@ -157,6 +145,10 @@ class SchoolBuyView(TemplateView): # Если произойдет ошибка и оплату бонусами повторят еще раз на те же дни, то вернет ошибку if school_payment.amount <= 0: messages.error(request, 'Выбранные дни отсутствуют в оставшемся периоде подписки') + if request.is_ajax(): + return JsonResponse({ + 'error': 'Выбранные дни отсутствуют в оставшемся периоде подписки' + }) return redirect(reverse_lazy('school:school')) else: school_payment = SchoolPayment.objects.create( @@ -170,7 +162,7 @@ class SchoolBuyView(TemplateView): if request.user.bonus >= school_payment.amount: bonus = UserBonus.objects.create(amount= -school_payment.amount, user=request.user, payment=school_payment) school_payment.amount = 0 - school_payment.status = Pingback.PINGBACK_TYPE_REGULAR + school_payment.status = Payment.STATUS_PAID else: bonus = UserBonus.objects.create(amount= -request.user.bonus, user=request.user, payment=school_payment) @@ -178,27 +170,22 @@ class SchoolBuyView(TemplateView): school_payment.bonus = bonus school_payment.save() if school_payment.is_paid(): + if request.is_ajax(): + return JsonResponse({ + 'status': 'ok', + 'paid': True, + }) return redirect(reverse_lazy('payment-success')) - product = Product( - f'school_{school_payment.id}', - school_payment.amount, - 'RUB', - 'Школа', - ) - widget = Widget( - str(request.user.id), - 'p1_1', - [product], - extra_params={ - 'lang': 'ru', - 'evaluation': 1, - 'demo': 1, - 'test_mode': 1, - 'success_url': host + str(reverse_lazy('payment-success')), - 'failure_url': host + str(reverse_lazy('payment-error')), - } - ) - return self.render_to_response(context={'widget': widget.get_html_code()}) + + if request.is_ajax(): + return JsonResponse({ + 'status': 'ok', + 'amount': school_payment.amount, + 'payment_id': school_payment.id, + 'success_url': reverse('payment-success', args=[school_payment.id]), + 'description': f'Оплата школы до {school_payment.date_end_humanize}, {school_payment.weekdays_str}', + }) + return self.render_to_response(context={'payment': school_payment}) @method_decorator(csrf_exempt, name='dispatch') @@ -407,19 +394,80 @@ class CloudPaymentsTestView(TemplateView): return self.render_to_response(context) -class CloudPaymentsCallbackTestView(TemplateView): - template_name = 'payment/cloud_payments_callback_test.html' +class PaymentSuccessView(View): - def get(self, request, *args, **kwargs): - context = self.get_context_data(**kwargs) - invoice_id = request.GET.get('invoice_id') - client = CloudPayments('pk_9ae020cd5fed74499dee247067d17', 'a7d5f88ea69ef59f8b5db2252e39cedf') - context['test_ok'] = bool(client.test()) + def post(self, request, payment_id): + payment_data = request.POST.get('payment_data') try: - t = client.find_payment(int(invoice_id)) - print('transaction.status', t.status, 'transaction.reason', t.reason) - # status = 'Completed', reason = 'Approved' - context['transaction'] = t - except: - context['transaction'] = None - return self.render_to_response(context) + payment = Payment.objects.get(id=payment_id) + except Payment.DoesNotExist: + raise Http404() + client = CloudPayments(settings.CLOUD_PAYMENTS_PUBLIC_ID, settings.CLOUD_PAYMENTS_API_KRY) + transaction = client.find_payment(payment_id) + if transaction.status == 'Completed' and transaction.reason == 'Approved': + payment.data = payment_data + payment.status = Payment.STATUS_PAID + payment.save() + + author_balance = getattr(payment, 'author_balance', None) + if author_balance and author_balance.type == AuthorBalance.IN: + payment.author_balance.status = AuthorBalance.DECLINED + payment.author_balance.save() + + if isinstance(payment, CoursePayment): + product_type_name = 'course' + properties = { + 'payment_id': payment.id, + 'amount': payment.amount, + 'status': payment.status, + 'course': payment.course.id, + 'created_at': payment.created_at, + 'update_at': payment.update_at, + } + redirect_to = reverse('course_payment_success', args=[payment.course_id]) + elif isinstance(payment, SchoolPayment): + product_type_name = 'school' + properties = { + 'payment_id': payment.id, + 'amount': payment.amount, + 'status': payment.status, + 'weekdays': payment.weekdays, + 'add_days': payment.add_days, + 'date_start': payment.date_start, + 'date_end': payment.date_end, + 'created_at': payment.created_at, + 'update_at': payment.update_at, + } + redirect_to = reverse('payment-success') + elif isinstance(payment, GiftCertificatePayment): + product_type_name = 'gift_certificate' + properties = { + 'payment_id': payment.id, + 'amount': payment.amount, + 'status': payment.status, + 'gift_certificate': payment.gift_certificate.id, + 'created_at': payment.created_at, + 'update_at': payment.update_at, + } + redirect_to = reverse('gift-certificate-payment-success', args=[payment_id]) + + product_payment_to_mixpanel.delay( + payment.user.id, + f'{product_type_name.title()} payment', + now().strftime('%Y-%m-%dT%H:%M:%S'), + properties, + ) + + transaction_to_roistat.delay( + payment.user.id, + payment.id, + f'{product_type_name.title()} payment', + payment.amount, + now().strftime('%Y-%m-%d %H:%M:%S'), + payment.STATUS_PAID, + product_type_name, + payment.roistat_visit, + ) + + return redirect_to + return reverse('payment-error') diff --git a/apps/school/templates/school/summer_school.html b/apps/school/templates/school/summer_school.html index e0e05514..d5a5457b 100644 --- a/apps/school/templates/school/summer_school.html +++ b/apps/school/templates/school/summer_school.html @@ -1,6 +1,10 @@ {% extends "templates/lilcity/index.html" %} {% load static %} {% block title %}Онлайн-школа LilCity{% endblock title%} {% block ogimage %}http://{{request.META.HTTP_HOST}}{% static 'img/og_main.jpg' %}{% endblock %} +{% block head %} + {{ block.super }} + +{% endblock head %} {% block content %} {% if not is_purchased and not prev_school_payments_exists %} {% include "../summer/promo.html" %} diff --git a/apps/school/views.py b/apps/school/views.py index 3dc0f01a..4317614c 100644 --- a/apps/school/views.py +++ b/apps/school/views.py @@ -1,5 +1,4 @@ from datetime import datetime, timedelta, date -from paymentwall import Pingback from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required, user_passes_test @@ -67,15 +66,10 @@ class LiveLessonsDetailView(DetailView): if lesson_date: self.object = LiveLesson.objects.get(date=datetime.strptime(lesson_date, '%d-%m-%y')) if request.user.is_authenticated: - is_purchased = SchoolPayment.objects.filter( + is_purchased = SchoolPayment.objects.paid().filter( user=request.user, date_start__lte=now(), date_end__gte=now() - timedelta(days=7), - status__in=[ - Pingback.PINGBACK_TYPE_REGULAR, - Pingback.PINGBACK_TYPE_GOODWILL, - Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, - ], weekdays__contains=[self.object.date.weekday() + 1], ).exists() if not is_purchased and request.user.role not in [User.ADMIN_ROLE, User.TEACHER_ROLE]: @@ -142,17 +136,15 @@ class SchoolView(TemplateView): prev_school_payments = None prev_range = [date_now - timedelta(days=7), date_now - timedelta(days=1)] if self.request.user.is_authenticated: - school_payment = SchoolPayment.objects.filter( + school_payment = SchoolPayment.objects.paid().filter( user=self.request.user, - status__in=SchoolPayment.PW_PAID_STATUSES, date_start__lte=date_now, date_end__gte=date_now ) school_payment_exists = school_payment.exists() - school_purchased_future = SchoolPayment.objects.filter( + school_purchased_future = SchoolPayment.objects.paid().filter( user=self.request.user, - status__in=SchoolPayment.PW_PAID_STATUSES, date_start__gt=date_now, date_end__gt=date_now ) @@ -167,15 +159,10 @@ class SchoolView(TemplateView): joined_weekdays=Func(F('weekdays'), function='unnest',) ).values_list('joined_weekdays', flat=True).distinct() - prev_school_payments = SchoolPayment.objects.filter( + prev_school_payments = SchoolPayment.objects.paid().filter( date_start__lte=prev_range[1], date_end__gte=prev_range[0], user=self.request.user, - status__in=[ - Pingback.PINGBACK_TYPE_REGULAR, - Pingback.PINGBACK_TYPE_GOODWILL, - Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, - ], weekdays__len__gt=0, ) diff --git a/apps/user/models.py b/apps/user/models.py index 5713e6db..37da804e 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -110,13 +110,12 @@ class User(AbstractUser): def balance(self): from apps.payment.models import Payment, AuthorBalance income = self.balances.filter( - type=AuthorBalance.IN, - payment__isnull=False, - payment__status__in=Payment.PW_PAID_STATUSES, - ).aggregate( - models.Sum('amount'), - models.Sum('commission'), - ) + Q(payment__payment_platform=Payment.PAYMENT_PLATFORM_PAYMENTWALL, payment__status__in=Payment.PAID_STATUSES) + | Q(payment__payment_platform=Payment.PAYMENT_PLATFORM_CLOUDPAYMENTS, payment__status=Payment.STATUS_PAID), + type=AuthorBalance.IN,).aggregate( + models.Sum('amount'), + models.Sum('commission'), + ) income_amount = income.get('amount__sum') or 0 income_commission = income.get('commission__sum') or 0 @@ -129,7 +128,9 @@ class User(AbstractUser): def bonus(self): from apps.payment.models import Payment return int(self.bonuses.filter( - Q(payment__isnull=False, payment__status__in=Payment.PW_PAID_STATUSES) | Q(is_service=True), + Q(payment__payment_platform=Payment.PAYMENT_PLATFORM_PAYMENTWALL, payment__status__in=Payment.PAID_STATUSES) + | Q(payment__payment_platform=Payment.PAYMENT_PLATFORM_CLOUDPAYMENTS, payment__status=Payment.STATUS_PAID) + | Q(is_service=True), ).aggregate(models.Sum('amount')).get('amount__sum') or 0) diff --git a/apps/user/views.py b/apps/user/views.py index 1c244b01..9177f39c 100644 --- a/apps/user/views.py +++ b/apps/user/views.py @@ -3,7 +3,6 @@ from PIL import Image from uuid import uuid4 from os.path import splitext from datetime import timedelta -from paymentwall import Pingback import short_url from django.conf import settings @@ -61,23 +60,13 @@ class ProfileView(TemplateView): context['is_author'] = context['published'] or self.request.user.role == User.AUTHOR_ROLE context['user_gift_certificates'] = UserGiftCertificate.objects.filter(user=self.request.user) context['paid'] = Course.objects.filter( - payments__in=CoursePayment.objects.filter( + payments__in=CoursePayment.objects.paid().filter( user=self.object, - status__in=[ - Pingback.PINGBACK_TYPE_REGULAR, - Pingback.PINGBACK_TYPE_GOODWILL, - Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, - ], ), ).distinct() - school_payment = SchoolPayment.objects.filter( + school_payment = SchoolPayment.objects.paid().filter( user=self.object, date_end__gte=now(), - status__in=[ - Pingback.PINGBACK_TYPE_REGULAR, - Pingback.PINGBACK_TYPE_GOODWILL, - Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, - ], ) context['is_school_purchased'] = school_payment.exists() if context['is_school_purchased']: @@ -303,7 +292,9 @@ class BonusHistoryView(TemplateView): config = Config.load() context = self.get_context_data(**kwargs) context['bonuses'] = request.user.bonuses.filter( - Q(payment__isnull=False, payment__status__in=Payment.PW_PAID_STATUSES) | Q(is_service=True), + Q(payment__payment_platform=Payment.PAYMENT_PLATFORM_PAYMENTWALL, payment__status__in=Payment.PAID_STATUSES) + | Q(payment__payment_platform=Payment.PAYMENT_PLATFORM_CLOUDPAYMENTS, payment__status=Payment.STATUS_PAID) + | Q(is_service=True) ) context['referrer_url'] = 'https://%s%s?referrer=%s' % ( settings.MAIN_HOST, reverse('index'), short_url.encode_url(request.user.id) diff --git a/project/context_processors.py b/project/context_processors.py index 6e954da7..6b966957 100644 --- a/project/context_processors.py +++ b/project/context_processors.py @@ -1,6 +1,5 @@ from django.db.models import Func, F from django.utils.timezone import now -from paymentwall.pingback import Pingback from django.contrib.auth import get_user_model from django.conf import settings from apps.config.models import Config @@ -21,13 +20,8 @@ def baner(request): def school_purchased(request): if request.user.is_authenticated: n = now().date() - school_payment = SchoolPayment.objects.filter( + school_payment = SchoolPayment.objects.paid().filter( user=request.user, - status__in=[ - Pingback.PINGBACK_TYPE_REGULAR, - Pingback.PINGBACK_TYPE_GOODWILL, - Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, - ], date_start__lte=n, date_end__gte=n ) diff --git a/project/settings.py b/project/settings.py index 9f835203..f635b6eb 100644 --- a/project/settings.py +++ b/project/settings.py @@ -264,6 +264,9 @@ CELERY_BEAT_SCHEDULE = { }, } +CLOUD_PAYMENTS_PUBLIC_ID = os.getenv('CLOUD_PAYMENTS_PUBLIC_ID', 'pk_9ae020cd5fed74499dee247067d17') +CLOUD_PAYMENTS_API_KRY = os.getenv('CLOUD_PAYMENTS_API_KRY', 'a7d5f88ea69ef59f8b5db2252e39cedf') + try: from paymentwall import * except ImportError: diff --git a/project/urls.py b/project/urls.py index c29a1803..4054af90 100644 --- a/project/urls.py +++ b/project/urls.py @@ -35,7 +35,7 @@ from apps.payment.views import ( CourseBuySuccessView, CourseBuyView, PaymentwallCallbackView, SchoolBuySuccessView, SchoolBuyView, GiftCertificatesView, GiftCertificateBuyView, - GiftCertificateBuySuccessView, GiftCertificateGetView, CloudPaymentsTestView, CloudPaymentsCallbackTestView) + GiftCertificateBuySuccessView, GiftCertificateGetView, CloudPaymentsTestView, PaymentSuccessView) from .views import AboutView, IndexView, SchoolSchedulesView @@ -66,6 +66,8 @@ urlpatterns = [ path('payments/course//success', CourseBuySuccessView.as_view(), name='course_payment_success'), path('payments/school/success', SchoolBuySuccessView.as_view(), name='payment-success'), path('payments/error', TemplateView.as_view(template_name='payment/payment_error.html'), name='payment-error'), + path('payments//success', PaymentSuccessView.as_view(), name='payment-success'), + path('cloud_payments_test', CloudPaymentsTestView.as_view(), name='cloud_payments_test'), path('school/checkout', SchoolBuyView.as_view(), name='school-checkout'), path('search/', SearchView.as_view(), name='search'), path('user/profile/', ProfileView.as_view(), name='user-profile'), @@ -98,8 +100,6 @@ urlpatterns = [ name='gift-certificate-payment-success'), path('gift-certificate//get', GiftCertificateGetView.as_view(), name='gift-certificate-get'), path('faq', FAQView.as_view(), name='faq'), - path('cloud_payments_callback_test', CloudPaymentsCallbackTestView.as_view(), name='cloud_payments_callback_test'), - path('cloud_payments_test', CloudPaymentsTestView.as_view(), name='cloud_payments_test'), ] diff --git a/project/views.py b/project/views.py index 7404e317..4b8dcaee 100644 --- a/project/views.py +++ b/project/views.py @@ -6,7 +6,6 @@ from django.db.models import Min, Func, F from django.contrib.auth import get_user_model from django.views.generic import TemplateView from django.utils.timezone import now -from paymentwall.pingback import Pingback from apps.course.models import Course from apps.school.models import SchoolSchedule @@ -50,15 +49,10 @@ class IndexView(TemplateView): ) date_now = now_time.date() if self.request.user.is_authenticated: - school_payment = SchoolPayment.objects.filter( + school_payment = SchoolPayment.objects.paid().filter( user=self.request.user, date_start__lte=date_now, date_end__gte=date_now, - status__in=[ - Pingback.PINGBACK_TYPE_REGULAR, - Pingback.PINGBACK_TYPE_GOODWILL, - Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED, - ], ) school_payment_exists = school_payment.exists() school_schedules_purchased = school_payment.annotate( @@ -105,7 +99,7 @@ class IndexView(TemplateView): 'school_schedules': SchoolSchedule.objects.all(), 'school_schedules_purchased': set(school_schedules_purchased), 'teachers': User.objects.filter(role=User.TEACHER_ROLE, show_in_mainpage=True), - 'works_count': Payment.objects.filter(status__in=Payment.PW_PAID_STATUSES).count() * 5, + 'works_count': Payment.objects.paid().count() * 5, 'subscription_ends': school_payment.filter(add_days=False).first().date_end if school_payment_exists else None, 'subscription_ends_humanize': school_payment.filter(add_days=False).first().date_end_humanize if school_payment_exists else None, diff --git a/web/src/js/modules/popup.js b/web/src/js/modules/popup.js index 8d3079ba..6a6b461e 100644 --- a/web/src/js/modules/popup.js +++ b/web/src/js/modules/popup.js @@ -215,6 +215,7 @@ $(document).ready(function () { } if(data === '.js-popup-course-buy'){ + //TODO: cloudpayments const updateCourseCart = () => { const $orderPrice = popup.find('.order_price_text'); const useBonuses = $bonusesCheckbox.prop('checked');