From 0ff18ef89100612966edaebbeb3d99e5805c4ffc Mon Sep 17 00:00:00 2001 From: phzhik Date: Sun, 26 May 2024 02:40:04 +0400 Subject: [PATCH] + BonusProgramLevel * Moved Bonus models to separate app --- account/admin.py | 13 +--------- account/migrations/0001_initial.py | 3 ++- ...options_user_balance_user_referral_code.py | 5 ++-- account/models/__init__.py | 4 --- account/models/user.py | 8 ++++-- account/serializers.py | 13 +++------- account/signals.py | 3 ++- account/views.py | 4 +-- bonus_program/__init__.py | 0 bonus_program/admin.py | 16 ++++++++++++ bonus_program/apps.py | 6 +++++ bonus_program/migrations/__init__.py | 0 .../bonus.py => bonus_program/models.py | 25 ++++++++++++++++--- bonus_program/serializers.py | 12 +++++++++ bonus_program/tests.py | 3 +++ bonus_program/views.py | 3 +++ core/models.py | 2 -- poizonstore/settings.py | 3 ++- .../commands/create_initial_data.py | 15 +++++++++++ store/models.py | 5 ++-- store/serializers.py | 2 +- 21 files changed, 99 insertions(+), 46 deletions(-) create mode 100644 bonus_program/__init__.py create mode 100644 bonus_program/admin.py create mode 100644 bonus_program/apps.py create mode 100644 bonus_program/migrations/__init__.py rename account/models/bonus.py => bonus_program/models.py (90%) create mode 100644 bonus_program/serializers.py create mode 100644 bonus_program/tests.py create mode 100644 bonus_program/views.py diff --git a/account/admin.py b/account/admin.py index d8a7112..0ff8bd5 100644 --- a/account/admin.py +++ b/account/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import User, BonusProgramTransaction +from .models import User @admin.register(User) @@ -11,14 +11,3 @@ class UserAdmin(admin.ModelAdmin): return User.objects.with_base_related() -@admin.register(BonusProgramTransaction) -class BonusProgramTransactionAdmin(admin.ModelAdmin): - list_display = ('id', 'type', 'user', 'date', 'amount', 'comment', 'order', 'was_cancelled') - - def get_queryset(self, request): - return BonusProgramTransaction.objects.with_base_related() - - def delete_queryset(self, request, queryset): - for obj in queryset: - obj.cancel() - diff --git a/account/migrations/0001_initial.py b/account/migrations/0001_initial.py index 3e752b4..39f1c7f 100644 --- a/account/migrations/0001_initial.py +++ b/account/migrations/0001_initial.py @@ -6,6 +6,7 @@ from django.db import migrations, models import django.utils.timezone import account.models +import bonus_program.models class Migration(migrations.Migration): @@ -64,7 +65,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('balance', models.PositiveSmallIntegerField(default=0, verbose_name='Баланс, руб')), - ('referral_code', models.CharField(default=account.models.generate_referral_code, editable=False, max_length=9)), + ('referral_code', models.CharField(default=bonus_program.models.generate_referral_code, editable=False, max_length=9)), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), diff --git a/account/migrations/0009_alter_user_options_user_balance_user_referral_code.py b/account/migrations/0009_alter_user_options_user_balance_user_referral_code.py index e8c180c..587d362 100644 --- a/account/migrations/0009_alter_user_options_user_balance_user_referral_code.py +++ b/account/migrations/0009_alter_user_options_user_balance_user_referral_code.py @@ -1,9 +1,8 @@ # Generated by Django 4.2.2 on 2024-04-07 17:36 -import account.models from django.db import migrations, models -import account.models +import bonus_program.models class Migration(migrations.Migration): @@ -25,6 +24,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='user', name='referral_code', - field=models.CharField(default=account.models.generate_referral_code, editable=False, max_length=10), + field=models.CharField(default=bonus_program.models.generate_referral_code, editable=False, max_length=10), ), ] diff --git a/account/models/__init__.py b/account/models/__init__.py index 72b0ff9..836a739 100644 --- a/account/models/__init__.py +++ b/account/models/__init__.py @@ -1,5 +1 @@ -from .bonus import (generate_referral_code, BonusType, BonusProgramMixin, BonusProgram, - BonusProgramTransaction, BonusProgramTransactionQuerySet) from .user import User, UserManager, UserQuerySet, ReferralRelationship - - diff --git a/account/models/user.py b/account/models/user.py index 3a7b30f..078b4eb 100644 --- a/account/models/user.py +++ b/account/models/user.py @@ -12,8 +12,7 @@ from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField from phonenumber_field.phonenumber import PhoneNumber -from account.models import BonusProgramMixin -from account.models.bonus import BonusProgram +from bonus_program.models import BonusProgramMixin, BonusProgram from store.utils import concat_not_null_values from tg_bot.tasks import send_tg_message @@ -179,6 +178,11 @@ class User(BonusProgramMixin, AbstractUser): .annotate(_orders_count=Count('customer_orders')) .filter(_orders_count__gt=0)) + @property + def completed_orders_count(self): + from store.models import Checklist + return Checklist.objects.filter(customer_id=self.id, status=Checklist.Status.COMPLETED).count() + @property def inviter(self): return User.objects.filter(user_invited__invited=self.id).first() diff --git a/account/serializers.py b/account/serializers.py index 327fb01..ed4f9cb 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -5,7 +5,9 @@ from djoser.conf import settings as djoser_settings from rest_framework import serializers from rest_framework.exceptions import AuthenticationFailed -from .models import User, BonusProgramTransaction, BonusType +from bonus_program.serializers import BonusProgramTransactionSerializer +from .models import User +from bonus_program.models import BonusType from .utils import verify_telegram_authentication @@ -24,15 +26,6 @@ class UserSerializer(serializers.ModelSerializer): return obj.invited_users_with_orders.count() -class BonusProgramTransactionSerializer(serializers.ModelSerializer): - order_id = serializers.StringRelatedField(source='order.id', allow_null=True) - type = serializers.CharField(source='get_type_display') - - class Meta: - model = BonusProgramTransaction - fields = ('id', 'type', 'date', 'amount', 'order_id', 'comment', 'was_cancelled') - - def non_zero_validator(value): if value == 0: raise serializers.ValidationError("Value cannot be zero") diff --git a/account/signals.py b/account/signals.py index f10f176..be4d7f9 100644 --- a/account/signals.py +++ b/account/signals.py @@ -3,7 +3,8 @@ import logging from django.db.models.signals import post_save, post_delete from django.dispatch import receiver -from account.models import User, ReferralRelationship, BonusProgramTransaction +from account.models import User, ReferralRelationship +from bonus_program.models import BonusProgramTransaction logger = logging.getLogger(__name__) diff --git a/account/views.py b/account/views.py index abe2f13..2a104e1 100644 --- a/account/views.py +++ b/account/views.py @@ -11,8 +11,8 @@ from rest_framework.renderers import StaticHTMLRenderer from rest_framework.response import Response from account.models import User -from account.serializers import SetInitialPasswordSerializer, BonusProgramTransactionSerializer, \ - UserBalanceUpdateSerializer, TelegramCallbackSerializer +from account.serializers import SetInitialPasswordSerializer, UserBalanceUpdateSerializer, TelegramCallbackSerializer +from bonus_program.serializers import BonusProgramTransactionSerializer from tg_bot.handlers.start import request_phone_sync from tg_bot.messages import TGCoreMessage diff --git a/bonus_program/__init__.py b/bonus_program/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bonus_program/admin.py b/bonus_program/admin.py new file mode 100644 index 0000000..92e4cce --- /dev/null +++ b/bonus_program/admin.py @@ -0,0 +1,16 @@ +from django.contrib import admin + +from bonus_program.models import BonusProgramTransaction + + +# Register your models here. +@admin.register(BonusProgramTransaction) +class BonusProgramTransactionAdmin(admin.ModelAdmin): + list_display = ('id', 'type', 'user', 'date', 'amount', 'comment', 'order', 'was_cancelled') + + def get_queryset(self, request): + return BonusProgramTransaction.objects.with_base_related() + + def delete_queryset(self, request, queryset): + for obj in queryset: + obj.cancel() diff --git a/bonus_program/apps.py b/bonus_program/apps.py new file mode 100644 index 0000000..e8f24ac --- /dev/null +++ b/bonus_program/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BonusProgramConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'bonus_program' diff --git a/bonus_program/migrations/__init__.py b/bonus_program/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/account/models/bonus.py b/bonus_program/models.py similarity index 90% rename from account/models/bonus.py rename to bonus_program/models.py index 4b53e10..2a070b9 100644 --- a/account/models/bonus.py +++ b/bonus_program/models.py @@ -10,6 +10,7 @@ from django.utils.formats import localize from django.utils.functional import cached_property from core.models import BonusProgramConfig +from store.models import Checklist from tg_bot.messages import TGBonusMessage logger = logging.getLogger(__name__) @@ -76,7 +77,7 @@ class BonusProgramTransaction(models.Model): """ Represents the history of all bonus program transactions """ type = models.PositiveSmallIntegerField('Тип транзакции', choices=BonusType.CHOICES) - user = models.ForeignKey('User', verbose_name='Пользователь транзакции', on_delete=models.CASCADE, related_name='bonus_transactions') + user = models.ForeignKey('account.User', verbose_name='Пользователь транзакции', on_delete=models.CASCADE, related_name='bonus_transactions') date = models.DateTimeField('Дата транзакции', auto_now_add=True) amount = models.SmallIntegerField('Количество, руб') comment = models.CharField('Комментарий', max_length=200, null=True, blank=True) @@ -273,9 +274,8 @@ class BonusProgram: user.update_balance(amount, bonus_type) @staticmethod - def add_order_bonus(order: 'Checklist'): + def add_order_bonus(order: Checklist): bonus_type = BonusType.DEFAULT_PURCHASE - amount = BonusProgramConfig.load().amount_default_purchase # Check if data is sufficient if order is None or order.customer_id is None: @@ -285,11 +285,14 @@ class BonusProgram: if order.status != settings.BONUS_ELIGIBILITY_STATUS: return + level = BonusProgramLevel.objects.level_for_order_count(order.customer.completed_orders_count) + amount = getattr(level, 'amount_default_purchase', 0) + # Add bonuses order.customer.update_balance(amount, bonus_type, order=order) @staticmethod - def add_referral_bonus(order: 'Checklist', for_inviter: bool): + def add_referral_bonus(order: Checklist, for_inviter: bool): amount = BonusProgramConfig.load().amount_referral # Check if data is sufficient @@ -306,3 +309,17 @@ class BonusProgram: # Add bonuses user.update_balance(amount, bonus_type, order=order) + + +class BonusProgramLevelQuerySet(models.QuerySet): + def level_for_order_count(self, count): + return self.filter(orders_count__lt=count).order_by('-orders_count').first() + + +class BonusProgramLevel(models.Model): + slug = models.SlugField('Идентификатор', unique=True) + name = models.CharField('Название', max_length=30) + orders_count = models.PositiveSmallIntegerField('Минимальное количество заказов', unique=True) + amount_default_purchase = models.PositiveSmallIntegerField('Бонус за обычную покупку') + + objects = BonusProgramLevelQuerySet.as_manager() diff --git a/bonus_program/serializers.py b/bonus_program/serializers.py new file mode 100644 index 0000000..85ebdf6 --- /dev/null +++ b/bonus_program/serializers.py @@ -0,0 +1,12 @@ +from rest_framework import serializers + +from bonus_program.models import BonusProgramTransaction + + +class BonusProgramTransactionSerializer(serializers.ModelSerializer): + order_id = serializers.StringRelatedField(source='order.id', allow_null=True) + type = serializers.CharField(source='get_type_display') + + class Meta: + model = BonusProgramTransaction + fields = ('id', 'type', 'date', 'amount', 'order_id', 'comment', 'was_cancelled') diff --git a/bonus_program/tests.py b/bonus_program/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/bonus_program/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/bonus_program/views.py b/bonus_program/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/bonus_program/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/core/models.py b/core/models.py index a337f7e..ff7390d 100644 --- a/core/models.py +++ b/core/models.py @@ -37,8 +37,6 @@ DEFAULT_CONFIG = settings.BONUS_PROGRAM_DEFAULT_CONFIG class BonusProgramConfig(models.Model): amount_signup = models.PositiveSmallIntegerField( 'Бонус за регистрацию', default=DEFAULT_CONFIG['amounts']['signup']) - amount_default_purchase = models.PositiveSmallIntegerField( - 'Бонус за обычную покупку', default=DEFAULT_CONFIG['amounts']['default_purchase']) amount_referral = models.PositiveSmallIntegerField( 'Реферальный бонус', default=DEFAULT_CONFIG['amounts']['referral']) diff --git a/poizonstore/settings.py b/poizonstore/settings.py index a401e43..9383515 100644 --- a/poizonstore/settings.py +++ b/poizonstore/settings.py @@ -105,7 +105,8 @@ INSTALLED_APPS = [ 'account', 'store', 'tg_bot', - 'core' + 'core', + 'bonus_program' ] MIDDLEWARE = [ diff --git a/store/management/commands/create_initial_data.py b/store/management/commands/create_initial_data.py index d68f9b5..91bcece 100644 --- a/store/management/commands/create_initial_data.py +++ b/store/management/commands/create_initial_data.py @@ -1,6 +1,8 @@ from django.core.management import BaseCommand +from django.conf import settings from tqdm import tqdm +from bonus_program.models import BonusProgramLevel from store.models import Category, PaymentMethod @@ -134,10 +136,23 @@ def create_payment_types(): PaymentMethod.objects.get_or_create(slug=slug, defaults=data) +def create_bonus_program_levels(): + for cfg in settings.BONUS_PROGRAM_DEFAULT_CONFIG['levels']: + slug, name, order_count, amount_default_purchase = cfg + BonusProgramLevel.objects.get_or_create( + slug=slug, + defaults={ + 'name': name, + 'orders_count': order_count, + 'amount_default_purchase': amount_default_purchase + }) + + class Command(BaseCommand): help = ''' Create root categories ''' def handle(self, *args, **kwargs): create_categories() create_payment_types() + create_bonus_program_levels() diff --git a/store/models.py b/store/models.py index b8b620e..f863395 100644 --- a/store/models.py +++ b/store/models.py @@ -20,7 +20,6 @@ from django_cleanup import cleanup from mptt.fields import TreeForeignKey from mptt.models import MPTTModel -from account.models import BonusProgram from core.models import GlobalSettings from store.utils import create_preview @@ -166,7 +165,7 @@ class ChecklistQuerySet(models.QuerySet): .prefetch_related(Prefetch('images', to_attr='_images')) def annotate_bonus_used(self): - from account.models import BonusProgramTransaction, BonusType + from bonus_program.models import BonusProgramTransaction, BonusType amount_subquery = Subquery( BonusProgramTransaction.objects.all() @@ -606,7 +605,7 @@ class Checklist(models.Model): return # Check if any BonusProgramTransaction bound to current order exists - from account.models import BonusProgramTransaction + from bonus_program.models import BonusProgramTransaction, BonusProgram if BonusProgramTransaction.objects.filter(order_id=self.id).exists(): return diff --git a/store/serializers.py b/store/serializers.py index cda4997..4c52762 100644 --- a/store/serializers.py +++ b/store/serializers.py @@ -2,7 +2,7 @@ from django.db.transaction import atomic from drf_extra_fields.fields import Base64ImageField from rest_framework import serializers -from account.models.bonus import BonusProgram +from bonus_program.models import BonusProgram from account.serializers import UserSerializer from utils.exceptions import CRMException from store.models import Checklist, Category, PaymentMethod, Promocode, Image, Gift