diff --git a/api_spot/api/serializers/__init__.py b/api_spot/api/serializers/__init__.py index c00c2f8e..c45659a6 100644 --- a/api_spot/api/serializers/__init__.py +++ b/api_spot/api/serializers/__init__.py @@ -6,6 +6,7 @@ LocationGetSerializer, LocationGetShortSerializer, LocationMapSerializer, ) from .plan_photo import PlanPhotoGetSerializer +from .promocode_check import PromocodeCheckSerializer from .question import QuestionSerializer from .rule import RuleSerializer from .spot import SpotDetailSerializer, SpotQuerySerializer, SpotSerializer @@ -19,6 +20,7 @@ LocationGetShortSerializer LocationMapSerializer PlanPhotoGetSerializer +PromocodeCheckSerializer QuestionSerializer RuleSerializer SpotDetailSerializer diff --git a/api_spot/api/serializers/promocode_check.py b/api_spot/api/serializers/promocode_check.py new file mode 100644 index 00000000..598f7bd6 --- /dev/null +++ b/api_spot/api/serializers/promocode_check.py @@ -0,0 +1,22 @@ +from rest_framework import serializers + +from api.services.promocode_check import promocode_available_check +from api.fields import GetSpot + + +class PromocodeCheckSerializer(serializers.Serializer): + promocode = serializers.CharField(max_length=64) + order = serializers.HiddenField( + default=GetSpot() + ) + user = serializers.HiddenField( + default=serializers.CurrentUserDefault() + ) + + def validate(self, data, *args, **kwargs): + promocode_name = data.get('promocode') + spot = data.get('order') + user = data.get('user') + promocode = promocode_available_check(promocode_name, spot, user) + data['promocode'] = promocode + return data diff --git a/api_spot/api/services/promocode_check.py b/api_spot/api/services/promocode_check.py new file mode 100644 index 00000000..1733a51f --- /dev/null +++ b/api_spot/api/services/promocode_check.py @@ -0,0 +1,25 @@ +from django.utils import timezone +from rest_framework.exceptions import ValidationError + +from promo.models import Promocode + + +def promocode_available_check(promocode_name, spot, user): + """ + Проверка доступности использования промокода. + """ + promocode = Promocode.objects.filter(name=promocode_name).first() + if promocode: + if promocode.balance < 1: + raise ValidationError('Количество использований исчерпано.') + if promocode.expiry_date < timezone.now().date(): + raise ValidationError('Срок годности промокода истек.') + if (promocode.only_category + and promocode.only_category != spot.category): + raise ValidationError('Промокод не применяется для этой категории') + used_by_user = promocode.promocode_user.filter(user=user).exists() + if promocode.one_off and used_by_user: + raise ValidationError('Вы уже использовали данный промокод') + return promocode + else: + raise ValidationError('Промокода не существует') diff --git a/api_spot/api/urls.py b/api_spot/api/urls.py index 89f259c1..8e6c53d5 100644 --- a/api_spot/api/urls.py +++ b/api_spot/api/urls.py @@ -5,8 +5,8 @@ AddSpotsAPIView, AvatarViewSet, EquipmentViewSet, EventViewSet, FavoriteViewSet, LocationMapListAPIView, LocationShortListAPIView, LocationViewSet, OrderGetViewSet, OrderViewSet, PayView, PlanPhotoAPIView, - QuestionViewSet, ReviewCreateViewSet, ReviewGetViewSet, RuleViewSet, - SpotViewSet, SubscireAPIView, UserViewSet, + PromocodeCheckAPIView, QuestionViewSet, ReviewCreateViewSet, + ReviewGetViewSet, RuleViewSet, SpotViewSet, SubscireAPIView, UserViewSet, ) @@ -75,9 +75,14 @@ r'/order/(?P\d+)/pay/', PayView.as_view(), name='pay' ), + re_path( + r'locations/(?P\d+)/spots/(?P\d+)' + r'/order/(?P\d+)/check_promocode/', + PromocodeCheckAPIView.as_view(), name='check_promocode' + ), path('short_locations/', LocationShortListAPIView.as_view()), path('map_locations/', LocationMapListAPIView.as_view()), - path('subscribe/', SubscireAPIView.as_view(),), + path('subscribe/', SubscireAPIView.as_view()), re_path( r'locations/(?P\d+)/plan_photo/', PlanPhotoAPIView.as_view() diff --git a/api_spot/api/views/__init__.py b/api_spot/api/views/__init__.py index 81053f15..ffaaee6a 100644 --- a/api_spot/api/views/__init__.py +++ b/api_spot/api/views/__init__.py @@ -9,6 +9,7 @@ from .order import OrderGetViewSet, OrderViewSet from .pay import PayView from .plan_photo import PlanPhotoAPIView +from .promocode_check import PromocodeCheckAPIView from .question import QuestionViewSet from .review import ReviewCreateViewSet, ReviewGetViewSet from .rule import RuleViewSet @@ -29,6 +30,7 @@ OrderViewSet PayView PlanPhotoAPIView +PromocodeCheckAPIView QuestionViewSet ReviewCreateViewSet ReviewGetViewSet diff --git a/api_spot/api/views/promocode_check.py b/api_spot/api/views/promocode_check.py new file mode 100644 index 00000000..bfd67943 --- /dev/null +++ b/api_spot/api/views/promocode_check.py @@ -0,0 +1,28 @@ +from drf_spectacular.utils import extend_schema +from rest_framework import status +from rest_framework.generics import CreateAPIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from api.serializers.promocode_check import PromocodeCheckSerializer + + +@extend_schema( + tags=('pay',), +) +class PromocodeCheckAPIView(CreateAPIView): + """ + Представление проверки доступности промокода. + """ + serializer_class = PromocodeCheckSerializer + permission_classes = (IsAuthenticated,) + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + promocode = serializer.validated_data.get('promocode') + discount = promocode.percent_discount + return Response( + {'message': f'Промокод доступен на скидку {discount} %'}, + status=status.HTTP_200_OK, + ) diff --git a/api_spot/promo/admin.py b/api_spot/promo/admin.py index b2bb9a97..9546215b 100644 --- a/api_spot/promo/admin.py +++ b/api_spot/promo/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from promo.models import EmailNews, Promocode +from promo.models import EmailNews, Promocode, PromocodeUser @admin.register(Promocode) @@ -11,9 +11,21 @@ class PromcodeAdmin(admin.ModelAdmin): 'percent_discount', 'expiry_date', 'balance', + 'only_category', + 'one_off', ) +@admin.register(PromocodeUser) +class PromocodeUserAdmin(admin.ModelAdmin): + list_display = ( + 'id', + 'user', + 'promocode', + ) + list_filter = ('promocode',) + + @admin.register(EmailNews) class EmailNewsAdmin(admin.ModelAdmin): list_display = ( diff --git a/api_spot/promo/migrations/0005_promocode_one_off_promocode_only_category_and_more.py b/api_spot/promo/migrations/0005_promocode_one_off_promocode_only_category_and_more.py new file mode 100644 index 00000000..d43e6cf4 --- /dev/null +++ b/api_spot/promo/migrations/0005_promocode_one_off_promocode_only_category_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.5 on 2023-10-31 22:13 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('promo', '0004_alter_promocode_expiry_date'), + ] + + operations = [ + migrations.AddField( + model_name='promocode', + name='one_off', + field=models.BooleanField(default=True, verbose_name='Однократное использование пользователем'), + ), + migrations.AddField( + model_name='promocode', + name='only_category', + field=models.CharField(blank=True, choices=[('Рабочее место', 'Рабочее место'), ('Переговорная', 'Переговорная')], null=True, verbose_name='Только для категории'), + ), + migrations.CreateModel( + name='PromocodeUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('promocode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='promocode_user', to='promo.promocode')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='promocode_user', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Пользователь-промокод', + 'verbose_name_plural': 'Пользователи-промокоды', + }, + ), + migrations.AddField( + model_name='promocode', + name='used_user', + field=models.ManyToManyField(related_name='promocode', through='promo.PromocodeUser', to=settings.AUTH_USER_MODEL, verbose_name='Оборудование'), + ), + migrations.AddConstraint( + model_name='promocodeuser', + constraint=models.UniqueConstraint(fields=('user', 'promocode'), name='unique_user_promocode'), + ), + ] diff --git a/api_spot/promo/models.py b/api_spot/promo/models.py index c5c74da1..b2d6011e 100644 --- a/api_spot/promo/models.py +++ b/api_spot/promo/models.py @@ -1,3 +1,4 @@ +from django.contrib.auth import get_user_model from django.db import models, transaction from django_celery_beat.models import PeriodicTask @@ -6,6 +7,10 @@ MaxDiscountValidator, validate_date_less_present, validate_datetime_less_present, ) +from spots.constants import CATEGORY_CHOICES + + +User = get_user_model() class Promocode(models.Model): @@ -25,6 +30,22 @@ class Promocode(models.Model): balance = models.PositiveIntegerField( 'Количество использований', ) + only_category = models.CharField( + 'Только для категории', + choices=CATEGORY_CHOICES, + blank=True, + null=True, + ) + one_off = models.BooleanField( + 'Однократное использование пользователем', + default=True, + ) + used_user = models.ManyToManyField( + User, + related_name='promocode', + verbose_name='Оборудование', + through='PromocodeUser', + ) def __str__(self): return self.name @@ -35,6 +56,29 @@ class Meta: ordering = ('expiry_date',) +class PromocodeUser(models.Model): + user = models.ForeignKey( + User, + related_name='promocode_user', + on_delete=models.CASCADE, + ) + promocode = models.ForeignKey( + Promocode, + related_name='promocode_user', + on_delete=models.CASCADE, + ) + + class Meta: + verbose_name = 'Пользователь-промокод' + verbose_name_plural = 'Пользователи-промокоды' + constraints = ( + models.UniqueConstraint( + fields=('user', 'promocode'), + name='unique_user_promocode', + ), + ) + + class EmailNews(models.Model): subject_message = models.CharField( 'Тема письма', diff --git a/api_spot/spots/migrations/0015_alter_price_discount.py b/api_spot/spots/migrations/0015_alter_price_discount.py new file mode 100644 index 00000000..afd409e7 --- /dev/null +++ b/api_spot/spots/migrations/0015_alter_price_discount.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.5 on 2023-10-31 22:13 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spots', '0014_alter_order_options'), + ] + + operations = [ + migrations.AlterField( + model_name='price', + name='discount', + field=models.PositiveSmallIntegerField(default=0, validators=[django.core.validators.MaxValueValidator(limit_value=70, message='Скидка не может превышать 70%%')], verbose_name='Скидка'), + ), + ] diff --git a/api_spot/users/migrations/0008_remove_user_have_orders.py b/api_spot/users/migrations/0008_remove_user_have_orders.py new file mode 100644 index 00000000..59540f53 --- /dev/null +++ b/api_spot/users/migrations/0008_remove_user_have_orders.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.5 on 2023-10-31 23:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0007_user_have_orders_user_is_subscribed'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='have_orders', + ), + ] diff --git a/api_spot/users/models.py b/api_spot/users/models.py index c4556687..227fad84 100644 --- a/api_spot/users/models.py +++ b/api_spot/users/models.py @@ -108,10 +108,6 @@ class User(AbstractBaseUser, PermissionsMixin): 'Подписан на рассылку', default=False, ) - have_orders = models.BooleanField( - 'Имеет заказы', - default=False, - ) EMAIL_FIELD = 'email' USERNAME_FIELD = 'email'