Compare commits

...

4 Commits

Author SHA1 Message Date
cfe2f68305 + SENTRY_ENVIRONMENT in env file 2024-05-26 02:41:13 +04:00
8da934bae7 + Default bonus levels in settings 2024-05-26 02:40:47 +04:00
1761f57fde + BonusProgramLevel
* Moved Bonus models to separate app
2024-05-26 02:40:04 +04:00
4e5360553a * Reverted endpoint for PaymentMethod back to /payment 2024-05-26 02:33:47 +04:00
23 changed files with 115 additions and 50 deletions

View File

@ -21,4 +21,7 @@ CURRENCY_GETGEOIP_API_KEY=""
FLOWER_BASIC_AUTH="login:pwd"
# Logging
SENTRY_DSN=""
SENTRY_DSN=""
# production/stage
SENTRY_ENVIRONMENT=""

View File

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

View File

@ -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)),
],
),

View File

@ -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),
),
]

View File

@ -1,5 +1 @@
from .bonus import (generate_referral_code, BonusType, BonusProgramMixin, BonusProgram,
BonusProgramTransaction, BonusProgramTransactionQuerySet)
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.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()

View File

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

View File

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

View File

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

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

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):
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'])

View File

@ -105,7 +105,8 @@ INSTALLED_APPS = [
'account',
'store',
'tg_bot',
'core'
'core',
'bonus_program'
]
MIDDLEWARE = [
@ -250,6 +251,7 @@ if SENTRY_DSN:
# of sampled transactions.
# We recommend adjusting this value in production.
profiles_sample_rate=1.0,
environment=get_secret("SENTRY_ENVIRONMENT"),
)
# Celery
@ -267,7 +269,15 @@ BONUS_ELIGIBILITY_STATUS = 'completed'
BONUS_PROGRAM_DEFAULT_CONFIG = {
"amounts": {
"signup": 150,
"default_purchase": 50,
"referral": 500,
}
},
"levels": [
# slug, name, orders_count, amount_default_purchase
("new", "Новичок", 0, 50),
("fashion", "Модник", 3, 150),
("pro", "Профессионал", 10, 250),
("shopaholic", "Шопоголик", 20, 350),
("killer", "Фэшн Киллер", 30, 500),
]
}

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ router.register(r'gifts', views.GiftAPI, basename='gifts')
router.register(r'poizon', views.PoizonAPI, basename='poizon')
router.register(r'promo', views.PromoCodeAPI, basename='promo')
router.register(r'category', views.CategoryAPI, basename='category')
router.register(r'settings/payment', views.PaymentMethodsAPI, basename='payment')
router.register(r'payment', views.PaymentMethodsAPI, basename='payment')
urlpatterns = [