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.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'] permission_classes = [IsAuthenticated] required_alternate_scopes = { "GET": [[]], "DELETE": [[]], } serializer_class = CartSerializer 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/update/remove a single product line in the authenticated user's cart.""" schema_tags = ['Cart'] permission_classes = [IsAuthenticated] required_alternate_scopes = { "POST": [[]], "PATCH": [[]], "DELETE": [[]], } serializer_class = CartItemWriteSerializer @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) @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'] permission_classes = [IsAuthenticated] required_alternate_scopes = { "POST": [[]], } serializer_class = CheckoutSerializer @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'] permission_classes = [IsAuthenticated] required_alternate_scopes = { "GET": [[]], } serializer_class = OrderGroupSerializer 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'] permission_classes = [IsAuthenticated] required_alternate_scopes = { "GET": [[]], "POST": [[]], } serializer_class = OrderSerializer 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'] permission_classes = [IsAuthenticated, IsStoreOwner] required_alternate_scopes = { "GET": [[]], "POST": [[]], } serializer_class = OrderSerializer def get_queryset(self): if getattr(self, 'swagger_fake_view', False): return Order.objects.none() queryset = Order.objects.filter(store=self.request.user.store).select_related('store').prefetch_related('items') status_param = self.request.query_params.get('status') if status_param: queryset = queryset.filter(status=status_param) return queryset 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'] permission_classes = [IsAuthenticated] required_alternate_scopes = { "GET": [[]], "POST": [[]], } serializer_class = NotificationSerializer 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)