+ BonusProgramLevel

* Moved Bonus models to separate app
This commit is contained in:
Phil Zhitnikov 2024-05-26 02:40:04 +04:00
parent 92c2f53e65
commit 0ff18ef891
21 changed files with 99 additions and 46 deletions

View File

@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import User, BonusProgramTransaction from .models import User
@admin.register(User) @admin.register(User)
@ -11,14 +11,3 @@ class UserAdmin(admin.ModelAdmin):
return User.objects.with_base_related() 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()

View File

@ -6,6 +6,7 @@ from django.db import migrations, models
import django.utils.timezone import django.utils.timezone
import account.models import account.models
import bonus_program.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -64,7 +65,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('balance', models.PositiveSmallIntegerField(default=0, verbose_name='Баланс, руб')), ('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)), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
), ),

View File

@ -1,9 +1,8 @@
# Generated by Django 4.2.2 on 2024-04-07 17:36 # Generated by Django 4.2.2 on 2024-04-07 17:36
import account.models
from django.db import migrations, models from django.db import migrations, models
import account.models import bonus_program.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -25,6 +24,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='user', model_name='user',
name='referral_code', 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),
), ),
] ]

View File

@ -1,5 +1 @@
from .bonus import (generate_referral_code, BonusType, BonusProgramMixin, BonusProgram,
BonusProgramTransaction, BonusProgramTransactionQuerySet)
from .user import User, UserManager, UserQuerySet, ReferralRelationship from .user import User, UserManager, UserQuerySet, ReferralRelationship

View File

@ -12,8 +12,7 @@ from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField from phonenumber_field.modelfields import PhoneNumberField
from phonenumber_field.phonenumber import PhoneNumber from phonenumber_field.phonenumber import PhoneNumber
from account.models import BonusProgramMixin from bonus_program.models import BonusProgramMixin, BonusProgram
from account.models.bonus import BonusProgram
from store.utils import concat_not_null_values from store.utils import concat_not_null_values
from tg_bot.tasks import send_tg_message from tg_bot.tasks import send_tg_message
@ -179,6 +178,11 @@ class User(BonusProgramMixin, AbstractUser):
.annotate(_orders_count=Count('customer_orders')) .annotate(_orders_count=Count('customer_orders'))
.filter(_orders_count__gt=0)) .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 @property
def inviter(self): def inviter(self):
return User.objects.filter(user_invited__invited=self.id).first() return User.objects.filter(user_invited__invited=self.id).first()

View File

@ -5,7 +5,9 @@ from djoser.conf import settings as djoser_settings
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import AuthenticationFailed 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 from .utils import verify_telegram_authentication
@ -24,15 +26,6 @@ class UserSerializer(serializers.ModelSerializer):
return obj.invited_users_with_orders.count() 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): def non_zero_validator(value):
if value == 0: if value == 0:
raise serializers.ValidationError("Value cannot be zero") raise serializers.ValidationError("Value cannot be zero")

View File

@ -3,7 +3,8 @@ import logging
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver 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__) logger = logging.getLogger(__name__)

View File

@ -11,8 +11,8 @@ from rest_framework.renderers import StaticHTMLRenderer
from rest_framework.response import Response from rest_framework.response import Response
from account.models import User from account.models import User
from account.serializers import SetInitialPasswordSerializer, BonusProgramTransactionSerializer, \ from account.serializers import SetInitialPasswordSerializer, UserBalanceUpdateSerializer, TelegramCallbackSerializer
UserBalanceUpdateSerializer, TelegramCallbackSerializer from bonus_program.serializers import BonusProgramTransactionSerializer
from tg_bot.handlers.start import request_phone_sync from tg_bot.handlers.start import request_phone_sync
from tg_bot.messages import TGCoreMessage from tg_bot.messages import TGCoreMessage

View File

16
bonus_program/admin.py Normal file
View File

@ -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()

6
bonus_program/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class BonusProgramConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'bonus_program'

View File

View File

@ -10,6 +10,7 @@ from django.utils.formats import localize
from django.utils.functional import cached_property from django.utils.functional import cached_property
from core.models import BonusProgramConfig from core.models import BonusProgramConfig
from store.models import Checklist
from tg_bot.messages import TGBonusMessage from tg_bot.messages import TGBonusMessage
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -76,7 +77,7 @@ class BonusProgramTransaction(models.Model):
""" Represents the history of all bonus program transactions """ """ Represents the history of all bonus program transactions """
type = models.PositiveSmallIntegerField('Тип транзакции', choices=BonusType.CHOICES) 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) date = models.DateTimeField('Дата транзакции', auto_now_add=True)
amount = models.SmallIntegerField('Количество, руб') amount = models.SmallIntegerField('Количество, руб')
comment = models.CharField('Комментарий', max_length=200, null=True, blank=True) comment = models.CharField('Комментарий', max_length=200, null=True, blank=True)
@ -273,9 +274,8 @@ class BonusProgram:
user.update_balance(amount, bonus_type) user.update_balance(amount, bonus_type)
@staticmethod @staticmethod
def add_order_bonus(order: 'Checklist'): def add_order_bonus(order: Checklist):
bonus_type = BonusType.DEFAULT_PURCHASE bonus_type = BonusType.DEFAULT_PURCHASE
amount = BonusProgramConfig.load().amount_default_purchase
# Check if data is sufficient # Check if data is sufficient
if order is None or order.customer_id is None: if order is None or order.customer_id is None:
@ -285,11 +285,14 @@ class BonusProgram:
if order.status != settings.BONUS_ELIGIBILITY_STATUS: if order.status != settings.BONUS_ELIGIBILITY_STATUS:
return return
level = BonusProgramLevel.objects.level_for_order_count(order.customer.completed_orders_count)
amount = getattr(level, 'amount_default_purchase', 0)
# Add bonuses # Add bonuses
order.customer.update_balance(amount, bonus_type, order=order) order.customer.update_balance(amount, bonus_type, order=order)
@staticmethod @staticmethod
def add_referral_bonus(order: 'Checklist', for_inviter: bool): def add_referral_bonus(order: Checklist, for_inviter: bool):
amount = BonusProgramConfig.load().amount_referral amount = BonusProgramConfig.load().amount_referral
# Check if data is sufficient # Check if data is sufficient
@ -306,3 +309,17 @@ class BonusProgram:
# Add bonuses # Add bonuses
user.update_balance(amount, bonus_type, order=order) 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()

View File

@ -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')

3
bonus_program/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
bonus_program/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -37,8 +37,6 @@ DEFAULT_CONFIG = settings.BONUS_PROGRAM_DEFAULT_CONFIG
class BonusProgramConfig(models.Model): class BonusProgramConfig(models.Model):
amount_signup = models.PositiveSmallIntegerField( amount_signup = models.PositiveSmallIntegerField(
'Бонус за регистрацию', default=DEFAULT_CONFIG['amounts']['signup']) 'Бонус за регистрацию', default=DEFAULT_CONFIG['amounts']['signup'])
amount_default_purchase = models.PositiveSmallIntegerField(
'Бонус за обычную покупку', default=DEFAULT_CONFIG['amounts']['default_purchase'])
amount_referral = models.PositiveSmallIntegerField( amount_referral = models.PositiveSmallIntegerField(
'Реферальный бонус', default=DEFAULT_CONFIG['amounts']['referral']) 'Реферальный бонус', default=DEFAULT_CONFIG['amounts']['referral'])

View File

@ -105,7 +105,8 @@ INSTALLED_APPS = [
'account', 'account',
'store', 'store',
'tg_bot', 'tg_bot',
'core' 'core',
'bonus_program'
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@ -1,6 +1,8 @@
from django.core.management import BaseCommand from django.core.management import BaseCommand
from django.conf import settings
from tqdm import tqdm from tqdm import tqdm
from bonus_program.models import BonusProgramLevel
from store.models import Category, PaymentMethod from store.models import Category, PaymentMethod
@ -134,10 +136,23 @@ def create_payment_types():
PaymentMethod.objects.get_or_create(slug=slug, defaults=data) 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): class Command(BaseCommand):
help = ''' Create root categories ''' help = ''' Create root categories '''
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
create_categories() create_categories()
create_payment_types() create_payment_types()
create_bonus_program_levels()

View File

@ -20,7 +20,6 @@ from django_cleanup import cleanup
from mptt.fields import TreeForeignKey from mptt.fields import TreeForeignKey
from mptt.models import MPTTModel from mptt.models import MPTTModel
from account.models import BonusProgram
from core.models import GlobalSettings from core.models import GlobalSettings
from store.utils import create_preview from store.utils import create_preview
@ -166,7 +165,7 @@ class ChecklistQuerySet(models.QuerySet):
.prefetch_related(Prefetch('images', to_attr='_images')) .prefetch_related(Prefetch('images', to_attr='_images'))
def annotate_bonus_used(self): def annotate_bonus_used(self):
from account.models import BonusProgramTransaction, BonusType from bonus_program.models import BonusProgramTransaction, BonusType
amount_subquery = Subquery( amount_subquery = Subquery(
BonusProgramTransaction.objects.all() BonusProgramTransaction.objects.all()
@ -606,7 +605,7 @@ class Checklist(models.Model):
return return
# Check if any BonusProgramTransaction bound to current order exists # 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(): if BonusProgramTransaction.objects.filter(order_id=self.id).exists():
return return

View File

@ -2,7 +2,7 @@ from django.db.transaction import atomic
from drf_extra_fields.fields import Base64ImageField from drf_extra_fields.fields import Base64ImageField
from rest_framework import serializers from rest_framework import serializers
from account.models.bonus import BonusProgram from bonus_program.models import BonusProgram
from account.serializers import UserSerializer from account.serializers import UserSerializer
from utils.exceptions import CRMException from utils.exceptions import CRMException
from store.models import Checklist, Category, PaymentMethod, Promocode, Image, Gift from store.models import Checklist, Category, PaymentMethod, Promocode, Image, Gift