+ 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
55f2e0b02e
commit
e5c104bc11
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import logging
|
||||
from contextlib import suppress
|
||||
|
||||
from django.conf import settings
|
||||
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.functional import cached_property
|
||||
|
||||
from store.models import Checklist
|
||||
from core.models import BonusProgramConfig
|
||||
from tg_bot.messages import TGBonusMessage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -60,7 +59,7 @@ class BonusType:
|
|||
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}
|
||||
|
||||
|
||||
|
|
@ -171,30 +170,31 @@ class BonusProgramTransaction(models.Model):
|
|||
if self.id:
|
||||
qs = qs.exclude(id=self.id)
|
||||
|
||||
uniqueness_err = None
|
||||
bonus_name = BonusType.LOG_NAMES.get(self.type, self.type)
|
||||
|
||||
match self.type:
|
||||
case t if t in BonusType.ONE_TIME_TYPES:
|
||||
if qs.exists():
|
||||
uniqueness_err = f"User {self.user_id} already got {bonus_name} one-time bonus"
|
||||
if self.type in BonusType.ONE_TIME_TYPES:
|
||||
if qs.exists():
|
||||
raise ValidationError(f"User {self.user_id} already got {bonus_name} one-time bonus")
|
||||
|
||||
case t if t in BonusType.ORDER_TYPES:
|
||||
# Check that order is defined
|
||||
if self.order_id is None:
|
||||
raise ValidationError("Order is required for that type")
|
||||
if self.type in BonusType.ORDER_TYPES:
|
||||
# Check that order is defined
|
||||
if self.order_id is None:
|
||||
raise ValidationError("Order is required for that type")
|
||||
|
||||
# Check for duplicates for the same order
|
||||
already_exists = qs.filter(order_id=self.order_id).exists()
|
||||
if already_exists:
|
||||
uniqueness_err = 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)
|
||||
# Check for duplicates for the same order
|
||||
already_exists = qs.filter(order_id=self.order_id).exists()
|
||||
if already_exists:
|
||||
raise ValidationError(f"User {self.user_id} already got {bonus_name} bonus for order #{self.order_id}")
|
||||
|
||||
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:
|
||||
self._notify_user_about_new_transaction()
|
||||
|
|
@ -219,6 +219,8 @@ def generate_referral_code():
|
|||
|
||||
|
||||
class BonusProgramMixin(models.Model):
|
||||
"""BonusProgram fields for User model"""
|
||||
|
||||
balance = models.PositiveSmallIntegerField('Баланс, руб', default=0, editable=False)
|
||||
referral_code = models.CharField(max_length=settings.REFERRAL_CODE_LENGTH, default=generate_referral_code,
|
||||
editable=False)
|
||||
|
|
@ -249,44 +251,54 @@ class BonusProgramMixin(models.Model):
|
|||
self.balance = max(0, total_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:
|
||||
return
|
||||
|
||||
# Always use fresh balance
|
||||
self.recalculate_balance()
|
||||
order.customer.recalculate_balance()
|
||||
|
||||
# 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)
|
||||
|
||||
def add_signup_bonus(self):
|
||||
@staticmethod
|
||||
def add_signup_bonus(user: 'User'):
|
||||
bonus_type = BonusType.SIGNUP
|
||||
amount = self._get_bonus_amount("signup")
|
||||
amount = BonusProgramConfig.load().amount_signup
|
||||
|
||||
self.update_balance(amount, bonus_type)
|
||||
|
||||
def add_order_bonus(self, order):
|
||||
from store.models import Checklist
|
||||
user.update_balance(amount, bonus_type)
|
||||
|
||||
@staticmethod
|
||||
def add_order_bonus(order: 'Checklist'):
|
||||
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
|
||||
|
||||
self.update_balance(amount, bonus_type, order=order)
|
||||
|
||||
def add_referral_bonus(self, order: Checklist, for_inviter: bool):
|
||||
amount = self._get_bonus_amount("referral")
|
||||
@staticmethod
|
||||
def add_referral_bonus(order: 'Checklist', for_inviter: bool):
|
||||
amount = BonusProgramConfig.load().amount_referral
|
||||
|
||||
# Check if data is sufficient
|
||||
if order.customer_id is None or order.customer.inviter is None:
|
||||
return
|
||||
|
||||
# Check if eligible
|
||||
# Bonus is only for first purchase and only for orders that reached the CHINA_RUSSIA status
|
||||
if order.status != Checklist.Status.CHINA_RUSSIA or order.customer.customer_orders.count() != 1:
|
||||
# Bonus is only for first purchase and only for orders that reached the COMPLETED status
|
||||
if order.status != settings.BONUS_ELIGIBILITY_STATUS or order.customer.customer_orders.count() != 1:
|
||||
return
|
||||
|
||||
user = order.customer.inviter if for_inviter else order.customer
|
||||
|
|
@ -294,16 +306,3 @@ class BonusProgramMixin(models.Model):
|
|||
|
||||
# Add bonuses
|
||||
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 account.models import BonusProgramMixin
|
||||
from account.models.bonus import BonusProgram
|
||||
from store.utils import concat_not_null_values
|
||||
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 ?
|
||||
if freshly_created or user.tg_user_id is None:
|
||||
# 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
|
||||
# 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 rest_framework.exceptions import ValidationError as DRFValidationError
|
||||
from rest_framework.views import exception_handler as drf_exception_handler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def exception_handler(exc, context):
|
||||
""" Handle Django ValidationError as an accepted exception """
|
||||
logger.error(exc)
|
||||
|
||||
if isinstance(exc, DjangoValidationError):
|
||||
if hasattr(exc, 'message_dict'):
|
||||
|
|
|
|||
|
|
@ -104,7 +104,8 @@ INSTALLED_APPS = [
|
|||
|
||||
'account',
|
||||
'store',
|
||||
'tg_bot'
|
||||
'tg_bot',
|
||||
'core'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
|
@ -261,8 +262,9 @@ CELERY_RESULT_SERIALIZER = 'json'
|
|||
CELERY_TIMEZONE = TIME_ZONE
|
||||
|
||||
# Bonus program
|
||||
# TODO: move to GlobalSettings?
|
||||
BONUS_PROGRAM_CONFIG = {
|
||||
BONUS_ELIGIBILITY_STATUS = 'completed'
|
||||
|
||||
BONUS_PROGRAM_DEFAULT_CONFIG = {
|
||||
"amounts": {
|
||||
"signup": 150,
|
||||
"default_purchase": 50,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from django.contrib import admin
|
|||
from django.contrib.admin import display
|
||||
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)
|
||||
|
|
@ -32,11 +32,6 @@ class ChecklistAdmin(admin.ModelAdmin):
|
|||
return Checklist.objects.with_base_related()
|
||||
|
||||
|
||||
@admin.register(GlobalSettings)
|
||||
class GlobalSettingsAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(PaymentMethod)
|
||||
class PaymentMethodAdmin(admin.ModelAdmin):
|
||||
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 django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.db import models
|
||||
|
|
@ -21,50 +20,11 @@ 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
|
||||
|
||||
|
||||
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):
|
||||
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)
|
||||
|
|
@ -290,7 +250,6 @@ class Checklist(models.Model):
|
|||
CDEK_READY_STATUSES = (RUSSIA, SPLIT_PAID, CDEK)
|
||||
|
||||
CANCELLABLE_ORDER_STATUSES = (DRAFT, NEW, PAYMENT, BUYING)
|
||||
BONUS_ORDER_STATUSES = (CHINA_RUSSIA, RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED)
|
||||
|
||||
CHOICES = (
|
||||
(DELETED, 'Удален'),
|
||||
|
|
@ -643,7 +602,7 @@ class Checklist(models.Model):
|
|||
if self.customer_id is None:
|
||||
return
|
||||
|
||||
if self.status != Checklist.Status.CHINA_RUSSIA:
|
||||
if self.status != settings.BONUS_ELIGIBILITY_STATUS:
|
||||
return
|
||||
|
||||
# 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
|
||||
if self.customer.inviter is not None and self.customer.customer_orders.count() == 1:
|
||||
self.customer.add_referral_bonus(self, for_inviter=False)
|
||||
self.customer.inviter.add_referral_bonus(self, for_inviter=True)
|
||||
BonusProgram.add_referral_bonus(self, for_inviter=False)
|
||||
BonusProgram.add_referral_bonus(self, for_inviter=True)
|
||||
else:
|
||||
self.customer.add_order_bonus(self)
|
||||
BonusProgram.add_order_bonus(self)
|
||||
|
||||
# TODO: split into sub-functions
|
||||
def save(self, *args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ 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 account.serializers import UserSerializer
|
||||
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 poizonstore.utils import PriceField
|
||||
|
||||
|
|
@ -146,7 +148,7 @@ class ChecklistSerializer(serializers.ModelSerializer):
|
|||
self._create_main_images(instance, images.get('main_images'))
|
||||
|
||||
if use_bonuses:
|
||||
instance.customer.spend_bonuses(order=instance)
|
||||
BonusProgram.spend_bonuses(order=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.currency import client as CurrencyAPIClient
|
||||
from .models import Checklist, GlobalSettings
|
||||
from .models import Checklist
|
||||
from core.models import GlobalSettings
|
||||
|
||||
|
||||
@shared_task
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ from external_api.cdek import CDEKClient, CDEKWebhookTypes, CDEK_STATUS_TO_ORDER
|
|||
from external_api.poizon import PoizonClient
|
||||
from utils.exceptions import CRMException
|
||||
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,
|
||||
PaymentMethodSerializer, AnonymousGlobalSettingsSerializer, GlobalSettingsSerializer,
|
||||
PromocodeSerializer, ClientUpdateChecklistSerializer, GiftSerializer,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user