diff --git a/api/v1/serializers/config.py b/api/v1/serializers/config.py index 4c1028b2..e494f861 100644 --- a/api/v1/serializers/config.py +++ b/api/v1/serializers/config.py @@ -20,6 +20,8 @@ class ConfigSerializer(serializers.ModelSerializer): MAIN_PAGE_TOP_IMAGE = serializers.SerializerMethodField() # SCHOOL_LOGO_IMAGE = serializers.ImageField(required=False, allow_null=True) # MAIN_PAGE_TOP_IMAGE = serializers.ImageField(required=False, allow_null=True) + REFERRER_BONUS = serializers.IntegerField() + REFERRAL_BONUS = serializers.IntegerField() class Meta: model = Config @@ -37,6 +39,8 @@ class ConfigSerializer(serializers.ModelSerializer): 'INSTAGRAM_PROFILE_URL', 'SCHOOL_LOGO_IMAGE', 'MAIN_PAGE_TOP_IMAGE', + 'REFERRER_BONUS', + 'REFERRAL_BONUS', ) def get_SCHOOL_LOGO_IMAGE(self, config): diff --git a/apps/auth/views.py b/apps/auth/views.py index 379ab0a5..c9ae8dfc 100644 --- a/apps/auth/views.py +++ b/apps/auth/views.py @@ -19,6 +19,7 @@ from django.shortcuts import redirect from apps.notification.utils import send_email from apps.config.models import Config +from apps.user.models import Referral from .forms import LearnerRegistrationForm from .tokens import verification_email_token @@ -31,6 +32,7 @@ class LearnerRegistrationView(FormView): template_name = "auth/registration-learner.html" def form_valid(self, form): + config = Config.load() first_name = form.cleaned_data['first_name'] last_name = form.cleaned_data['last_name'] email = form.cleaned_data['email'].lower() @@ -52,15 +54,20 @@ class LearnerRegistrationView(FormView): user.set_password(password) user.save() + referrer = self.request.session.get('referrer') + if referrer: + Referral.objects.create(referral=user, referrer_id=referrer, bonus=config.REFERRAL_BONUS, + referrer_bonus=config.REFERRER_BONUS) + # TODO: email admins? мб реферера уже нет, старая ссылка + self.request.session['referrer'] = None login(self.request, user) # fixme: change email text # fixme: async send email - config = Config.load() - refferer = urlsplit(self.request.META.get('HTTP_REFERER')) - refferer = str(refferer[0]) + '://' + str(refferer[1]) + http_referer = urlsplit(self.request.META.get('HTTP_REFERER')) + http_referer = str(http_referer[0]) + '://' + str(http_referer[1]) token = verification_email_token.make_token(user) - url = refferer + str(reverse_lazy('lilcity:verification-email', args=[token, user.id])) + url = http_referer + str(reverse_lazy('lilcity:verification-email', args=[token, user.id])) send_email('Вы успешно прошли регистрацию', email, "notification/email/verification_email.html", url=url, config=config) return JsonResponse({"success": True}, status=201) @@ -81,6 +88,7 @@ class LoginView(FormView): def form_valid(self, form): login(self.request, form.get_user()) + self.request.session['referrer'] = None return JsonResponse({"success": True}) def form_invalid(self, form): @@ -97,6 +105,7 @@ class VerificationEmailView(View): user.is_email_proved = True user.save() login(request, user) + self.request.session['referrer'] = None return redirect(reverse_lazy('lilcity:success-verification-email')) else: return JsonResponse({"success": False}, status=400) @@ -186,6 +195,14 @@ class FacebookLoginOrRegistration(View): user.photo.save(fname, photo, save=True) user.save() + referrer = self.request.session.get('referrer') + if referrer: + config = Config.load() + Referral.objects.create(referral=user, referrer_id=referrer, bonus=config.REFERRAL_BONUS, + referrer_bonus=config.REFERRER_BONUS) + # TODO: email admins? мб реферера уже нет, старая ссылка + self.request.session['referrer'] = None + login(requests, user=user) return JsonResponse({"success": True}) else: @@ -194,4 +211,5 @@ class FacebookLoginOrRegistration(View): fname = str(fb_id) + '.jpg' user.photo.save(fname, photo, save=True) login(requests, user=user) + self.request.session['referrer'] = None return JsonResponse({"success": True}) diff --git a/apps/config/migrations/0009_auto_20180729_0503.py b/apps/config/migrations/0009_auto_20180729_0503.py new file mode 100644 index 00000000..6a55aee1 --- /dev/null +++ b/apps/config/migrations/0009_auto_20180729_0503.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.6 on 2018-07-29 05:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('config', '0008_auto_20180425_1451'), + ] + + operations = [ + migrations.AddField( + model_name='config', + name='REFERRAL_DISCOUNT', + field=models.IntegerField(default=10), + ), + migrations.AddField( + model_name='config', + name='REFERRER_CASHBACK', + field=models.IntegerField(default=10), + ), + ] diff --git a/apps/config/migrations/0010_auto_20180820_0853.py b/apps/config/migrations/0010_auto_20180820_0853.py new file mode 100644 index 00000000..0b9af168 --- /dev/null +++ b/apps/config/migrations/0010_auto_20180820_0853.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.6 on 2018-08-20 08:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('config', '0009_auto_20180729_0503'), + ] + + operations = [ + migrations.RenameField( + model_name='config', + old_name='REFERRAL_DISCOUNT', + new_name='REFERRAL_BONUS', + ), + migrations.RenameField( + model_name='config', + old_name='REFERRER_CASHBACK', + new_name='REFERRER_BONUS', + ), + ] diff --git a/apps/config/models.py b/apps/config/models.py index c6be681d..498dd00f 100644 --- a/apps/config/models.py +++ b/apps/config/models.py @@ -17,6 +17,8 @@ class Config(models.Model): SERVICE_DISCOUNT = models.IntegerField(default=1000) SCHOOL_LOGO_IMAGE = models.ImageField(null=True, blank=True) MAIN_PAGE_TOP_IMAGE = models.ImageField(null=True, blank=True) + REFERRER_BONUS = models.IntegerField(default=10) + REFERRAL_BONUS = models.IntegerField(default=10) def save(self, *args, **kwargs): self.pk = 1 @@ -45,5 +47,7 @@ class Config(models.Model): 'SERVICE_DISCOUNT': '', 'SCHOOL_LOGO_IMAGE': '', 'MAIN_PAGE_TOP_IMAGE': '', + 'REFERRER_BONUS': '', + 'REFERRAL_BONUS': '', } return obj diff --git a/apps/payment/migrations/0020_userbonus.py b/apps/payment/migrations/0020_userbonus.py new file mode 100644 index 00000000..eaf9f835 --- /dev/null +++ b/apps/payment/migrations/0020_userbonus.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.6 on 2018-09-03 22:57 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('payment', '0019_payment_roistat_visit'), + ] + + operations = [ + migrations.CreateModel( + name='UserBonus', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, default=0, editable=False, max_digits=8)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('payment', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='payment.Payment')), + ], + ), + ] diff --git a/apps/payment/migrations/0021_auto_20180903_2257.py b/apps/payment/migrations/0021_auto_20180903_2257.py new file mode 100644 index 00000000..381eebd8 --- /dev/null +++ b/apps/payment/migrations/0021_auto_20180903_2257.py @@ -0,0 +1,27 @@ +# Generated by Django 2.0.6 on 2018-09-03 22:57 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0024_referral'), + ('payment', '0020_userbonus'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='userbonus', + name='referral', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='user.Referral'), + ), + migrations.AddField( + model_name='userbonus', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bonuses', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/apps/payment/migrations/0022_auto_20180904_0106.py b/apps/payment/migrations/0022_auto_20180904_0106.py new file mode 100644 index 00000000..d0b3dc03 --- /dev/null +++ b/apps/payment/migrations/0022_auto_20180904_0106.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.6 on 2018-09-04 01:06 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('payment', '0021_auto_20180903_2257'), + ] + + operations = [ + migrations.AlterField( + model_name='userbonus', + name='payment', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='payment.Payment'), + ), + ] diff --git a/apps/payment/migrations/0023_payment_bonus.py b/apps/payment/migrations/0023_payment_bonus.py new file mode 100644 index 00000000..3cc2a771 --- /dev/null +++ b/apps/payment/migrations/0023_payment_bonus.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.6 on 2018-09-05 23:37 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('payment', '0022_auto_20180904_0106'), + ] + + operations = [ + migrations.AddField( + model_name='payment', + name='bonus', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_payments', to='payment.UserBonus'), + ), + ] diff --git a/apps/payment/models.py b/apps/payment/models.py index e2d7cb13..b59269c9 100644 --- a/apps/payment/models.py +++ b/apps/payment/models.py @@ -1,3 +1,4 @@ +from decimal import Decimal import arrow from django.db.models import Func, F @@ -102,6 +103,7 @@ class Payment(PolymorphicModel): roistat_visit = models.PositiveIntegerField('Номер визита Roistat', null=True, editable=False) 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') objects = PaymentManger() @@ -120,18 +122,23 @@ class Payment(PolymorphicModel): return result.datetime @classmethod - def calc_amount(cls, course_payment=None, school_payment=None, user=None, course=None, date_start=None, weekdays=None): + def calc_amount(cls, payment=None, user=None, course=None, date_start=None, weekdays=None): date_start = date_start or now().date() date_end = Payment.add_months(date_start, 1) - if course_payment: - course = course_payment.course - user = course_payment.user - if school_payment: - user = school_payment.user - weekdays = school_payment.weekdays - date_start = school_payment.date_start - discount = 0 + if isinstance(payment, CoursePayment): + course = payment.course + user = payment.user + if isinstance(payment, SchoolPayment): + user = payment.user + weekdays = payment.weekdays + date_start = payment.date_start price = 0 + referral_bonus = 0 + referrer_bonus = 0 + if hasattr(user, 'referral') and not user.referral.payment: + referral_bonus = user.referral.bonus + referrer_bonus = user.referral.referrer_bonus + discount = 0 if course: price = course.price else: @@ -140,7 +147,6 @@ class Payment(PolymorphicModel): user=user, date_start__lte=date_start, date_end__gte=date_start, - add_days=False, status__in=[ Pingback.PINGBACK_TYPE_REGULAR, Pingback.PINGBACK_TYPE_GOODWILL, @@ -150,8 +156,8 @@ class Payment(PolymorphicModel): school_schedules_purchased = school_payments.annotate( joined_weekdays=Func(F('weekdays'), function='unnest', ) ).values_list('joined_weekdays', flat=True).distinct() - weekdays = set(map(int, weekdays)) - set(school_schedules_purchased) - prev_school_payment = school_payments.last() + weekdays = list(set(map(int, weekdays)) - set(school_schedules_purchased)) + prev_school_payment = school_payments.filter(add_days=False).last() add_days = bool(prev_school_payment) else: add_days = False @@ -169,15 +175,20 @@ class Payment(PolymorphicModel): price = school_schedules.aggregate( models.Sum('month_price'), ).get('month_price__sum', 0) - if not (school_payment and school_payment.id) and price >= config.SERVICE_DISCOUNT_MIN_AMOUNT: + if not (payment and payment.id) and price >= config.SERVICE_DISCOUNT_MIN_AMOUNT: discount = config.SERVICE_DISCOUNT amount = price - discount + referral_bonus = round(amount * referral_bonus / 100) + referrer_bonus = round(amount * referrer_bonus / 100) return { 'price': price, 'amount': amount, + 'referral_bonus': referral_bonus, + 'referrer_bonus': referrer_bonus, 'discount': discount, 'date_start': date_start, 'date_end': date_end, + 'weekdays': weekdays, } def calc_commission(self): @@ -199,6 +210,36 @@ class Payment(PolymorphicModel): Pingback.PINGBACK_TYPE_RISK_REVIEWED_DECLINED, ] + def save(self, *args, **kwargs): + paid = self.status in [Pingback.PINGBACK_TYPE_REGULAR, Pingback.PINGBACK_TYPE_GOODWILL, + Pingback.PINGBACK_TYPE_RISK_REVIEWED_ACCEPTED,] + amount_data = Payment.calc_amount(payment=self) + print('amount_data', amount_data) + if self.status is None and not self.bonus: + print(123) + self.amount = amount_data.get('amount') + if isinstance(self, SchoolPayment): + self.weekdays = amount_data.get('weekdays') + super().save(*args, **kwargs) + if isinstance(self, CoursePayment) and paid: + 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() + # Если юзер реферал и нет платежа, где применялась скидка + if hasattr(self.user, 'referral') and not self.user.referral.payment and paid: + # Платеж - как сигнал, что скидка применилась + self.user.referral.payment = self + self.user.referral.save() + # Отправляем кэшбэк + self.user.referral.send_bonuses(amount_data.get('referral_bonus'), amount_data.get('referrer_bonus')) + class CoursePayment(Payment): course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name='Курс', related_name='payments') @@ -207,22 +248,6 @@ class CoursePayment(Payment): verbose_name = 'Платеж за курс' verbose_name_plural = 'Платежи за курсы' - def save(self, *args, **kwargs): - if self.status is None: - amount_data = Payment.calc_amount(course_payment=self) - self.amount = amount_data.get('amount') - 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='Дни недели') @@ -241,12 +266,17 @@ class SchoolPayment(Payment): ]) return days - def save(self, *args, **kwargs): - if self.status is None: - amount_data = Payment.calc_amount(school_payment=self) - self.amount = amount_data.get('amount') - super().save(*args, **kwargs) - @property def date_end_humanize(self): return arrow.get(self.date_end, settings.TIME_ZONE).humanize(locale='ru') + + +class UserBonus(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='bonuses') + amount = models.DecimalField(max_digits=8, decimal_places=2, default=0, editable=False) + payment = models.ForeignKey(Payment, on_delete=models.SET_NULL, null=True) + referral = models.ForeignKey('user.Referral', on_delete=models.SET_NULL, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ('created_at',) diff --git a/apps/payment/views.py b/apps/payment/views.py index ad5a76ef..5663df39 100644 --- a/apps/payment/views.py +++ b/apps/payment/views.py @@ -27,7 +27,7 @@ from apps.course.models import Course from apps.school.models import SchoolSchedule from apps.payment.tasks import transaction_to_mixpanel, product_payment_to_mixpanel, transaction_to_roistat -from .models import AuthorBalance, CoursePayment, SchoolPayment, Payment +from .models import AuthorBalance, CoursePayment, SchoolPayment, Payment, UserBonus logger = logging.getLogger('django') @@ -100,17 +100,18 @@ class SchoolBuyView(TemplateView): host = urlsplit(self.request.META.get('HTTP_REFERER')) host = str(host[0]) + '://' + str(host[1]) weekdays = set(request.GET.getlist('weekdays', [])) + use_bonuses = request.GET.get('use_bonuses') roistat_visit = request.COOKIES.get('roistat_visit', None) 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, 'Выберите несколько дней недели.') - return redirect('school:summer-school') + return redirect('school:school') try: weekdays = [int(weekday) for weekday in weekdays] except ValueError: messages.error(request, 'Ошибка выбора дней недели.') - return redirect('school:summer-school') + return redirect('school:school') prev_school_payment = SchoolPayment.objects.filter( user=request.user, date_start__lte=date_start, @@ -132,6 +133,7 @@ class SchoolBuyView(TemplateView): add_days=True, roistat_visit=roistat_visit, ) + # Если произойдет ошибка и оплату бонусами повторят еще раз на те же дни, то вернет ошибку if school_payment.amount <= 0: messages.error(request, 'Выбранные дни отсутствуют в оставшемся периоде подписки') return redirect(reverse_lazy('school:school')) @@ -143,6 +145,19 @@ class SchoolBuyView(TemplateView): date_start=date_start, date_end=Payment.add_months(date_start), ) + if use_bonuses: + 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 + else: + bonus = UserBonus.objects.create(amount= -request.user.bonus, user=request.user, + payment=school_payment) + school_payment.amount -= request.user.bonus + school_payment.bonus = bonus + school_payment.save() + if school_payment.status == Pingback.PINGBACK_TYPE_REGULAR: + return redirect(reverse_lazy('payment-success')) product = Product( f'school_{school_payment.id}', school_payment.amount, @@ -213,6 +228,7 @@ class PaymentwallCallbackView(View): product_type_name, ) + if product_type_name == 'course': properties = { 'payment_id': payment.id, diff --git a/apps/user/migrations/0024_referral.py b/apps/user/migrations/0024_referral.py new file mode 100644 index 00000000..f16eeed5 --- /dev/null +++ b/apps/user/migrations/0024_referral.py @@ -0,0 +1,31 @@ +# Generated by Django 2.0.6 on 2018-09-03 22:57 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('payment', '0020_userbonus'), + ('user', '0023_user_trial_lesson'), + ] + + operations = [ + migrations.CreateModel( + name='Referral', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('bonus', models.IntegerField()), + ('referrer_bonus', models.IntegerField()), + ('payment', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='payment.Payment')), + ('referral', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='referral', to=settings.AUTH_USER_MODEL)), + ('referrer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='referrals', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Реферал', + 'verbose_name_plural': 'Рефералы', + }, + ), + ] diff --git a/apps/user/models.py b/apps/user/models.py index f9723e31..84d9a329 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -1,5 +1,6 @@ from json import dumps +from django.utils.functional import cached_property from rest_framework.authtoken.models import Token from phonenumber_field.modelfields import PhoneNumberField @@ -14,7 +15,6 @@ from django.urls import reverse from api.v1 import serializers from apps.notification.utils import send_email - from apps.user.tasks import user_to_mixpanel @@ -100,7 +100,7 @@ class User(AbstractUser): user_data = dumps(user_data, ensure_ascii=False) return user_data - @property + @cached_property def balance(self): income = self.balances.filter( type=0, @@ -118,6 +118,10 @@ class User(AbstractUser): return income_amount - income_commission - payout_amount + @cached_property + def bonus(self): + return int(self.bonuses.aggregate(models.Sum('amount')).get('amount__sum') or 0) + @receiver(post_save, sender=User) def create_auth_token(sender, instance=None, created=False, **kwargs): @@ -271,3 +275,20 @@ class EmailSubscription(models.Model): email = models.EmailField(_('email address'), unique=True) categories = models.ManyToManyField(SubscriptionCategory) mailchimp_status = models.PositiveSmallIntegerField(choices=MAILCHIMP_STATUS_CHOICES, default=ERROR) + + +class Referral(models.Model): + referral = models.OneToOneField(User, on_delete=models.CASCADE, related_name='referral') + referrer = models.ForeignKey(User, on_delete=models.CASCADE, related_name='referrals') + bonus = models.IntegerField() + referrer_bonus = models.IntegerField() + payment = models.OneToOneField('payment.Payment', null=True, blank=True, on_delete=models.CASCADE) + + class Meta: + verbose_name = 'Реферал' + verbose_name_plural = 'Рефералы' + + def send_bonuses(self, referral_bonus, referrer_bonus): + from apps.payment.models import UserBonus + UserBonus.objects.create(user=self.referral, amount=referral_bonus, payment=self.payment, referral=self) + UserBonus.objects.create(user=self.referrer, amount=referrer_bonus, payment=self.payment, referral=self) diff --git a/apps/user/templates/blocks/profile-menu.html b/apps/user/templates/blocks/profile-menu.html new file mode 100644 index 00000000..5bb21b94 --- /dev/null +++ b/apps/user/templates/blocks/profile-menu.html @@ -0,0 +1,14 @@ +
diff --git a/apps/user/templates/user/bonus-history.html b/apps/user/templates/user/bonus-history.html new file mode 100644 index 00000000..d4f32bcb --- /dev/null +++ b/apps/user/templates/user/bonus-history.html @@ -0,0 +1,71 @@ +{% extends "templates/lilcity/index.html" %} +{% load static %} +{% load rupluralize from plural %} + +{% block content %} +{% include "../blocks/profile-menu.html" with active="bonuses" %} + +