winofy-backend/apps/orders/views.py
2026-08-31 14:11:38 +03:30

239 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema
from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
from apps.core.permissions import IsStoreOwner
from . import services
from .models import Cart, CartItem, Notification, Order, OrderGroup
from .serializers import (
CartItemSerializer,
CartItemWriteSerializer,
CartSerializer,
CheckoutSerializer,
NotificationSerializer,
OrderCancelSerializer,
OrderGroupSerializer,
OrderSerializer,
OrderStatusActionSerializer,
)
class CartView(APIView):
"""The authenticated user's cart, grouped by store (C07)."""
schema_tags = ['Cart']
serializer_class = CartSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"GET": [[]],
}
def get(self, request):
cart, _ = Cart.objects.get_or_create(user=request.user)
return Response(CartSerializer(cart, context={'request': request}).data)
@extend_schema(responses=None)
def delete(self, request):
cart, _ = Cart.objects.get_or_create(user=request.user)
cart.items.all().delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class CartItemView(APIView):
"""Add a product line to the authenticated user's cart."""
schema_tags = ['Cart']
serializer_class = CartItemWriteSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [[]],
}
@extend_schema(request=CartItemWriteSerializer, responses=CartItemSerializer)
def post(self, request):
serializer = CartItemWriteSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
cart, _ = Cart.objects.get_or_create(user=request.user)
product = serializer.validated_data['product']
quantity = serializer.validated_data['quantity']
item, created = CartItem.objects.get_or_create(
cart=cart, product=product, defaults={'quantity': quantity},
)
if not created:
item.quantity += quantity
item.save(update_fields=['quantity', 'updated_at'])
return Response(CartItemSerializer(item).data, status=status.HTTP_201_CREATED)
class CartItemDetailView(APIView):
"""Update/remove a single product line in the authenticated user's cart."""
schema_tags = ['Cart']
serializer_class = CartItemWriteSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"DELETE": [[]],
"PATCH": [[]],
}
@extend_schema(responses=CartItemSerializer)
def patch(self, request, item_uuid):
item = get_object_or_404(CartItem, uuid=item_uuid, cart__user=request.user)
quantity = request.data.get('quantity')
if quantity is None or int(quantity) < 1:
return Response({'quantity': 'مقدار باید حداقل ۱ باشد.'}, status=status.HTTP_400_BAD_REQUEST)
item.quantity = int(quantity)
item.save(update_fields=['quantity', 'updated_at'])
return Response(CartItemSerializer(item).data)
@extend_schema(responses=None)
def delete(self, request, item_uuid):
item = get_object_or_404(CartItem, uuid=item_uuid, cart__user=request.user)
item.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class CheckoutView(APIView):
"""Splits the authenticated customer's multi-store cart into per-store orders (C08)."""
schema_tags = ['Checkout']
serializer_class = CheckoutSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [[]],
}
@extend_schema(request=CheckoutSerializer, responses=OrderGroupSerializer)
def post(self, request):
serializer = CheckoutSerializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
order_group = services.checkout(user=request.user, **serializer.validated_data)
data = OrderGroupSerializer(order_group).data
return Response(data, status=status.HTTP_201_CREATED)
class OrderGroupViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
"""Customer order history — each group may contain orders from several stores."""
schema_tags = ['Orders']
serializer_class = OrderGroupSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [[]],
}
def get_queryset(self):
if getattr(self, 'swagger_fake_view', False):
return OrderGroup.objects.none()
return OrderGroup.objects.filter(user=self.request.user).prefetch_related('orders__items')
class OrderViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
"""Customer-facing single-order tracking (C09) + cancel."""
schema_tags = ['Orders']
serializer_class = OrderSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [[]],
}
def get_queryset(self):
if getattr(self, 'swagger_fake_view', False):
return Order.objects.none()
return Order.objects.filter(customer=self.request.user).select_related('store')
@action(detail=True, methods=['post'])
def cancel(self, request, pk=None):
order = self.get_object()
serializer = OrderCancelSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
services.cancel_order(order, changed_by=request.user, reason=serializer.validated_data['reason'])
return Response(OrderSerializer(order).data)
class SellerOrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
"""Seller order management (S09 list w/ status tabs, S10 detail + stepper actions)."""
schema_tags = ['Seller · Orders']
serializer_class = OrderSerializer
# permission_classes = [IsAuthenticated, IsStoreOwner]
# TODO: IsStoreOwner
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [[]],
}
filterset_fields = ['status']
def get_queryset(self):
if getattr(self, 'swagger_fake_view', False):
return Order.objects.none()
return Order.objects.filter(store=self.request.user.store).select_related('store').prefetch_related('items')
def _apply_transition(self, request, to_status):
order = self.get_object()
serializer = OrderStatusActionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
services.advance_status(order, to_status, changed_by=request.user, **serializer.validated_data)
return Response(OrderSerializer(order).data)
@action(detail=True, methods=['post'])
def confirm(self, request, pk=None):
return self._apply_transition(request, Order.Status.PREPARING)
@action(detail=True, methods=['post'], url_path='mark-ready')
def mark_ready(self, request, pk=None):
return self._apply_transition(request, Order.Status.READY_TO_SHIP)
@action(detail=True, methods=['post'], url_path='mark-shipped')
def mark_shipped(self, request, pk=None):
return self._apply_transition(request, Order.Status.HANDED_TO_COURIER)
@action(detail=True, methods=['post'], url_path='mark-delivered')
def mark_delivered(self, request, pk=None):
return self._apply_transition(request, Order.Status.DELIVERED)
@action(detail=True, methods=['post'])
def cancel(self, request, pk=None):
order = self.get_object()
serializer = OrderCancelSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
services.cancel_order(order, changed_by=request.user, reason=serializer.validated_data['reason'])
return Response(OrderSerializer(order).data)
class NotificationViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
"""Shared notification feed (S16 for sellers; same model serves the customer app)."""
schema_tags = ['Notifications']
serializer_class = NotificationSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [[]],
}
def get_queryset(self):
if getattr(self, 'swagger_fake_view', False):
return Notification.objects.none()
return Notification.objects.filter(recipient=self.request.user)
@action(detail=True, methods=['post'], url_path='mark-read')
def mark_read(self, request, pk=None):
notification = self.get_object()
notification.is_read = True
notification.save(update_fields=['is_read', 'updated_at'])
return Response(NotificationSerializer(notification).data)
@action(detail=False, methods=['post'], url_path='mark-all-read')
def mark_all_read(self, request):
self.get_queryset().filter(is_read=False).update(is_read=True)
return Response(status=204)