Compare commits
5 Commits
12ca631d15
...
d0d637051d
| Author | SHA1 | Date | |
|---|---|---|---|
| d0d637051d | |||
| 4a987b9646 | |||
| 4a821faee9 | |||
| 81a3e15418 | |||
| 1f4d693c81 |
|
|
@ -17,5 +17,5 @@ class BonusProgramTransactionAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
def delete_queryset(self, request, queryset):
|
def delete_queryset(self, request, queryset):
|
||||||
for obj in queryset:
|
for obj in queryset:
|
||||||
obj.cancel_transaction()
|
obj.cancel()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.2.2 on 2024-05-20 17:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0016_alter_user_role'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bonusprogramtransaction',
|
||||||
|
name='type',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(0, 'Другое начисление'), (1, 'Бонус за регистрацию'), (2, 'Бонус за покупку'), (3, 'Бонус за первую покупку приглашенного'), (4, 'Бонус за первую покупку'), (10, 'Другое списание'), (11, 'Списание бонусов за заказ')], verbose_name='Тип транзакции'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -2,11 +2,13 @@ import logging
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.crypto import get_random_string
|
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 store.models import Checklist
|
from store.models import Checklist
|
||||||
from tg_bot.messages import TGBonusMessage
|
from tg_bot.messages import TGBonusMessage
|
||||||
|
|
@ -36,12 +38,6 @@ class BonusType:
|
||||||
# Клиент потратил баллы на заказ
|
# Клиент потратил баллы на заказ
|
||||||
SPENT_PURCHASE = 11
|
SPENT_PURCHASE = 11
|
||||||
|
|
||||||
# Отмена начисления
|
|
||||||
CANCELLED_DEPOSIT = 20
|
|
||||||
|
|
||||||
# Отмена списания
|
|
||||||
CANCELLED_WITHDRAWAL = 21
|
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(OTHER_DEPOSIT, 'Другое начисление'),
|
(OTHER_DEPOSIT, 'Другое начисление'),
|
||||||
(SIGNUP, 'Бонус за регистрацию'),
|
(SIGNUP, 'Бонус за регистрацию'),
|
||||||
|
|
@ -51,16 +47,31 @@ class BonusType:
|
||||||
|
|
||||||
(OTHER_WITHDRAWAL, 'Другое списание'),
|
(OTHER_WITHDRAWAL, 'Другое списание'),
|
||||||
(SPENT_PURCHASE, 'Списание бонусов за заказ'),
|
(SPENT_PURCHASE, 'Списание бонусов за заказ'),
|
||||||
|
|
||||||
(CANCELLED_DEPOSIT, 'Отмена начисления'),
|
|
||||||
(CANCELLED_WITHDRAWAL, 'Отмена списания'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LOG_NAMES = {
|
||||||
|
OTHER_DEPOSIT: 'OTHER_DEPOSIT',
|
||||||
|
SIGNUP: 'SIGNUP',
|
||||||
|
DEFAULT_PURCHASE: 'DEFAULT_PURCHASE',
|
||||||
|
FOR_INVITER: 'FOR_INVITER',
|
||||||
|
INVITED_FIRST_PURCHASE: 'INVITED_FIRST_PURCHASE',
|
||||||
|
|
||||||
|
OTHER_WITHDRAWAL: 'OTHER_WITHDRAWAL',
|
||||||
|
SPENT_PURCHASE: 'SPENT_PURCHASE',
|
||||||
|
}
|
||||||
|
|
||||||
|
ONE_TIME_TYPES = {SIGNUP}
|
||||||
|
ORDER_TYPES = {DEFAULT_PURCHASE, FOR_INVITER, INVITED_FIRST_PURCHASE, SPENT_PURCHASE}
|
||||||
|
|
||||||
|
|
||||||
class BonusProgramTransactionQuerySet(models.QuerySet):
|
class BonusProgramTransactionQuerySet(models.QuerySet):
|
||||||
# TODO: optimize queries
|
# TODO: optimize queries
|
||||||
def with_base_related(self):
|
def with_base_related(self):
|
||||||
return self
|
return self.select_related('order')
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
for t in self:
|
||||||
|
t.cancel()
|
||||||
|
|
||||||
|
|
||||||
class BonusProgramTransaction(models.Model):
|
class BonusProgramTransaction(models.Model):
|
||||||
|
|
@ -99,11 +110,11 @@ class BonusProgramTransaction(models.Model):
|
||||||
case BonusType.INVITED_FIRST_PURCHASE:
|
case BonusType.INVITED_FIRST_PURCHASE:
|
||||||
msg = TGBonusMessage.INVITED_FIRST_PURCHASE.format(amount=self.amount, order_id=self.order.id)
|
msg = TGBonusMessage.INVITED_FIRST_PURCHASE.format(amount=self.amount, order_id=self.order.id)
|
||||||
|
|
||||||
case BonusType.OTHER_DEPOSIT | BonusType.CANCELLED_DEPOSIT:
|
case BonusType.OTHER_DEPOSIT:
|
||||||
comment = self.comment or "—"
|
comment = self.comment or "—"
|
||||||
msg = TGBonusMessage.OTHER_DEPOSIT.format(amount=self.amount, comment=comment)
|
msg = TGBonusMessage.OTHER_DEPOSIT.format(amount=self.amount, comment=comment)
|
||||||
|
|
||||||
case BonusType.OTHER_WITHDRAWAL | BonusType.CANCELLED_WITHDRAWAL:
|
case BonusType.OTHER_WITHDRAWAL:
|
||||||
comment = self.comment or "—"
|
comment = self.comment or "—"
|
||||||
msg = TGBonusMessage.OTHER_WITHDRAWAL.format(amount=abs(self.amount), comment=comment)
|
msg = TGBonusMessage.OTHER_WITHDRAWAL.format(amount=abs(self.amount), comment=comment)
|
||||||
|
|
||||||
|
|
@ -116,10 +127,9 @@ class BonusProgramTransaction(models.Model):
|
||||||
if msg is not None:
|
if msg is not None:
|
||||||
self.user.notify_tg_bot(msg)
|
self.user.notify_tg_bot(msg)
|
||||||
|
|
||||||
def cancel_transaction(self):
|
def cancel(self):
|
||||||
# Skip already cancelled transactions
|
# Skip transactions that refers to cancelled ones
|
||||||
# TODO: if reverse transaction is being deleted, revert the source one?
|
if self.was_cancelled:
|
||||||
if self.was_cancelled or self.type in (BonusType.OTHER_WITHDRAWAL, BonusType.OTHER_DEPOSIT):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
date_formatted = localize(timezone.localtime(self.date))
|
date_formatted = localize(timezone.localtime(self.date))
|
||||||
|
|
@ -134,13 +144,14 @@ class BonusProgramTransaction(models.Model):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create reverse transaction, user's balance will be recalculated in post_save signal
|
# Create reverse transaction, user's balance will be recalculated in post_save signal
|
||||||
transaction = BonusProgramTransaction()
|
transaction = BonusProgramTransaction(
|
||||||
|
user_id=self.user_id,
|
||||||
transaction.user_id = self.user_id
|
type=bonus_type,
|
||||||
transaction.type = bonus_type
|
amount=self.amount * -1,
|
||||||
transaction.amount = self.amount * -1
|
comment=comment,
|
||||||
transaction.comment = comment
|
order=self.order,
|
||||||
transaction.order = self.order
|
was_cancelled=True
|
||||||
|
)
|
||||||
transaction.save()
|
transaction.save()
|
||||||
|
|
||||||
self.was_cancelled = True
|
self.was_cancelled = True
|
||||||
|
|
@ -148,9 +159,43 @@ class BonusProgramTransaction(models.Model):
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
# Don't delete transaction, cancel it instead
|
# Don't delete transaction, cancel it instead
|
||||||
self.cancel_transaction()
|
self.cancel()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
# No underflow or dummy transactions allowed. Fail loudly
|
||||||
|
if self.amount == 0 or (self.user.balance + self.amount) < 0:
|
||||||
|
raise ValidationError("No underflow or dummy transactions allowed")
|
||||||
|
|
||||||
|
# Check for uniqueness for given user
|
||||||
|
qs = self.user.bonus_history.filter(type=self.type)
|
||||||
|
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"
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
self.full_clean()
|
||||||
|
|
||||||
if self.id is None:
|
if self.id is None:
|
||||||
self._notify_user_about_new_transaction()
|
self._notify_user_about_new_transaction()
|
||||||
|
|
||||||
|
|
@ -181,12 +226,12 @@ class BonusProgramMixin(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def bonus_history(self):
|
def bonus_history(self):
|
||||||
return BonusProgramTransaction.objects.filter(user_id=self.id)
|
return BonusProgramTransaction.objects.filter(user_id=self.id)
|
||||||
|
|
||||||
def update_balance(self, amount, bonus_type, comment=None, order=None):
|
def update_balance(self, amount, bonus_type, comment=None, order=None):
|
||||||
# No underflow or dummy transactions allowed
|
# No underflow or dummy transactions allowed, fail silently
|
||||||
if amount == 0 or (self.balance + amount) < 0:
|
if amount == 0 or (self.balance + amount) < 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -195,27 +240,30 @@ class BonusProgramMixin(models.Model):
|
||||||
amount=amount, type=bonus_type,
|
amount=amount, type=bonus_type,
|
||||||
comment=comment, order=order)
|
comment=comment, order=order)
|
||||||
transaction.save()
|
transaction.save()
|
||||||
|
self.refresh_from_db(fields=['balance'])
|
||||||
|
|
||||||
def recalculate_balance(self):
|
def recalculate_balance(self):
|
||||||
# TODO: use this method when checking the available balance upon order creation
|
total_balance = self.bonus_history \
|
||||||
total_balance = BonusProgramTransaction.objects \
|
.aggregate(total_balance=Sum('amount'))['total_balance'] or 0
|
||||||
.filter(user_id=self.id) \
|
|
||||||
.aggregate(total_balance=Sum('amount'))['total_balance'] or 0
|
|
||||||
|
|
||||||
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):
|
||||||
|
if order is None or order.customer_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Always use fresh balance
|
||||||
|
self.recalculate_balance()
|
||||||
|
|
||||||
|
# Spend full_price bonuses or nothing
|
||||||
|
to_spend = min(self.balance, order.full_price)
|
||||||
|
order.customer.update_balance(-to_spend, BonusType.SPENT_PURCHASE, order=order)
|
||||||
|
|
||||||
def add_signup_bonus(self):
|
def add_signup_bonus(self):
|
||||||
bonus_type = BonusType.SIGNUP
|
bonus_type = BonusType.SIGNUP
|
||||||
amount = self._get_bonus_amount("signup")
|
amount = self._get_bonus_amount("signup")
|
||||||
|
|
||||||
already_exists = (BonusProgramTransaction.objects
|
|
||||||
.filter(user_id=self.id, type=bonus_type)
|
|
||||||
.exists())
|
|
||||||
if already_exists:
|
|
||||||
self._log(logging.INFO, "User already had signup bonus")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.update_balance(amount, bonus_type)
|
self.update_balance(amount, bonus_type)
|
||||||
|
|
||||||
def add_order_bonus(self, order):
|
def add_order_bonus(self, order):
|
||||||
|
|
@ -224,15 +272,7 @@ class BonusProgramMixin(models.Model):
|
||||||
bonus_type = BonusType.DEFAULT_PURCHASE
|
bonus_type = BonusType.DEFAULT_PURCHASE
|
||||||
amount = self._get_bonus_amount("default_purchase")
|
amount = self._get_bonus_amount("default_purchase")
|
||||||
|
|
||||||
if order.status != Checklist.Status.CHINA_RUSSIA:
|
if order.status not in Checklist.Status.BONUS_ORDER_STATUSES:
|
||||||
return
|
|
||||||
|
|
||||||
already_exists = (BonusProgramTransaction.objects
|
|
||||||
.filter(user_id=self.id, type=bonus_type, order_id=order.id)
|
|
||||||
.exists())
|
|
||||||
|
|
||||||
if already_exists:
|
|
||||||
self._log(logging.INFO, f"User already got bonus for order #{order.id}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self.update_balance(amount, bonus_type, order=order)
|
self.update_balance(amount, bonus_type, order=order)
|
||||||
|
|
@ -252,15 +292,6 @@ class BonusProgramMixin(models.Model):
|
||||||
user = order.customer.inviter if for_inviter else order.customer
|
user = order.customer.inviter if for_inviter else order.customer
|
||||||
bonus_type = BonusType.FOR_INVITER if for_inviter else BonusType.INVITED_FIRST_PURCHASE
|
bonus_type = BonusType.FOR_INVITER if for_inviter else BonusType.INVITED_FIRST_PURCHASE
|
||||||
|
|
||||||
# Check if user didn't receive bonus yet
|
|
||||||
already_exists = (BonusProgramTransaction.objects
|
|
||||||
.filter(user_id=user.id, type=bonus_type, order_id=order.id)
|
|
||||||
.exists())
|
|
||||||
|
|
||||||
if already_exists:
|
|
||||||
self._log(logging.INFO, f"User already got referral bonus for order #{order.id}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Add bonuses
|
# Add bonuses
|
||||||
user.update_balance(amount, bonus_type, order=order)
|
user.update_balance(amount, bonus_type, order=order)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ class UserManager(models.Manager.from_queryset(UserQuerySet), _UserManager):
|
||||||
logger.warning(f"User #{inviter.id} already invited user #{user_id}")
|
logger.warning(f"User #{inviter.id} already invited user #{user_id}")
|
||||||
return
|
return
|
||||||
|
|
||||||
async def bind_tg_user(self, tg_user_id, phone, referral_code=None):
|
async def bind_tg_user(self, tg_user_id, phone, referral_code=None) -> bool:
|
||||||
# Normalize phone: 79111234567 -> +79111234567
|
# Normalize phone: 79111234567 -> +79111234567
|
||||||
phone = PhoneNumber.from_string(phone).as_e164
|
phone = PhoneNumber.from_string(phone).as_e164
|
||||||
|
|
||||||
|
|
@ -109,6 +109,8 @@ class UserManager(models.Manager.from_queryset(UserQuerySet), _UserManager):
|
||||||
|
|
||||||
logger.info(f"tgbot: Telegram user #{tg_user_id} was bound to user #{user.id}")
|
logger.info(f"tgbot: Telegram user #{tg_user_id} was bound to user #{user.id}")
|
||||||
|
|
||||||
|
return freshly_created
|
||||||
|
|
||||||
|
|
||||||
class User(BonusProgramMixin, AbstractUser):
|
class User(BonusProgramMixin, AbstractUser):
|
||||||
ADMIN = "admin"
|
ADMIN = "admin"
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,12 @@ class UserSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class BonusProgramTransactionSerializer(serializers.ModelSerializer):
|
class BonusProgramTransactionSerializer(serializers.ModelSerializer):
|
||||||
|
order_id = serializers.StringRelatedField(source='order.id', allow_null=True)
|
||||||
type = serializers.CharField(source='get_type_display')
|
type = serializers.CharField(source='get_type_display')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BonusProgramTransaction
|
model = BonusProgramTransaction
|
||||||
fields = ('id', 'type', 'date', 'amount', 'comment', 'was_cancelled')
|
fields = ('id', 'type', 'date', 'amount', 'order_id', 'comment', 'was_cancelled')
|
||||||
|
|
||||||
|
|
||||||
def non_zero_validator(value):
|
def non_zero_validator(value):
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,9 @@ from account.serializers import SetInitialPasswordSerializer, BonusProgramTransa
|
||||||
UserBalanceUpdateSerializer, TelegramCallbackSerializer
|
UserBalanceUpdateSerializer, TelegramCallbackSerializer
|
||||||
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
|
||||||
from tg_bot.bot import bot_sync
|
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(djoser_views.UserViewSet):
|
class UserViewSet(djoser_views.UserViewSet):
|
||||||
""" Replacement for Djoser's UserViewSet """
|
|
||||||
|
|
||||||
def permission_denied(self, request, **kwargs):
|
def permission_denied(self, request, **kwargs):
|
||||||
if (
|
if (
|
||||||
djoser_settings.HIDE_USERS
|
djoser_settings.HIDE_USERS
|
||||||
|
|
@ -147,7 +144,7 @@ class TelegramLoginForm(views.APIView):
|
||||||
# Sign up user
|
# Sign up user
|
||||||
user = User.objects.create_draft_user(tg_user_id=tg_user_id)
|
user = User.objects.create_draft_user(tg_user_id=tg_user_id)
|
||||||
# Request the phone through the bot
|
# Request the phone through the bot
|
||||||
request_phone_sync(tg_user_id, TGCoreMessage.SIGN_UP_SHARE_PHONE)
|
request_phone_sync(tg_user_id, TGCoreMessage.TG_AUTH_SHARE_PHONE)
|
||||||
|
|
||||||
token = login_user(request, user)
|
token = login_user(request, user)
|
||||||
return Response({"auth_token": token.key})
|
return Response({"auth_token": token.key})
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@ TG_BOT_TOKEN = get_secret("TG_BOT_TOKEN")
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = bool(int(os.environ.get("DJANGO_DEBUG") or 0))
|
DEBUG = bool(int(os.environ.get("DJANGO_DEBUG") or 0))
|
||||||
DISABLE_PERMISSIONS = False
|
|
||||||
DISABLE_CORS = True
|
DISABLE_CORS = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = get_secret('ALLOWED_HOSTS').split(',')
|
ALLOWED_HOSTS = get_secret('ALLOWED_HOSTS').split(',')
|
||||||
|
|
@ -175,9 +174,6 @@ REST_FRAMEWORK = {
|
||||||
# or allow read-only access for unauthenticated users.
|
# or allow read-only access for unauthenticated users.
|
||||||
'DEFAULT_PERMISSION_CLASSES': [
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
'rest_framework.permissions.IsAuthenticated'
|
'rest_framework.permissions.IsAuthenticated'
|
||||||
if not DISABLE_PERMISSIONS
|
|
||||||
else
|
|
||||||
'rest_framework.permissions.AllowAny'
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.TokenAuthentication'],
|
'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.TokenAuthentication'],
|
||||||
|
|
|
||||||
6
poizonstore/utils.py
Normal file
6
poizonstore/utils.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
from rest_framework.fields import DecimalField
|
||||||
|
|
||||||
|
|
||||||
|
class PriceField(DecimalField):
|
||||||
|
def __init__(self, *args, max_digits=10, decimal_places=2, min_value=0, **kwargs):
|
||||||
|
super().__init__(*args, max_digits=max_digits, decimal_places=decimal_places, min_value=min_value, **kwargs)
|
||||||
18
store/migrations/0004_alter_checklist_status.py
Normal file
18
store/migrations/0004_alter_checklist_status.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.2.2 on 2024-05-20 17:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('store', '0003_remove_checklist_buyer_name_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='checklist',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('deleted', 'Удален'), ('draft', 'Черновик'), ('neworder', 'Новый заказ'), ('payment', 'Проверка оплаты'), ('buying', 'На закупке'), ('bought', 'Закуплен'), ('china', 'На складе в Китае'), ('chinarush', 'Доставка на склад РФ'), ('rush', 'На складе в РФ'), ('split_waiting', 'Сплит: ожидание оплаты 2й части'), ('split_paid', 'Сплит: полностью оплачено'), ('cdek', 'Доставляется СДЭК'), ('completed', 'Завершен')], default='neworder', max_length=15, verbose_name='Статус заказа'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -14,6 +14,7 @@ from django.db import models
|
||||||
from django.db.models import F, Case, When, DecimalField, Prefetch, Max, Q
|
from django.db.models import F, Case, When, DecimalField, Prefetch, Max, Q
|
||||||
from django.db.models.functions import Ceil
|
from django.db.models.functions import Ceil
|
||||||
from django.db.models.lookups import GreaterThan
|
from django.db.models.lookups import GreaterThan
|
||||||
|
from django.db.transaction import atomic
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django_cleanup import cleanup
|
from django_cleanup import cleanup
|
||||||
from mptt.fields import TreeForeignKey
|
from mptt.fields import TreeForeignKey
|
||||||
|
|
@ -251,6 +252,7 @@ class PriceSnapshot(models.Model):
|
||||||
class Checklist(models.Model):
|
class Checklist(models.Model):
|
||||||
# Statuses
|
# Statuses
|
||||||
class Status:
|
class Status:
|
||||||
|
DELETED = "deleted"
|
||||||
DRAFT = "draft"
|
DRAFT = "draft"
|
||||||
NEW = "neworder"
|
NEW = "neworder"
|
||||||
PAYMENT = "payment"
|
PAYMENT = "payment"
|
||||||
|
|
@ -267,7 +269,11 @@ class Checklist(models.Model):
|
||||||
PDF_AVAILABLE_STATUSES = (RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED)
|
PDF_AVAILABLE_STATUSES = (RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED)
|
||||||
CDEK_READY_STATUSES = (RUSSIA, SPLIT_PAID, CDEK)
|
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 = (
|
CHOICES = (
|
||||||
|
(DELETED, 'Удален'),
|
||||||
(DRAFT, 'Черновик'),
|
(DRAFT, 'Черновик'),
|
||||||
(NEW, 'Новый заказ'),
|
(NEW, 'Новый заказ'),
|
||||||
(PAYMENT, 'Проверка оплаты'),
|
(PAYMENT, 'Проверка оплаты'),
|
||||||
|
|
@ -506,6 +512,7 @@ class Checklist(models.Model):
|
||||||
# Default commission
|
# Default commission
|
||||||
commission = GlobalSettings.load().commission_rub
|
commission = GlobalSettings.load().commission_rub
|
||||||
|
|
||||||
|
# For big orders there is an additional commission
|
||||||
if self.price_rub > 150_000:
|
if self.price_rub > 150_000:
|
||||||
commission = max(commission, self.price_rub * Decimal(settings.COMMISSION_OVER_150K))
|
commission = max(commission, self.price_rub * Decimal(settings.COMMISSION_OVER_150K))
|
||||||
|
|
||||||
|
|
@ -516,6 +523,32 @@ class Checklist(models.Model):
|
||||||
|
|
||||||
return commission
|
return commission
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bonus_used(self):
|
||||||
|
if self.customer_id is None:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# It is guaranteed that there is only one SPENT_PURCHASE for this order
|
||||||
|
transaction = self.customer.bonus_history.filter(order_id=self.id).first()
|
||||||
|
return abs(transaction.amount) if transaction else 0
|
||||||
|
|
||||||
|
@atomic()
|
||||||
|
def cancel(self):
|
||||||
|
""" Cancel the order and return all bonuses """
|
||||||
|
|
||||||
|
# Don't delete orders entirely, just change the status
|
||||||
|
if self.status == Checklist.Status.DRAFT:
|
||||||
|
self.status = Checklist.Status.DELETED
|
||||||
|
else:
|
||||||
|
self.status = Checklist.Status.DRAFT
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
if self.customer_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.customer.bonus_history.filter(order_id=self.id, was_cancelled=False).cancel()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def preview_image(self):
|
def preview_image(self):
|
||||||
# Prefer annotated field
|
# Prefer annotated field
|
||||||
|
|
@ -589,6 +622,7 @@ class Checklist(models.Model):
|
||||||
if tg_message:
|
if tg_message:
|
||||||
self.customer.notify_tg_bot(tg_message)
|
self.customer.notify_tg_bot(tg_message)
|
||||||
|
|
||||||
|
@atomic
|
||||||
def _check_eligible_for_order_bonus(self):
|
def _check_eligible_for_order_bonus(self):
|
||||||
if self.customer_id is None:
|
if self.customer_id is None:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
|
@ -5,6 +6,7 @@ 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, GlobalSettings, Category, PaymentMethod, Promocode, Image, Gift
|
||||||
from store.utils import get_primary_key_related_model
|
from store.utils import get_primary_key_related_model
|
||||||
|
from poizonstore.utils import PriceField
|
||||||
|
|
||||||
|
|
||||||
class ImageSerializer(serializers.ModelSerializer):
|
class ImageSerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -35,7 +37,7 @@ class CategoryChecklistSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class CategorySerializer(serializers.ModelSerializer):
|
class CategorySerializer(serializers.ModelSerializer):
|
||||||
chinarush = serializers.DecimalField(source='delivery_price_CN_RU', max_digits=10, decimal_places=2)
|
chinarush = PriceField(source='delivery_price_CN_RU')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Category
|
model = Category
|
||||||
|
|
@ -55,6 +57,7 @@ class CategoryFullSerializer(CategorySerializer):
|
||||||
|
|
||||||
class GiftSerializer(serializers.ModelSerializer):
|
class GiftSerializer(serializers.ModelSerializer):
|
||||||
image = Base64ImageField(required=False, allow_null=True)
|
image = Base64ImageField(required=False, allow_null=True)
|
||||||
|
min_price = PriceField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Gift
|
model = Gift
|
||||||
|
|
@ -75,19 +78,20 @@ class ChecklistSerializer(serializers.ModelSerializer):
|
||||||
promocode = serializers.SlugRelatedField(slug_field='name', queryset=Promocode.objects.active(),
|
promocode = serializers.SlugRelatedField(slug_field='name', queryset=Promocode.objects.active(),
|
||||||
required=False, allow_null=True)
|
required=False, allow_null=True)
|
||||||
|
|
||||||
|
use_bonuses = serializers.BooleanField(write_only=True, default=False)
|
||||||
gift = get_primary_key_related_model(GiftSerializer, required=False, allow_null=True)
|
gift = get_primary_key_related_model(GiftSerializer, required=False, allow_null=True)
|
||||||
|
|
||||||
yuan_rate = serializers.DecimalField(read_only=True, max_digits=10, decimal_places=2)
|
yuan_rate = PriceField(read_only=True)
|
||||||
price_yuan = serializers.DecimalField(required=False, max_digits=10, decimal_places=2)
|
price_yuan = PriceField(required=False)
|
||||||
price_rub = serializers.IntegerField(read_only=True)
|
price_rub = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
delivery_price_CN = serializers.DecimalField(read_only=True, max_digits=10, decimal_places=2)
|
delivery_price_CN = PriceField(read_only=True)
|
||||||
delivery_price_CN_RU = serializers.DecimalField(read_only=True, max_digits=10, decimal_places=2)
|
delivery_price_CN_RU = PriceField(read_only=True)
|
||||||
|
|
||||||
full_price = serializers.IntegerField(read_only=True)
|
full_price = PriceField(read_only=True)
|
||||||
real_price = serializers.DecimalField(required=False, allow_null=True, max_digits=10, decimal_places=2)
|
real_price = PriceField(required=False, allow_null=True)
|
||||||
|
|
||||||
commission_rub = serializers.DecimalField(read_only=True, max_digits=10, decimal_places=2)
|
commission_rub = PriceField(read_only=True)
|
||||||
|
|
||||||
customer = get_primary_key_related_model(UserSerializer, required=False, allow_null=True)
|
customer = get_primary_key_related_model(UserSerializer, required=False, allow_null=True)
|
||||||
|
|
||||||
|
|
@ -126,6 +130,7 @@ class ChecklistSerializer(serializers.ModelSerializer):
|
||||||
instance.images.set(img_objs)
|
instance.images.set(img_objs)
|
||||||
instance.generate_preview(next(iter(img_objs), None))
|
instance.generate_preview(next(iter(img_objs), None))
|
||||||
|
|
||||||
|
@atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
images = self._collect_images_by_fields(validated_data)
|
images = self._collect_images_by_fields(validated_data)
|
||||||
|
|
||||||
|
|
@ -135,8 +140,14 @@ class ChecklistSerializer(serializers.ModelSerializer):
|
||||||
if not user.is_manager or validated_data.get('customer') is None:
|
if not user.is_manager or validated_data.get('customer') is None:
|
||||||
validated_data['customer'] = user
|
validated_data['customer'] = user
|
||||||
|
|
||||||
|
use_bonuses = validated_data.pop('use_bonuses')
|
||||||
|
|
||||||
instance = super().create(validated_data)
|
instance = super().create(validated_data)
|
||||||
self._create_main_images(instance, images.get('main_images'))
|
self._create_main_images(instance, images.get('main_images'))
|
||||||
|
|
||||||
|
if use_bonuses:
|
||||||
|
instance.customer.spend_bonuses(order=instance)
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
|
|
@ -170,7 +181,7 @@ class ChecklistSerializer(serializers.ModelSerializer):
|
||||||
'image',
|
'image',
|
||||||
'preview_image_url',
|
'preview_image_url',
|
||||||
'yuan_rate', 'price_yuan', 'price_rub', 'delivery_price_CN', 'delivery_price_CN_RU', 'commission_rub',
|
'yuan_rate', 'price_yuan', 'price_rub', 'delivery_price_CN', 'delivery_price_CN_RU', 'commission_rub',
|
||||||
'promocode', 'gift',
|
'promocode', 'gift', 'use_bonuses', 'bonus_used',
|
||||||
'comment',
|
'comment',
|
||||||
'full_price', 'real_price',
|
'full_price', 'real_price',
|
||||||
'customer',
|
'customer',
|
||||||
|
|
@ -203,7 +214,7 @@ class ClientCreateChecklistSerializer(ClientChecklistSerializerMixin, ChecklistS
|
||||||
writable_fields = {
|
writable_fields = {
|
||||||
'link',
|
'link',
|
||||||
'brand', 'model', 'size', 'category',
|
'brand', 'model', 'size', 'category',
|
||||||
'price_yuan',
|
'price_yuan', 'use_bonuses',
|
||||||
'comment',
|
'comment',
|
||||||
}
|
}
|
||||||
read_only_fields = tuple(set(ChecklistSerializer.Meta.fields) - writable_fields)
|
read_only_fields = tuple(set(ChecklistSerializer.Meta.fields) - writable_fields)
|
||||||
|
|
@ -228,15 +239,16 @@ class ClientUpdateChecklistSerializer(ClientChecklistSerializerMixin, ChecklistS
|
||||||
|
|
||||||
|
|
||||||
class GlobalSettingsSerializer(serializers.ModelSerializer):
|
class GlobalSettingsSerializer(serializers.ModelSerializer):
|
||||||
currency = serializers.DecimalField(source='full_yuan_rate', read_only=True, max_digits=10, decimal_places=2)
|
yuan_rate = PriceField(source='full_yuan_rate', read_only=True)
|
||||||
yuan_rate_last_updated = serializers.DateTimeField(read_only=True)
|
yuan_rate_commission = PriceField()
|
||||||
chinadelivery = serializers.DecimalField(source='delivery_price_CN', max_digits=10, decimal_places=2)
|
chinadelivery = PriceField(source='delivery_price_CN')
|
||||||
commission = serializers.DecimalField(source='commission_rub', max_digits=10, decimal_places=2)
|
commission = PriceField(source='commission_rub')
|
||||||
pickup = serializers.CharField(source='pickup_address')
|
pickup = serializers.CharField(source='pickup_address')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GlobalSettings
|
model = GlobalSettings
|
||||||
fields = ('currency', 'yuan_rate_last_updated', 'yuan_rate_commission', 'commission', 'chinadelivery', 'pickup', 'time_to_buy')
|
fields = ('yuan_rate', 'yuan_rate_last_updated', 'yuan_rate_commission', 'commission', 'chinadelivery', 'pickup', 'time_to_buy')
|
||||||
|
read_only_fields = ('yuan_rate_last_updated',)
|
||||||
|
|
||||||
|
|
||||||
class AnonymousGlobalSettingsSerializer(GlobalSettingsSerializer):
|
class AnonymousGlobalSettingsSerializer(GlobalSettingsSerializer):
|
||||||
|
|
@ -254,9 +266,6 @@ class PaymentMethodSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class PromocodeSerializer(serializers.ModelSerializer):
|
class PromocodeSerializer(serializers.ModelSerializer):
|
||||||
freedelivery = serializers.BooleanField(source='free_delivery')
|
|
||||||
nocomission = serializers.BooleanField(source='no_comission')
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Promocode
|
model = Promocode
|
||||||
fields = ('id', 'name', 'discount', 'freedelivery', 'nocomission', 'is_active')
|
fields = ('id', 'name', 'discount', 'free_delivery', 'no_comission', 'is_active')
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,9 @@ router.register(r'cdek', views.CDEKAPI, basename='cdek')
|
||||||
router.register(r'gifts', views.GiftAPI, basename='gifts')
|
router.register(r'gifts', views.GiftAPI, basename='gifts')
|
||||||
router.register(r'poizon', views.PoizonAPI, basename='poizon')
|
router.register(r'poizon', views.PoizonAPI, basename='poizon')
|
||||||
router.register(r'promo', views.PromoCodeAPI, basename='promo')
|
router.register(r'promo', views.PromoCodeAPI, basename='promo')
|
||||||
|
router.register(r'category', views.CategoryAPI, basename='category')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("category/", views.CategoryAPI.as_view()),
|
|
||||||
path("category/<int:id>", views.CategoryAPI.as_view()),
|
|
||||||
|
|
||||||
path("payment/", views.PaymentMethodsAPI.as_view()),
|
path("payment/", views.PaymentMethodsAPI.as_view()),
|
||||||
path("settings/", views.GlobalSettingsAPI.as_view()),
|
path("settings/", views.GlobalSettingsAPI.as_view()),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from datetime import timedelta
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import F, Count, Sum, OuterRef, Subquery
|
from django.db.models import F, Count, Sum
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework import generics, permissions, mixins, status, viewsets
|
from rest_framework import generics, permissions, mixins, status, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
|
@ -36,12 +36,6 @@ def prepare_external_response(r: requests.Response):
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
class DisablePermissionsMixin(generics.GenericAPIView):
|
|
||||||
def get_permissions(self):
|
|
||||||
if settings.DISABLE_PERMISSIONS:
|
|
||||||
return [permissions.AllowAny()]
|
|
||||||
|
|
||||||
return super().get_permissions()
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
- managers can create/edit/delete orders
|
- managers can create/edit/delete orders
|
||||||
|
|
@ -78,23 +72,31 @@ class ChecklistAPI(viewsets.ModelViewSet):
|
||||||
if self.action == "create":
|
if self.action == "create":
|
||||||
return ClientCreateChecklistSerializer
|
return ClientCreateChecklistSerializer
|
||||||
|
|
||||||
# Then, clients can update small set of fields
|
# Then, clients can update small set of fields and cancel orders
|
||||||
elif self.action in ['update', 'partial_update']:
|
elif self.action in ['update', 'partial_update', 'destroy']:
|
||||||
return ClientUpdateChecklistSerializer
|
return ClientUpdateChecklistSerializer
|
||||||
|
|
||||||
# Fallback to error
|
# Fallback to error
|
||||||
self.permission_denied(self.request, **self.kwargs)
|
self.permission_denied(self.request, **self.kwargs)
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self):
|
||||||
if self.action in ['list', 'update', 'partial_update', 'destroy']:
|
if self.action in ['list', 'update', 'partial_update']:
|
||||||
self.permission_classes = [IsManager]
|
self.permission_classes = [IsManager]
|
||||||
elif self.action == 'retrieve':
|
elif self.action == 'retrieve':
|
||||||
self.permission_classes = [AllowAny]
|
self.permission_classes = [AllowAny]
|
||||||
elif self.action == 'create':
|
elif self.action in ['create', 'destroy']:
|
||||||
self.permission_classes = [IsAuthenticated]
|
self.permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
return super().get_permissions()
|
return super().get_permissions()
|
||||||
|
|
||||||
|
def perform_destroy(self, obj: Checklist):
|
||||||
|
# Non-managers can cancel orders only in certain statuses
|
||||||
|
if (not getattr(self.request.user, 'is_manager', False)
|
||||||
|
and obj.status not in Checklist.Status.CANCELLABLE_ORDER_STATUSES):
|
||||||
|
raise CRMException("Can't delete the order")
|
||||||
|
|
||||||
|
obj.cancel()
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Checklist.objects.all().with_base_related() \
|
return Checklist.objects.all().with_base_related() \
|
||||||
.annotate_price_rub().annotate_commission_rub() \
|
.annotate_price_rub().annotate_commission_rub() \
|
||||||
|
|
@ -112,28 +114,23 @@ class ChecklistAPI(viewsets.ModelViewSet):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class CategoryAPI(mixins.ListModelMixin, mixins.UpdateModelMixin, generics.GenericAPIView):
|
class CategoryAPI(mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
|
||||||
serializer_class = CategorySerializer
|
serializer_class = CategorySerializer
|
||||||
permission_classes = [IsManager | ReadOnly]
|
permission_classes = [IsManager | ReadOnly]
|
||||||
lookup_field = 'id'
|
lookup_field = 'id'
|
||||||
|
queryset = Category.objects.all()
|
||||||
|
|
||||||
def get_queryset(self):
|
def list(self, request, *args, **kwargs):
|
||||||
return Category.objects.all()
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
categories_qs = Category.objects.root_nodes()
|
categories_qs = Category.objects.root_nodes()
|
||||||
return Response(CategoryFullSerializer(categories_qs, many=True).data)
|
return Response(CategoryFullSerializer(categories_qs, many=True).data)
|
||||||
|
|
||||||
def patch(self, request, *args, **kwargs):
|
|
||||||
return self.partial_update(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class GlobalSettingsAPI(generics.RetrieveUpdateAPIView):
|
class GlobalSettingsAPI(generics.RetrieveUpdateAPIView):
|
||||||
serializer_class = GlobalSettingsSerializer
|
serializer_class = GlobalSettingsSerializer
|
||||||
permission_classes = [IsManager | ReadOnly]
|
permission_classes = [IsManager | ReadOnly]
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
if getattr(self.request.user, 'is_manager', False) or settings.DISABLE_PERMISSIONS:
|
if getattr(self.request.user, 'is_manager', False):
|
||||||
return GlobalSettingsSerializer
|
return GlobalSettingsSerializer
|
||||||
|
|
||||||
# Anonymous users can view only a certain set of fields
|
# Anonymous users can view only a certain set of fields
|
||||||
|
|
@ -143,12 +140,11 @@ class GlobalSettingsAPI(generics.RetrieveUpdateAPIView):
|
||||||
return GlobalSettings.load()
|
return GlobalSettings.load()
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: update to GenericViewSet
|
||||||
class PaymentMethodsAPI(generics.GenericAPIView):
|
class PaymentMethodsAPI(generics.GenericAPIView):
|
||||||
serializer_class = PaymentMethodSerializer
|
serializer_class = PaymentMethodSerializer
|
||||||
permission_classes = [IsManager | ReadOnly]
|
permission_classes = [IsManager | ReadOnly]
|
||||||
|
queryset = PaymentMethod.objects.all()
|
||||||
def get_queryset(self):
|
|
||||||
return PaymentMethod.objects.all()
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
qs = self.get_queryset()
|
qs = self.get_queryset()
|
||||||
|
|
@ -191,7 +187,7 @@ class GiftAPI(viewsets.ModelViewSet):
|
||||||
filterset_class = GiftFilter
|
filterset_class = GiftFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if getattr(self.request.user, 'is_manager', False) or settings.DISABLE_PERMISSIONS:
|
if getattr(self.request.user, 'is_manager', False):
|
||||||
return Gift.objects.all()
|
return Gift.objects.all()
|
||||||
|
|
||||||
# For anonymous users, show only available gifts
|
# For anonymous users, show only available gifts
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
class TGBonusMessage:
|
class TGBonusMessage:
|
||||||
SIGNUP = "Вам начислено {amount}Р баллов за привязку номера телефона 🎊"
|
SIGNUP = "Вам начислено {amount}Р баллов за привязку номера телефона 🎊"
|
||||||
PURCHASE_ADDED = "Ура! Вам начислено {amount}P баллов за оформление заказа №{order_id}} 🎉"
|
PURCHASE_ADDED = "Ура! Вам начислено {amount}P баллов за оформление заказа №{order_id} 🎉"
|
||||||
PURCHASE_SPENT = "Вы потратили {amount}P баллов на оформление заказа №{order_id}"
|
PURCHASE_SPENT = "Вы потратили {amount}P баллов на оформление заказа №{order_id}"
|
||||||
FOR_INVITER = """
|
FOR_INVITER = """
|
||||||
Спасибо, твой друг оформил заказ и получил {amount}Р бонусов ✅
|
Спасибо, твой друг оформил заказ и получил {amount}Р бонусов ✅
|
||||||
|
|
@ -75,7 +75,7 @@ class TGCoreMessage:
|
||||||
|
|
||||||
SHARE_PHONE_KEYBOARD = "Отправить номер телефона"
|
SHARE_PHONE_KEYBOARD = "Отправить номер телефона"
|
||||||
SHARE_PHONE = "Поделитесь своим номером, чтобы авторизоваться:"
|
SHARE_PHONE = "Поделитесь своим номером, чтобы авторизоваться:"
|
||||||
SIGN_UP_SHARE_PHONE = """
|
TG_AUTH_SHARE_PHONE = """
|
||||||
Добро пожаловать в PoizonStore!
|
Добро пожаловать в PoizonStore!
|
||||||
Для завершения регистрации, поделитесь номером телефона:
|
Для завершения регистрации, поделитесь номером телефона:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,6 @@ class CRMException(APIException):
|
||||||
detail = self.default_detail
|
detail = self.default_detail
|
||||||
|
|
||||||
self.detail = {'error': detail}
|
self.detail = {'error': detail}
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: exceptions with a same template: ok / error_code / error_message
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user