Compare commits

...

5 Commits

Author SHA1 Message Date
d0d637051d * Cleanup 2024-05-20 23:09:49 +04:00
4a987b9646 * Renamed fields in PromocodeSerializer 2024-05-20 23:08:30 +04:00
4a821faee9 + PriceField 2024-05-20 23:08:11 +04:00
81a3e15418 - DISABLE_PERMISSIONS 2024-05-20 23:05:27 +04:00
1f4d693c81 + More bonus validation
+ Bonus transaction & order cancellation
+ Spend bonus via API
+ New status for order: DELETED
* Fixed bug with not actual bonus balance returned
* Order bonus can be added in several statuses
* Fixed TG templates a bit
2024-05-20 21:46:24 +04:00
15 changed files with 223 additions and 114 deletions

View File

@ -17,5 +17,5 @@ class BonusProgramTransactionAdmin(admin.ModelAdmin):
def delete_queryset(self, request, queryset):
for obj in queryset:
obj.cancel_transaction()
obj.cancel()

View 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 = [
('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='Тип транзакции'),
),
]

View File

@ -2,11 +2,13 @@ import logging
from contextlib import suppress
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Sum
from django.utils import timezone
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 tg_bot.messages import TGBonusMessage
@ -36,12 +38,6 @@ class BonusType:
# Клиент потратил баллы на заказ
SPENT_PURCHASE = 11
# Отмена начисления
CANCELLED_DEPOSIT = 20
# Отмена списания
CANCELLED_WITHDRAWAL = 21
CHOICES = (
(OTHER_DEPOSIT, 'Другое начисление'),
(SIGNUP, 'Бонус за регистрацию'),
@ -51,16 +47,31 @@ class BonusType:
(OTHER_WITHDRAWAL, 'Другое списание'),
(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):
# TODO: optimize queries
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):
@ -99,11 +110,11 @@ class BonusProgramTransaction(models.Model):
case BonusType.INVITED_FIRST_PURCHASE:
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 ""
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 ""
msg = TGBonusMessage.OTHER_WITHDRAWAL.format(amount=abs(self.amount), comment=comment)
@ -116,10 +127,9 @@ class BonusProgramTransaction(models.Model):
if msg is not None:
self.user.notify_tg_bot(msg)
def cancel_transaction(self):
# Skip already cancelled transactions
# TODO: if reverse transaction is being deleted, revert the source one?
if self.was_cancelled or self.type in (BonusType.OTHER_WITHDRAWAL, BonusType.OTHER_DEPOSIT):
def cancel(self):
# Skip transactions that refers to cancelled ones
if self.was_cancelled:
return
date_formatted = localize(timezone.localtime(self.date))
@ -134,13 +144,14 @@ class BonusProgramTransaction(models.Model):
return
# Create reverse transaction, user's balance will be recalculated in post_save signal
transaction = BonusProgramTransaction()
transaction.user_id = self.user_id
transaction.type = bonus_type
transaction.amount = self.amount * -1
transaction.comment = comment
transaction.order = self.order
transaction = BonusProgramTransaction(
user_id=self.user_id,
type=bonus_type,
amount=self.amount * -1,
comment=comment,
order=self.order,
was_cancelled=True
)
transaction.save()
self.was_cancelled = True
@ -148,9 +159,43 @@ class BonusProgramTransaction(models.Model):
def delete(self, *args, **kwargs):
# 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):
self.full_clean()
if self.id is None:
self._notify_user_about_new_transaction()
@ -181,12 +226,12 @@ class BonusProgramMixin(models.Model):
class Meta:
abstract = True
@property
@cached_property
def bonus_history(self):
return BonusProgramTransaction.objects.filter(user_id=self.id)
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:
return
@ -195,27 +240,30 @@ class BonusProgramMixin(models.Model):
amount=amount, type=bonus_type,
comment=comment, order=order)
transaction.save()
self.refresh_from_db(fields=['balance'])
def recalculate_balance(self):
# TODO: use this method when checking the available balance upon order creation
total_balance = BonusProgramTransaction.objects \
.filter(user_id=self.id) \
.aggregate(total_balance=Sum('amount'))['total_balance'] or 0
total_balance = self.bonus_history \
.aggregate(total_balance=Sum('amount'))['total_balance'] or 0
self.balance = max(0, total_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):
bonus_type = BonusType.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)
def add_order_bonus(self, order):
@ -224,15 +272,7 @@ class BonusProgramMixin(models.Model):
bonus_type = BonusType.DEFAULT_PURCHASE
amount = self._get_bonus_amount("default_purchase")
if order.status != Checklist.Status.CHINA_RUSSIA:
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}")
if order.status not in Checklist.Status.BONUS_ORDER_STATUSES:
return
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
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
user.update_balance(amount, bonus_type, order=order)

View File

@ -68,7 +68,7 @@ class UserManager(models.Manager.from_queryset(UserQuerySet), _UserManager):
logger.warning(f"User #{inviter.id} already invited user #{user_id}")
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
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}")
return freshly_created
class User(BonusProgramMixin, AbstractUser):
ADMIN = "admin"

View File

@ -20,11 +20,12 @@ class UserSerializer(serializers.ModelSerializer):
class BonusProgramTransactionSerializer(serializers.ModelSerializer):
order_id = serializers.StringRelatedField(source='order.id', allow_null=True)
type = serializers.CharField(source='get_type_display')
class Meta:
model = BonusProgramTransaction
fields = ('id', 'type', 'date', 'amount', 'comment', 'was_cancelled')
fields = ('id', 'type', 'date', 'amount', 'order_id', 'comment', 'was_cancelled')
def non_zero_validator(value):

View File

@ -15,12 +15,9 @@ from account.serializers import SetInitialPasswordSerializer, BonusProgramTransa
UserBalanceUpdateSerializer, TelegramCallbackSerializer
from tg_bot.handlers.start import request_phone_sync
from tg_bot.messages import TGCoreMessage
from tg_bot.bot import bot_sync
class UserViewSet(djoser_views.UserViewSet):
""" Replacement for Djoser's UserViewSet """
def permission_denied(self, request, **kwargs):
if (
djoser_settings.HIDE_USERS
@ -147,7 +144,7 @@ class TelegramLoginForm(views.APIView):
# Sign up user
user = User.objects.create_draft_user(tg_user_id=tg_user_id)
# 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)
return Response({"auth_token": token.key})

View File

@ -52,7 +52,6 @@ TG_BOT_TOKEN = get_secret("TG_BOT_TOKEN")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(int(os.environ.get("DJANGO_DEBUG") or 0))
DISABLE_PERMISSIONS = False
DISABLE_CORS = True
ALLOWED_HOSTS = get_secret('ALLOWED_HOSTS').split(',')
@ -175,9 +174,6 @@ REST_FRAMEWORK = {
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated'
if not DISABLE_PERMISSIONS
else
'rest_framework.permissions.AllowAny'
],
'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.TokenAuthentication'],

6
poizonstore/utils.py Normal file
View 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)

View 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='Статус заказа'),
),
]

View File

@ -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.functions import Ceil
from django.db.models.lookups import GreaterThan
from django.db.transaction import atomic
from django.utils import timezone
from django_cleanup import cleanup
from mptt.fields import TreeForeignKey
@ -251,6 +252,7 @@ class PriceSnapshot(models.Model):
class Checklist(models.Model):
# Statuses
class Status:
DELETED = "deleted"
DRAFT = "draft"
NEW = "neworder"
PAYMENT = "payment"
@ -267,7 +269,11 @@ class Checklist(models.Model):
PDF_AVAILABLE_STATUSES = (RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED)
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, 'Удален'),
(DRAFT, 'Черновик'),
(NEW, 'Новый заказ'),
(PAYMENT, 'Проверка оплаты'),
@ -506,6 +512,7 @@ class Checklist(models.Model):
# Default commission
commission = GlobalSettings.load().commission_rub
# For big orders there is an additional commission
if self.price_rub > 150_000:
commission = max(commission, self.price_rub * Decimal(settings.COMMISSION_OVER_150K))
@ -516,6 +523,32 @@ class Checklist(models.Model):
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
def preview_image(self):
# Prefer annotated field
@ -589,6 +622,7 @@ class Checklist(models.Model):
if tg_message:
self.customer.notify_tg_bot(tg_message)
@atomic
def _check_eligible_for_order_bonus(self):
if self.customer_id is None:
return

View File

@ -1,3 +1,4 @@
from django.db.transaction import atomic
from drf_extra_fields.fields import Base64ImageField
from rest_framework import serializers
@ -5,6 +6,7 @@ from account.serializers import UserSerializer
from utils.exceptions import CRMException
from store.models import Checklist, GlobalSettings, Category, PaymentMethod, Promocode, Image, Gift
from store.utils import get_primary_key_related_model
from poizonstore.utils import PriceField
class ImageSerializer(serializers.ModelSerializer):
@ -35,7 +37,7 @@ class CategoryChecklistSerializer(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:
model = Category
@ -55,6 +57,7 @@ class CategoryFullSerializer(CategorySerializer):
class GiftSerializer(serializers.ModelSerializer):
image = Base64ImageField(required=False, allow_null=True)
min_price = PriceField()
class Meta:
model = Gift
@ -75,19 +78,20 @@ class ChecklistSerializer(serializers.ModelSerializer):
promocode = serializers.SlugRelatedField(slug_field='name', queryset=Promocode.objects.active(),
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)
yuan_rate = serializers.DecimalField(read_only=True, max_digits=10, decimal_places=2)
price_yuan = serializers.DecimalField(required=False, max_digits=10, decimal_places=2)
yuan_rate = PriceField(read_only=True)
price_yuan = PriceField(required=False)
price_rub = serializers.IntegerField(read_only=True)
delivery_price_CN = serializers.DecimalField(read_only=True, max_digits=10, decimal_places=2)
delivery_price_CN_RU = serializers.DecimalField(read_only=True, max_digits=10, decimal_places=2)
delivery_price_CN = PriceField(read_only=True)
delivery_price_CN_RU = PriceField(read_only=True)
full_price = serializers.IntegerField(read_only=True)
real_price = serializers.DecimalField(required=False, allow_null=True, max_digits=10, decimal_places=2)
full_price = PriceField(read_only=True)
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)
@ -126,6 +130,7 @@ class ChecklistSerializer(serializers.ModelSerializer):
instance.images.set(img_objs)
instance.generate_preview(next(iter(img_objs), None))
@atomic
def create(self, 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:
validated_data['customer'] = user
use_bonuses = validated_data.pop('use_bonuses')
instance = super().create(validated_data)
self._create_main_images(instance, images.get('main_images'))
if use_bonuses:
instance.customer.spend_bonuses(order=instance)
return instance
def update(self, instance, validated_data):
@ -170,7 +181,7 @@ class ChecklistSerializer(serializers.ModelSerializer):
'image',
'preview_image_url',
'yuan_rate', 'price_yuan', 'price_rub', 'delivery_price_CN', 'delivery_price_CN_RU', 'commission_rub',
'promocode', 'gift',
'promocode', 'gift', 'use_bonuses', 'bonus_used',
'comment',
'full_price', 'real_price',
'customer',
@ -203,7 +214,7 @@ class ClientCreateChecklistSerializer(ClientChecklistSerializerMixin, ChecklistS
writable_fields = {
'link',
'brand', 'model', 'size', 'category',
'price_yuan',
'price_yuan', 'use_bonuses',
'comment',
}
read_only_fields = tuple(set(ChecklistSerializer.Meta.fields) - writable_fields)
@ -228,15 +239,16 @@ class ClientUpdateChecklistSerializer(ClientChecklistSerializerMixin, ChecklistS
class GlobalSettingsSerializer(serializers.ModelSerializer):
currency = serializers.DecimalField(source='full_yuan_rate', read_only=True, max_digits=10, decimal_places=2)
yuan_rate_last_updated = serializers.DateTimeField(read_only=True)
chinadelivery = serializers.DecimalField(source='delivery_price_CN', max_digits=10, decimal_places=2)
commission = serializers.DecimalField(source='commission_rub', max_digits=10, decimal_places=2)
yuan_rate = PriceField(source='full_yuan_rate', read_only=True)
yuan_rate_commission = PriceField()
chinadelivery = PriceField(source='delivery_price_CN')
commission = PriceField(source='commission_rub')
pickup = serializers.CharField(source='pickup_address')
class Meta:
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):
@ -254,9 +266,6 @@ class PaymentMethodSerializer(serializers.ModelSerializer):
class PromocodeSerializer(serializers.ModelSerializer):
freedelivery = serializers.BooleanField(source='free_delivery')
nocomission = serializers.BooleanField(source='no_comission')
class Meta:
model = Promocode
fields = ('id', 'name', 'discount', 'freedelivery', 'nocomission', 'is_active')
fields = ('id', 'name', 'discount', 'free_delivery', 'no_comission', 'is_active')

View File

@ -12,11 +12,9 @@ router.register(r'cdek', views.CDEKAPI, basename='cdek')
router.register(r'gifts', views.GiftAPI, basename='gifts')
router.register(r'poizon', views.PoizonAPI, basename='poizon')
router.register(r'promo', views.PromoCodeAPI, basename='promo')
router.register(r'category', views.CategoryAPI, basename='category')
urlpatterns = [
path("category/", views.CategoryAPI.as_view()),
path("category/<int:id>", views.CategoryAPI.as_view()),
path("payment/", views.PaymentMethodsAPI.as_view()),
path("settings/", views.GlobalSettingsAPI.as_view()),

View File

@ -3,7 +3,7 @@ from datetime import timedelta
import requests
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 rest_framework import generics, permissions, mixins, status, viewsets
from rest_framework.decorators import action
@ -36,12 +36,6 @@ def prepare_external_response(r: requests.Response):
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
@ -78,23 +72,31 @@ class ChecklistAPI(viewsets.ModelViewSet):
if self.action == "create":
return ClientCreateChecklistSerializer
# Then, clients can update small set of fields
elif self.action in ['update', 'partial_update']:
# Then, clients can update small set of fields and cancel orders
elif self.action in ['update', 'partial_update', 'destroy']:
return ClientUpdateChecklistSerializer
# Fallback to error
self.permission_denied(self.request, **self.kwargs)
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]
elif self.action == 'retrieve':
self.permission_classes = [AllowAny]
elif self.action == 'create':
elif self.action in ['create', 'destroy']:
self.permission_classes = [IsAuthenticated]
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):
return Checklist.objects.all().with_base_related() \
.annotate_price_rub().annotate_commission_rub() \
@ -112,28 +114,23 @@ class ChecklistAPI(viewsets.ModelViewSet):
return obj
class CategoryAPI(mixins.ListModelMixin, mixins.UpdateModelMixin, generics.GenericAPIView):
class CategoryAPI(mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
serializer_class = CategorySerializer
permission_classes = [IsManager | ReadOnly]
lookup_field = 'id'
queryset = Category.objects.all()
def get_queryset(self):
return Category.objects.all()
def get(self, request, *args, **kwargs):
def list(self, request, *args, **kwargs):
categories_qs = Category.objects.root_nodes()
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):
serializer_class = GlobalSettingsSerializer
permission_classes = [IsManager | ReadOnly]
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
# Anonymous users can view only a certain set of fields
@ -143,12 +140,11 @@ class GlobalSettingsAPI(generics.RetrieveUpdateAPIView):
return GlobalSettings.load()
# TODO: update to GenericViewSet
class PaymentMethodsAPI(generics.GenericAPIView):
serializer_class = PaymentMethodSerializer
permission_classes = [IsManager | ReadOnly]
def get_queryset(self):
return PaymentMethod.objects.all()
queryset = PaymentMethod.objects.all()
def get(self, request, *args, **kwargs):
qs = self.get_queryset()
@ -191,7 +187,7 @@ class GiftAPI(viewsets.ModelViewSet):
filterset_class = GiftFilter
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()
# For anonymous users, show only available gifts

View File

@ -3,7 +3,7 @@
class TGBonusMessage:
SIGNUP = "Вам начислено {amount}Р баллов за привязку номера телефона 🎊"
PURCHASE_ADDED = "Ура! Вам начислено {amount}P баллов за оформление заказа №{order_id}} 🎉"
PURCHASE_ADDED = "Ура! Вам начислено {amount}P баллов за оформление заказа №{order_id} 🎉"
PURCHASE_SPENT = "Вы потратили {amount}P баллов на оформление заказа №{order_id}"
FOR_INVITER = """
Спасибо, твой друг оформил заказ и получил {amount}Р бонусов
@ -75,7 +75,7 @@ class TGCoreMessage:
SHARE_PHONE_KEYBOARD = "Отправить номер телефона"
SHARE_PHONE = "Поделитесь своим номером, чтобы авторизоваться:"
SIGN_UP_SHARE_PHONE = """
TG_AUTH_SHARE_PHONE = """
Добро пожаловать в PoizonStore!
Для завершения регистрации, поделитесь номером телефона:
"""

View File

@ -10,3 +10,6 @@ class CRMException(APIException):
detail = self.default_detail
self.detail = {'error': detail}
# TODO: exceptions with a same template: ok / error_code / error_message