diff --git a/access/views.py b/access/views.py index c3e69dd..8cb6305 100644 --- a/access/views.py +++ b/access/views.py @@ -128,6 +128,7 @@ class FindUserView(APIView): class DetailUserView(APIView): renderer_classes = (JSONRenderer,) + permission_classes = (permissions.IsAuthenticated,) @staticmethod def post(request, out_key=None): diff --git a/courses/models.py b/courses/models.py index ce50bf7..eacc7e6 100755 --- a/courses/models.py +++ b/courses/models.py @@ -72,7 +72,7 @@ class Topic(models.Model): class CourseManager(models.Manager): - def update_or_create_course(self, image=None, big_image=None, statistic=None, + def update_or_create_course(self, image=None, big_image=None, statistic=None, old_slug=None, big_mobile_image=None, slug=None, teacher_tokens=None, level=None, direction=None, **kwargs): @@ -102,10 +102,11 @@ class CourseManager(models.Manager): kwargs['direction'] = get_real_name(COURSE_DIRECTION, direction[0]) try: - course = self.get(slug=slug) + course = self.get(slug=old_slug) for i in kwargs: if kwargs[i]: setattr(course, i, kwargs[i]) + course.slug = slug course.save() except ObjectDoesNotExist: diff --git a/finance/migrations/0010_auto_20180412_1628.py b/finance/migrations/0010_auto_20180412_1628.py new file mode 100644 index 0000000..ef554ce --- /dev/null +++ b/finance/migrations/0010_auto_20180412_1628.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-04-12 16:28 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('finance', '0009_invoicerebilling_pay_count'), + ] + + operations = [ + migrations.RemoveField( + model_name='invoicerebilling', + name='pay_count', + ), + migrations.AddField( + model_name='bill', + name='freeze', + field=models.BooleanField(default=False, verbose_name='Отказ от платежей'), + ), + migrations.AddField( + model_name='invoice', + name='date_of_payment', + field=models.DateTimeField(blank=True, null=True, verbose_name='Дата фактической оплаты'), + ), + migrations.AddField( + model_name='invoice', + name='expected_date', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Ожидаемая дата платежа'), + ), + migrations.AlterField( + model_name='invoice', + name='date', + field=models.DateTimeField(auto_now_add=True, verbose_name='Дата создания платежа'), + ), + ] diff --git a/finance/models.py b/finance/models.py index 434e562..f2d50b9 100755 --- a/finance/models.py +++ b/finance/models.py @@ -1,8 +1,17 @@ # coding=utf-8 +from dateutil.relativedelta import relativedelta + from django.conf import settings from django.core.mail import EmailMessage from django.db import models +from django.utils import timezone from yandex_money.models import Payment +import logging + +from progress.models import Progress + + +logger_business_rules = logging.getLogger('business_rules') class Bill(models.Model): @@ -13,10 +22,75 @@ class Bill(models.Model): blank=True, editable=False) description = models.TextField(verbose_name='Внутренняя заметка', blank=True) date = models.DateTimeField(verbose_name="Дата выставления", auto_now_add=True) + freeze = models.BooleanField(verbose_name='Отказ от платежей', default=False) def __str__(self): return '%s: %s' % (self.id, self.user) + def freeze_course(self): + if self.invoice_set.exclude(status='F').exists(): + log = False + try: + p = Progress.objects.get(user=self.user, course_token=str(self.course_token)) + p.is_freeze = True + p.save() + except Progress.DoesNotExist: + log = True + + if log: + logger_business_rules.info('Отказ от платежей прошёл успешно', exc_info=True, extra={ + 'description': 'The privileges were not taken away, as they were not granted', + 'user': self.user, + }) + + msg = EmailMessage( + 'Вы откозались от оплаты по счёту', + """Вы откозались от оплаты по счёту. + Вы сможете продолжить оплату в личном кабинете""", + to=[self.user.email], + bcc=[self.opener.email], + reply_to=[self.opener.email], + ) + msg.send() + + self.freeze = True + self.save() + + else: + logger_business_rules.warning('Попытка нарушения правила отказа от платежей', exc_info=True, extra={ + 'description': 'All payments already paid', + 'user': self.user, + }) + + def unfreeze_course(self, force=False): + if force or self.invoice_set.exclude(status='F')\ + .filter(expected_date__lt=timezone.now() + relativedelta(days=1)).exists(): + if self.invoice_set.filter(status='F').exclude(expected_date__lt=timezone.now()).exists(): + try: + p = Progress.objects.get(user=self.user, course_token=str(self.course_token)) + p.is_freeze = False + p.save() + except Progress.DoesNotExist: + pass + + msg = EmailMessage( + 'Вы возобновили оплату по счёту', + """Вы возобновили оплату по счёту.""", + to=[self.user.email], + bcc=[self.opener.email], + reply_to=[self.opener.email], + ) + msg.send() + + self.freeze = False + self.save() + + else: + logger_business_rules.warning('Попытка нарушения правила возобновления платежей', exc_info=True, extra={ + 'description': 'Excepted date more than one day', + 'user': self.user, + }) + def get_full_price(self): return sum([i.price for i in self.invoice_set.all() if not i.price is None]) @@ -59,7 +133,9 @@ class Invoice(models.Model): blank=True, editable=False) bill = models.ForeignKey(to=Bill, verbose_name="Связный счёт") is_open = models.BooleanField(default=True, verbose_name="Открывает ли платёж курс") - date = models.DateTimeField(auto_now_add=True) + date = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания платежа") + expected_date = models.DateTimeField(default=timezone.now, verbose_name="Ожидаемая дата платежа") + date_of_payment = models.DateTimeField(verbose_name="Дата фактической оплаты", blank=True, null=True) def get_comment(self): return '''Вам выставлен счёт,''' if \ @@ -86,7 +162,19 @@ class Invoice(models.Model): class InvoiceRebilling(Invoice): rebilling_on = models.BooleanField(verbose_name='Повторять платеж', default=False, editable=False) - pay_count = models.SmallIntegerField(verbose_name='Всего платежей', editable=False) + + def create_child_pays(self, count): + for idx in range(int(count)-1): + InvoiceRebilling.objects.create( + bill=self.bill, + comment=self.comment, + method=self.method, + status='W', + is_open=self.is_open, + price=self.price, + rebilling_on=False, + expected_date=(timezone.now() + relativedelta(months=idx+1)), + ) class Meta: verbose_name = 'Повторный платёж' diff --git a/finance/tasks.py b/finance/tasks.py index a28b3ae..a034701 100644 --- a/finance/tasks.py +++ b/finance/tasks.py @@ -1,79 +1,40 @@ -import json -from datetime import datetime, timedelta - import logging import os import requests -from django_celery_beat.models import CrontabSchedule, PeriodicTask from yandex_money.models import Payment -from finance.models import Invoice +from finance.models import InvoiceRebilling from lms import celery_app from django.conf import settings - logger_yandex = logging.getLogger('yandex_money') -def setup_periodic_billing(order_number): - # TODO: настроить периодичность и срок окончания - # 12:00 первого числа каждого месяца - schedule, _ = CrontabSchedule.objects.get_or_create( - minute='0', - hour='12', - day_of_week='*', - day_of_month='1', - month_of_year='*' - ) - PeriodicTask.objects.create( - crontab=schedule, - name='Periodic billing (order_number={})'.format(order_number), - task='finance.tasks.periodic_billing', - kwargs=json.dumps({ - 'order_number': order_number - }), - expires=datetime.utcnow() + timedelta(days=180) # в течение полугода - ) - - @celery_app.task -def periodic_billing(order_number): - try: - sample = Invoice.objects.get(yandex_pay__order_number=order_number) - except Invoice.DoesNotExist: - raise ValueError('Номер заказа {} не найден'.format(order_number)) - - bill = sample.bill - - invoice = Invoice.objects.create( - status='P', - price=sample.price, - method=sample.method, - rebilling=True, - bill=bill - ) - - if invoice.method == 'Y': - user = bill.user - yandex_pay = Payment.objects.create( - invoice_id=sample.yandex_pay.invoice_id, - order_amount=invoice.price, - customer_number=user.id, - user=user, - cps_email=user.email - ) - invoice.yandex_pay = yandex_pay - invoice.save() - - repeat_card_payment(invoice) +def periodic_billing(): + logger_yandex.info("start periodic billing task") + + # for invoice in InvoiceRebilling.objects.filter(method='Y').exclude(status='F'): + # + # user = invoice.bill.user + # yandex_pay = Payment.objects.create( + # order_amount=invoice.price, + # customer_number=user.id, + # user=user, + # cps_email=user.email + # ) + # invoice.yandex_pay = yandex_pay + # invoice.save() + # + # repeat_card_payment(invoice) def repeat_card_payment(invoice): resp = requests.post(settings.YANDEX_MONEY_MWS_URL + 'repeatCardPayment', data={ 'clientOrderId': invoice.id, # уникальное возрастающее целое число - 'invoiceId': invoice.yandex_pay.invoice_id, + 'invoiceId': invoice.key, 'amount': invoice.price, 'orderNumber': invoice.yandex_pay.order_number }, diff --git a/finance/urls.py b/finance/urls.py index 7a83d74..7cb09a1 100644 --- a/finance/urls.py +++ b/finance/urls.py @@ -3,6 +3,8 @@ from finance import views urlpatterns = [ url(r'payment/([0-9]{1,99})/$', views.YandexPay.as_view()), + url(r'bill/([0-9]{1,99})/freeze/$', views.FreezeView.as_view()), + url(r'bill/([0-9]{1,99})/unfreeze/$', views.UnFreezeView.as_view()), url(r'bills/$', views.BillListView.as_view()), url(r'bills/([0-9]{1,99})/$', views.BillDetailView.as_view()), url(r'bills_find/$', views.FindBillView.as_view()), diff --git a/finance/views.py b/finance/views.py index 3a79e0d..c866150 100644 --- a/finance/views.py +++ b/finance/views.py @@ -21,7 +21,6 @@ from courses.models import Course from courses.api import CourseParamsApi from finance.models import Bill, Invoice, InvoiceRebilling from finance.serializers import BillSerializer, InvoiceSerializer -from finance.tasks import setup_periodic_billing from lms.global_decorators import transaction_decorator from lms.tools import get_real_name from django.utils import timezone @@ -39,6 +38,38 @@ def test_pay(request): }) +class FreezeView(APIView): + renderer_classes = (JSONRenderer,) + + @staticmethod + def post(request, pk): + try: + bill = Bill.objects.get(id=pk) + except Bill.DoesNotExist: + return Response("Счёт не найден", status=404) + + if request.user.is_authenticated and request.user.email == bill.user.email: + bill.freeze_course() + return Response(status=204) + return Response("Permission denied", status=403) + + +class UnFreezeView(APIView): + renderer_classes = (JSONRenderer,) + + @staticmethod + def post(request, pk): + try: + bill = Bill.objects.get(id=pk) + except Bill.DoesNotExist: + return Response("Счёт не найден", status=404) + + if request.user.is_authenticated and request.user.email == bill.user.email: + bill.unfreeze_course() + return Response(status=204) + return Response("Permission denied", status=403) + + class BillListView(APIView): renderer_classes = (JSONRenderer,) @@ -109,8 +140,7 @@ class InvoiceDetailView(APIView): price = request.JSON.get('price', None) comment = request.JSON.get('comment', None) real_price = request.JSON.get('real_price', None) - rebilling_on = request.JSON.get('is_rebilling', False) - pay_count = request.JSON.get('pay_count', None) + pay_count = int(request.JSON.get('pay_count', '1')) if bill_id is None: return Response("Не передан id счёта", status=400) @@ -129,14 +159,15 @@ class InvoiceDetailView(APIView): if bill.check_validate(invoice_id) and is_open: return Response("Уже есть платёж открывающий курс", status=400) - if rebilling_on: + if pay_count > 1: invoice = InvoiceRebilling.objects.create( bill=bill, method=method, status=status, is_open=is_open, - pay_count=pay_count, + rebilling_on=True, ) + else: try: invoice = Invoice.objects.get(id=invoice_id) @@ -172,6 +203,9 @@ class InvoiceDetailView(APIView): invoice.is_open = is_open invoice.comment = comment + if pay_count > 1: + invoice.create_child_pays(pay_count) + if invoice.method == 'Y' and invoice.yandex_pay is None: yandex_pay = Payment.objects.create( order_amount=invoice.price, @@ -427,6 +461,9 @@ class YandexAvisoView(APIView): }) pay.shop_amount = data['shopSumAmount'] + invoice = pay.invoice + invoice.key = data['invoiceId'] + invoice.save() pay.status = Payment.STATUS.SUCCESS now = timezone.now() pay.performed_datetime = now.isoformat() @@ -457,12 +494,6 @@ class YandexAvisoView(APIView): msg.attach_alternative(html_content, "text/html") msg.send() - try: - InvoiceRebilling.objects.get(yandex_pay=pay) - setup_periodic_billing(pay.order_number) - except InvoiceRebilling.DoesNotExist: - pass - return HttpResponse(xml_res, content_type='application/xml') @@ -470,7 +501,7 @@ class YandexFailView(APIView): renderer_classes = (JSONRenderer,) @staticmethod - def post(request): + def get(request): data = dict() for i in request.body.decode('utf-8').split('&'): key = i.split('=')[0] diff --git a/lms/celery.py b/lms/celery.py index 5f3c106..95c4803 100644 --- a/lms/celery.py +++ b/lms/celery.py @@ -3,6 +3,8 @@ import os from celery import Celery from raven import Client from raven.contrib.celery import register_signal, register_logger_signal +from celery.schedules import crontab + # set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lms.settings') @@ -13,11 +15,19 @@ app.config_from_object('django.conf:settings', namespace='CELERY') app.autodiscover_tasks() -client = Client('http://caaea487274f4e23a9107862484c79f3:3d463ad4717942508536f7a659921950@sentry.skillbox.ru/3') +client = Client('http://caaea487274f4e23a9107862484c79f3:3d463ad4717942508536f7a659921950@sentry.skillbox.ru/7') register_logger_signal(client) register_signal(client) +app.conf.beat_schedule = { + 'periodic_billing': { + 'schedule': crontab(minute='*/1'), + # crontab(minute='0',hour='*/3',), + 'task': 'finance.tasks.periodic_billing' + } +} + @app.task(bind=True) def debug_task(self): - print('Request: {0!r}'.format(self.request)) \ No newline at end of file + print('Request: {0!r}'.format(self.request)) diff --git a/lms/settings.py b/lms/settings.py index c9f4e61..adb44a1 100644 --- a/lms/settings.py +++ b/lms/settings.py @@ -5,6 +5,7 @@ import raven import environ import socket + root = environ.Path(__file__) - 2 env = environ.Env() @@ -212,6 +213,11 @@ LOGGING = { 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', 'tags': {'custom-tag': 'yandex'}, }, + 'business_rules': { + 'level': 'DEBUG', + 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', + 'tags': {'custom-tag': 'business_rules'}, + }, 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', @@ -228,6 +234,11 @@ LOGGING = { 'level': 'DEBUG', 'propagate': False }, + 'business_rules': { + 'handlers': ['business_rules'], + 'level': 'DEBUG', + 'propagate': False + }, }, } diff --git a/progress/views.py b/progress/views.py index 5b8b2de..97e9b3e 100644 --- a/progress/views.py +++ b/progress/views.py @@ -93,6 +93,7 @@ class CourseProgressUserView(APIView): class TeacherUpdateProgress(APIView): + permission_classes = (permissions.IsAuthenticated, ) renderer_classes = (JSONRenderer,) @staticmethod