+ BonusProgramConfig
* Moved GlobalSettings to core app * Moved bonus program logic from User to BonusProgram class * Worked on error handling a bit
This commit is contained in:
parent
c47864106e
commit
00686e9dc4
|
|
@ -1,4 +1,5 @@
|
||||||
from .bonus import generate_referral_code, BonusType, BonusProgramMixin, BonusProgramTransaction, BonusProgramTransactionQuerySet
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import logging
|
import logging
|
||||||
from contextlib import suppress
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
@ -10,7 +9,7 @@ from django.utils.crypto import get_random_string
|
||||||
from django.utils.formats import localize
|
from django.utils.formats import localize
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
from store.models import Checklist
|
from core.models import BonusProgramConfig
|
||||||
from tg_bot.messages import TGBonusMessage
|
from tg_bot.messages import TGBonusMessage
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -60,7 +59,7 @@ class BonusType:
|
||||||
SPENT_PURCHASE: 'SPENT_PURCHASE',
|
SPENT_PURCHASE: 'SPENT_PURCHASE',
|
||||||
}
|
}
|
||||||
|
|
||||||
ONE_TIME_TYPES = {SIGNUP}
|
ONE_TIME_TYPES = {SIGNUP, FOR_INVITER, INVITED_FIRST_PURCHASE}
|
||||||
ORDER_TYPES = {DEFAULT_PURCHASE, FOR_INVITER, INVITED_FIRST_PURCHASE, SPENT_PURCHASE}
|
ORDER_TYPES = {DEFAULT_PURCHASE, FOR_INVITER, INVITED_FIRST_PURCHASE, SPENT_PURCHASE}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -171,30 +170,31 @@ class BonusProgramTransaction(models.Model):
|
||||||
if self.id:
|
if self.id:
|
||||||
qs = qs.exclude(id=self.id)
|
qs = qs.exclude(id=self.id)
|
||||||
|
|
||||||
uniqueness_err = None
|
|
||||||
bonus_name = BonusType.LOG_NAMES.get(self.type, self.type)
|
bonus_name = BonusType.LOG_NAMES.get(self.type, self.type)
|
||||||
|
|
||||||
match self.type:
|
if self.type in BonusType.ONE_TIME_TYPES:
|
||||||
case t if t in BonusType.ONE_TIME_TYPES:
|
if qs.exists():
|
||||||
if qs.exists():
|
raise ValidationError(f"User {self.user_id} already got {bonus_name} one-time bonus")
|
||||||
uniqueness_err = f"User {self.user_id} already got {bonus_name} one-time bonus"
|
|
||||||
|
|
||||||
case t if t in BonusType.ORDER_TYPES:
|
if self.type in BonusType.ORDER_TYPES:
|
||||||
# Check that order is defined
|
# Check that order is defined
|
||||||
if self.order_id is None:
|
if self.order_id is None:
|
||||||
raise ValidationError("Order is required for that type")
|
raise ValidationError("Order is required for that type")
|
||||||
|
|
||||||
# Check for duplicates for the same order
|
# Check for duplicates for the same order
|
||||||
already_exists = qs.filter(order_id=self.order_id).exists()
|
already_exists = qs.filter(order_id=self.order_id).exists()
|
||||||
if already_exists:
|
if already_exists:
|
||||||
uniqueness_err = f"User {self.user_id} already got {bonus_name} bonus for order #{self.order_id}"
|
raise ValidationError(f"User {self.user_id} already got {bonus_name} bonus for order #{self.order_id}")
|
||||||
|
|
||||||
if uniqueness_err:
|
|
||||||
logger.info(uniqueness_err)
|
|
||||||
raise ValidationError(uniqueness_err)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
self.full_clean()
|
try:
|
||||||
|
self.full_clean()
|
||||||
|
except Exception as e:
|
||||||
|
# Catch all validation errors here and log it
|
||||||
|
logger.error(f"Error during bonus saving: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.user.recalculate_balance()
|
||||||
|
|
||||||
if self.id is None:
|
if self.id is None:
|
||||||
self._notify_user_about_new_transaction()
|
self._notify_user_about_new_transaction()
|
||||||
|
|
@ -219,6 +219,8 @@ def generate_referral_code():
|
||||||
|
|
||||||
|
|
||||||
class BonusProgramMixin(models.Model):
|
class BonusProgramMixin(models.Model):
|
||||||
|
"""BonusProgram fields for User model"""
|
||||||
|
|
||||||
balance = models.PositiveSmallIntegerField('Баланс, руб', default=0, editable=False)
|
balance = models.PositiveSmallIntegerField('Баланс, руб', default=0, editable=False)
|
||||||
referral_code = models.CharField(max_length=settings.REFERRAL_CODE_LENGTH, default=generate_referral_code,
|
referral_code = models.CharField(max_length=settings.REFERRAL_CODE_LENGTH, default=generate_referral_code,
|
||||||
editable=False)
|
editable=False)
|
||||||
|
|
@ -249,44 +251,54 @@ class BonusProgramMixin(models.Model):
|
||||||
self.balance = max(0, total_balance)
|
self.balance = max(0, total_balance)
|
||||||
self.save(update_fields=['balance'])
|
self.save(update_fields=['balance'])
|
||||||
|
|
||||||
def spend_bonuses(self, order: Checklist):
|
|
||||||
|
class BonusProgram:
|
||||||
|
@staticmethod
|
||||||
|
def spend_bonuses(order: 'Checklist'):
|
||||||
|
# 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:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Always use fresh balance
|
# Always use fresh balance
|
||||||
self.recalculate_balance()
|
order.customer.recalculate_balance()
|
||||||
|
|
||||||
# Spend full_price bonuses or nothing
|
# Spend full_price bonuses or nothing
|
||||||
to_spend = min(self.balance, order.full_price)
|
to_spend = min(order.customer.balance, order.full_price)
|
||||||
order.customer.update_balance(-to_spend, BonusType.SPENT_PURCHASE, order=order)
|
order.customer.update_balance(-to_spend, BonusType.SPENT_PURCHASE, order=order)
|
||||||
|
|
||||||
def add_signup_bonus(self):
|
@staticmethod
|
||||||
|
def add_signup_bonus(user: 'User'):
|
||||||
bonus_type = BonusType.SIGNUP
|
bonus_type = BonusType.SIGNUP
|
||||||
amount = self._get_bonus_amount("signup")
|
amount = BonusProgramConfig.load().amount_signup
|
||||||
|
|
||||||
self.update_balance(amount, bonus_type)
|
user.update_balance(amount, bonus_type)
|
||||||
|
|
||||||
def add_order_bonus(self, order):
|
|
||||||
from store.models import Checklist
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def add_order_bonus(order: 'Checklist'):
|
||||||
bonus_type = BonusType.DEFAULT_PURCHASE
|
bonus_type = BonusType.DEFAULT_PURCHASE
|
||||||
amount = self._get_bonus_amount("default_purchase")
|
amount = BonusProgramConfig.load().amount_default_purchase
|
||||||
|
|
||||||
if order.status not in Checklist.Status.BONUS_ORDER_STATUSES:
|
# Check if data is sufficient
|
||||||
|
if order is None or order.customer_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if eligible
|
||||||
|
if order.status != settings.BONUS_ELIGIBILITY_STATUS:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.update_balance(amount, bonus_type, order=order)
|
self.update_balance(amount, bonus_type, order=order)
|
||||||
|
|
||||||
def add_referral_bonus(self, order: Checklist, for_inviter: bool):
|
@staticmethod
|
||||||
amount = self._get_bonus_amount("referral")
|
def add_referral_bonus(order: 'Checklist', for_inviter: bool):
|
||||||
|
amount = BonusProgramConfig.load().amount_referral
|
||||||
|
|
||||||
# Check if data is sufficient
|
# Check if data is sufficient
|
||||||
if order.customer_id is None or order.customer.inviter is None:
|
if order.customer_id is None or order.customer.inviter is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if eligible
|
# Check if eligible
|
||||||
# Bonus is only for first purchase and only for orders that reached the CHINA_RUSSIA status
|
# Bonus is only for first purchase and only for orders that reached the COMPLETED status
|
||||||
if order.status != Checklist.Status.CHINA_RUSSIA or order.customer.customer_orders.count() != 1:
|
if order.status != settings.BONUS_ELIGIBILITY_STATUS or order.customer.customer_orders.count() != 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
user = order.customer.inviter if for_inviter else order.customer
|
user = order.customer.inviter if for_inviter else order.customer
|
||||||
|
|
@ -294,16 +306,3 @@ class BonusProgramMixin(models.Model):
|
||||||
|
|
||||||
# Add bonuses
|
# Add bonuses
|
||||||
user.update_balance(amount, bonus_type, order=order)
|
user.update_balance(amount, bonus_type, order=order)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_bonus_amount(config_key) -> int:
|
|
||||||
amount = 0
|
|
||||||
with suppress(KeyError):
|
|
||||||
amount = settings.BONUS_PROGRAM_CONFIG["amounts"][config_key]
|
|
||||||
|
|
||||||
return amount
|
|
||||||
|
|
||||||
# TODO: move to custom logger
|
|
||||||
def _log(self, level, message: str):
|
|
||||||
message = f"[BonusProgram #{self.id}] {message}"
|
|
||||||
logger.log(level, message)
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from phonenumber_field.modelfields import PhoneNumberField
|
||||||
from phonenumber_field.phonenumber import PhoneNumber
|
from phonenumber_field.phonenumber import PhoneNumber
|
||||||
|
|
||||||
from account.models import BonusProgramMixin
|
from account.models import BonusProgramMixin
|
||||||
|
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
|
||||||
|
|
||||||
|
|
@ -94,7 +95,7 @@ class UserManager(models.Manager.from_queryset(UserQuerySet), _UserManager):
|
||||||
# First-time binding Telegram <-> User ?
|
# First-time binding Telegram <-> User ?
|
||||||
if freshly_created or user.tg_user_id is None:
|
if freshly_created or user.tg_user_id is None:
|
||||||
# Add bonus for Telegram login
|
# Add bonus for Telegram login
|
||||||
await sync_to_async(user.add_signup_bonus)()
|
await sync_to_async(BonusProgram.add_signup_bonus)(user)
|
||||||
|
|
||||||
# Create referral relationship
|
# Create referral relationship
|
||||||
# Only for fresh registration
|
# Only for fresh registration
|
||||||
|
|
|
||||||
0
core/__init__.py
Normal file
0
core/__init__.py
Normal file
13
core/admin.py
Normal file
13
core/admin.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import GlobalSettings, BonusProgramConfig
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(GlobalSettings)
|
||||||
|
class GlobalSettingsAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(BonusProgramConfig)
|
||||||
|
class BonusProgramConfigAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
6
core/apps.py
Normal file
6
core/apps.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'core'
|
||||||
45
core/migrations/0001_initial.py
Normal file
45
core/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Generated by Django 4.2.13 on 2024-05-23 22:08
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BonusProgramConfig',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('amount_signup', models.PositiveSmallIntegerField(default=150, verbose_name='Бонус за регистрацию')),
|
||||||
|
('amount_default_purchase', models.PositiveSmallIntegerField(default=50, verbose_name='Бонус за обычную покупку')),
|
||||||
|
('amount_referral', models.PositiveSmallIntegerField(default=500, verbose_name='Реферальный бонус')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Настройки бонусной программы',
|
||||||
|
'verbose_name_plural': 'Настройки бонусной программы',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GlobalSettings',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('yuan_rate', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Курс CNY/RUB')),
|
||||||
|
('yuan_rate_last_updated', models.DateTimeField(default=None, null=True, verbose_name='Дата обновления курса CNY/RUB')),
|
||||||
|
('yuan_rate_commission', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Наценка на курс юаня, руб')),
|
||||||
|
('delivery_price_CN', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Цена доставки по Китаю')),
|
||||||
|
('commission_rub', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Комиссия, руб')),
|
||||||
|
('pickup_address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Адрес пункта самовывоза')),
|
||||||
|
('time_to_buy', models.DurationField(default=datetime.timedelta(seconds=10800), help_text="Через N времени заказ переходит из статуса 'Новый' в 'Черновик'", verbose_name='Время на покупку')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Глобальные настройки',
|
||||||
|
'verbose_name_plural': 'Глобальные настройки',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
core/migrations/__init__.py
Normal file
0
core/migrations/__init__.py
Normal file
47
core/models.py
Normal file
47
core/models.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from core.utils import CachedSingleton
|
||||||
|
|
||||||
|
|
||||||
|
@CachedSingleton("global_settings")
|
||||||
|
class GlobalSettings(models.Model):
|
||||||
|
yuan_rate = models.DecimalField('Курс CNY/RUB', max_digits=10, decimal_places=2, default=0)
|
||||||
|
yuan_rate_last_updated = models.DateTimeField('Дата обновления курса CNY/RUB', null=True, default=None)
|
||||||
|
yuan_rate_commission = models.DecimalField('Наценка на курс юаня, руб', max_digits=10, decimal_places=2, default=0)
|
||||||
|
delivery_price_CN = models.DecimalField('Цена доставки по Китаю', max_digits=10, decimal_places=2, default=0)
|
||||||
|
commission_rub = models.DecimalField('Комиссия, руб', max_digits=10, decimal_places=2, default=0)
|
||||||
|
pickup_address = models.CharField('Адрес пункта самовывоза', max_length=200, blank=True, null=True)
|
||||||
|
time_to_buy = models.DurationField('Время на покупку',
|
||||||
|
help_text="Через N времени заказ переходит из статуса 'Новый' в 'Черновик'",
|
||||||
|
default=timedelta(hours=3))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Глобальные настройки'
|
||||||
|
verbose_name_plural = 'Глобальные настройки'
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f'GlobalSettings <{self.id}>'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_yuan_rate(self):
|
||||||
|
return self.yuan_rate + self.yuan_rate_commission
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = settings.BONUS_PROGRAM_DEFAULT_CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
@CachedSingleton("bonus_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'])
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Настройки бонусной программы'
|
||||||
|
verbose_name_plural = 'Настройки бонусной программы'
|
||||||
37
core/utils.py
Normal file
37
core/utils.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
|
||||||
|
class CachedSingleton:
|
||||||
|
def __init__(self, cache_key):
|
||||||
|
self._cache_key = cache_key
|
||||||
|
|
||||||
|
def __call__(self, cls):
|
||||||
|
def save(_self, *args, **kwargs):
|
||||||
|
# Store only one instance of model
|
||||||
|
_self.id = 1
|
||||||
|
cls.objects.exclude(id=_self.id).delete()
|
||||||
|
|
||||||
|
# Model's default save
|
||||||
|
_self._model_save(*args, **kwargs)
|
||||||
|
|
||||||
|
# Store model instance in cache
|
||||||
|
cache.set(self._cache_key, _self)
|
||||||
|
|
||||||
|
def load(_self) -> cls:
|
||||||
|
"""Load instance from cache or create new one in DB"""
|
||||||
|
obj = cache.get(self._cache_key)
|
||||||
|
|
||||||
|
if not obj:
|
||||||
|
obj, _ = cls.objects.get_or_create(id=1)
|
||||||
|
cache.set(self._cache_key, obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
# Save old Model.save() method first
|
||||||
|
setattr(cls, '_model_save', cls.save)
|
||||||
|
|
||||||
|
# Then, override it with decorator's one
|
||||||
|
setattr(cls, 'save', save)
|
||||||
|
|
||||||
|
# Set the singleton loading method
|
||||||
|
setattr(cls, 'load', classmethod(load))
|
||||||
|
return cls
|
||||||
3
core/views.py
Normal file
3
core/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
|
|
||||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||||
from rest_framework.views import exception_handler as drf_exception_handler
|
from rest_framework.views import exception_handler as drf_exception_handler
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def exception_handler(exc, context):
|
def exception_handler(exc, context):
|
||||||
""" Handle Django ValidationError as an accepted exception """
|
""" Handle Django ValidationError as an accepted exception """
|
||||||
|
logger.error(exc)
|
||||||
|
|
||||||
if isinstance(exc, DjangoValidationError):
|
if isinstance(exc, DjangoValidationError):
|
||||||
if hasattr(exc, 'message_dict'):
|
if hasattr(exc, 'message_dict'):
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,8 @@ INSTALLED_APPS = [
|
||||||
|
|
||||||
'account',
|
'account',
|
||||||
'store',
|
'store',
|
||||||
'tg_bot'
|
'tg_bot',
|
||||||
|
'core'
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|
@ -261,8 +262,9 @@ CELERY_RESULT_SERIALIZER = 'json'
|
||||||
CELERY_TIMEZONE = TIME_ZONE
|
CELERY_TIMEZONE = TIME_ZONE
|
||||||
|
|
||||||
# Bonus program
|
# Bonus program
|
||||||
# TODO: move to GlobalSettings?
|
BONUS_ELIGIBILITY_STATUS = 'completed'
|
||||||
BONUS_PROGRAM_CONFIG = {
|
|
||||||
|
BONUS_PROGRAM_DEFAULT_CONFIG = {
|
||||||
"amounts": {
|
"amounts": {
|
||||||
"signup": 150,
|
"signup": 150,
|
||||||
"default_purchase": 50,
|
"default_purchase": 50,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from django.contrib import admin
|
||||||
from django.contrib.admin import display
|
from django.contrib.admin import display
|
||||||
from mptt.admin import MPTTModelAdmin
|
from mptt.admin import MPTTModelAdmin
|
||||||
|
|
||||||
from .models import Category, Checklist, GlobalSettings, PaymentMethod, Promocode, Image, Gift
|
from .models import Category, Checklist, PaymentMethod, Promocode, Image, Gift
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Category)
|
@admin.register(Category)
|
||||||
|
|
@ -32,11 +32,6 @@ class ChecklistAdmin(admin.ModelAdmin):
|
||||||
return Checklist.objects.with_base_related()
|
return Checklist.objects.with_base_related()
|
||||||
|
|
||||||
|
|
||||||
@admin.register(GlobalSettings)
|
|
||||||
class GlobalSettingsAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(PaymentMethod)
|
@admin.register(PaymentMethod)
|
||||||
class PaymentMethodAdmin(admin.ModelAdmin):
|
class PaymentMethodAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'slug')
|
list_display = ('name', 'slug')
|
||||||
|
|
|
||||||
16
store/migrations/0005_delete_globalsettings.py
Normal file
16
store/migrations/0005_delete_globalsettings.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Generated by Django 4.2.13 on 2024-05-22 21:30
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('store', '0004_alter_checklist_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='GlobalSettings',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -8,7 +8,6 @@ from io import BytesIO
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
@ -21,50 +20,11 @@ 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 store.utils import create_preview
|
from store.utils import create_preview
|
||||||
|
|
||||||
|
|
||||||
class GlobalSettings(models.Model):
|
|
||||||
# currency
|
|
||||||
yuan_rate = models.DecimalField('Курс CNY/RUB', max_digits=10, decimal_places=2, default=0)
|
|
||||||
yuan_rate_last_updated = models.DateTimeField('Дата обновления курса CNY/RUB', null=True, default=None)
|
|
||||||
yuan_rate_commission = models.DecimalField('Наценка на курс юаня, руб', max_digits=10, decimal_places=2, default=0)
|
|
||||||
# Chinadelivery
|
|
||||||
delivery_price_CN = models.DecimalField('Цена доставки по Китаю', max_digits=10, decimal_places=2, default=0)
|
|
||||||
commission_rub = models.DecimalField('Комиссия, руб', max_digits=10, decimal_places=2, default=0)
|
|
||||||
pickup_address = models.CharField('Адрес пункта самовывоза', max_length=200, blank=True, null=True)
|
|
||||||
time_to_buy = models.DurationField('Время на покупку',
|
|
||||||
help_text="Через N времени заказ переходит из статуса 'Новый' в 'Черновик'",
|
|
||||||
default=timedelta(hours=3))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = 'Глобальные настройки'
|
|
||||||
verbose_name_plural = 'Глобальные настройки'
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
# Store only one instance of GlobalSettings
|
|
||||||
self.id = 1
|
|
||||||
self.__class__.objects.exclude(id=self.id).delete()
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
cache.set('global_settings', self)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f'GlobalSettings <{self.id}>'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def load(cls) -> 'GlobalSettings':
|
|
||||||
obj = cache.get('global_settings')
|
|
||||||
|
|
||||||
if not obj:
|
|
||||||
obj, _ = cls.objects.get_or_create(id=1)
|
|
||||||
cache.set('global_settings', obj)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
@property
|
|
||||||
def full_yuan_rate(self):
|
|
||||||
return self.yuan_rate + self.yuan_rate_commission
|
|
||||||
|
|
||||||
|
|
||||||
class Category(MPTTModel):
|
class Category(MPTTModel):
|
||||||
name = models.CharField('Название', max_length=20)
|
name = models.CharField('Название', max_length=20)
|
||||||
parent = TreeForeignKey('self', verbose_name='Родительская категория', on_delete=models.SET_NULL, blank=True, null=True, related_name='children', db_index=True)
|
parent = TreeForeignKey('self', verbose_name='Родительская категория', on_delete=models.SET_NULL, blank=True, null=True, related_name='children', db_index=True)
|
||||||
|
|
@ -290,7 +250,6 @@ class Checklist(models.Model):
|
||||||
CDEK_READY_STATUSES = (RUSSIA, SPLIT_PAID, CDEK)
|
CDEK_READY_STATUSES = (RUSSIA, SPLIT_PAID, CDEK)
|
||||||
|
|
||||||
CANCELLABLE_ORDER_STATUSES = (DRAFT, NEW, PAYMENT, BUYING)
|
CANCELLABLE_ORDER_STATUSES = (DRAFT, NEW, PAYMENT, BUYING)
|
||||||
BONUS_ORDER_STATUSES = (CHINA_RUSSIA, RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED)
|
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(DELETED, 'Удален'),
|
(DELETED, 'Удален'),
|
||||||
|
|
@ -643,7 +602,7 @@ class Checklist(models.Model):
|
||||||
if self.customer_id is None:
|
if self.customer_id is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.status != Checklist.Status.CHINA_RUSSIA:
|
if self.status != settings.BONUS_ELIGIBILITY_STATUS:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if any BonusProgramTransaction bound to current order exists
|
# Check if any BonusProgramTransaction bound to current order exists
|
||||||
|
|
@ -653,10 +612,10 @@ class Checklist(models.Model):
|
||||||
|
|
||||||
# Apply either referral bonus or order bonus, not both
|
# Apply either referral bonus or order bonus, not both
|
||||||
if self.customer.inviter is not None and self.customer.customer_orders.count() == 1:
|
if self.customer.inviter is not None and self.customer.customer_orders.count() == 1:
|
||||||
self.customer.add_referral_bonus(self, for_inviter=False)
|
BonusProgram.add_referral_bonus(self, for_inviter=False)
|
||||||
self.customer.inviter.add_referral_bonus(self, for_inviter=True)
|
BonusProgram.add_referral_bonus(self, for_inviter=True)
|
||||||
else:
|
else:
|
||||||
self.customer.add_order_bonus(self)
|
BonusProgram.add_order_bonus(self)
|
||||||
|
|
||||||
# TODO: split into sub-functions
|
# TODO: split into sub-functions
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@ 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 account.serializers import UserSerializer
|
from account.serializers import UserSerializer
|
||||||
from utils.exceptions import CRMException
|
from utils.exceptions import CRMException
|
||||||
from store.models import Checklist, GlobalSettings, Category, PaymentMethod, Promocode, Image, Gift
|
from store.models import Checklist, Category, PaymentMethod, Promocode, Image, Gift
|
||||||
|
from core.models import GlobalSettings
|
||||||
from store.utils import get_primary_key_related_model
|
from store.utils import get_primary_key_related_model
|
||||||
from poizonstore.utils import PriceField
|
from poizonstore.utils import PriceField
|
||||||
|
|
||||||
|
|
@ -146,7 +148,7 @@ class ChecklistSerializer(serializers.ModelSerializer):
|
||||||
self._create_main_images(instance, images.get('main_images'))
|
self._create_main_images(instance, images.get('main_images'))
|
||||||
|
|
||||||
if use_bonuses:
|
if use_bonuses:
|
||||||
instance.customer.spend_bonuses(order=instance)
|
BonusProgram.spend_bonuses(order=instance)
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ from django.utils import timezone
|
||||||
|
|
||||||
from external_api.cdek import client as cdek_client, CDEKStatus
|
from external_api.cdek import client as cdek_client, CDEKStatus
|
||||||
from external_api.currency import client as CurrencyAPIClient
|
from external_api.currency import client as CurrencyAPIClient
|
||||||
from .models import Checklist, GlobalSettings
|
from .models import Checklist
|
||||||
|
from core.models import GlobalSettings
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ from external_api.cdek import CDEKClient, CDEKWebhookTypes, CDEK_STATUS_TO_ORDER
|
||||||
from external_api.poizon import PoizonClient
|
from external_api.poizon import PoizonClient
|
||||||
from utils.exceptions import CRMException
|
from utils.exceptions import CRMException
|
||||||
from store.filters import GiftFilter, ChecklistFilter
|
from store.filters import GiftFilter, ChecklistFilter
|
||||||
from store.models import Checklist, GlobalSettings, Category, PaymentMethod, Promocode, Gift
|
from store.models import Checklist, Category, PaymentMethod, Promocode, Gift
|
||||||
|
from core.models import GlobalSettings
|
||||||
from store.serializers import (ChecklistSerializer, CategorySerializer, CategoryFullSerializer,
|
from store.serializers import (ChecklistSerializer, CategorySerializer, CategoryFullSerializer,
|
||||||
PaymentMethodSerializer, AnonymousGlobalSettingsSerializer, GlobalSettingsSerializer,
|
PaymentMethodSerializer, AnonymousGlobalSettingsSerializer, GlobalSettingsSerializer,
|
||||||
PromocodeSerializer, ClientUpdateChecklistSerializer, GiftSerializer,
|
PromocodeSerializer, ClientUpdateChecklistSerializer, GiftSerializer,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user