winofy-backend/apps/orders/views.py
Ali Asadi 52adc732fa Initial Winofy backend: marketplace core (no payment integration)
Django 6 + DRF resource server against the Gooyal accounts OAuth2 service,
matching the Winsoo ecosystem's conventions. Covers locations, stores,
catalog, cart, checkout/orders (with the multi-store-cart split and the
status stepper), reviews, and notifications, plus a demo-data seed command.

Payment integration (wallet debits, online gateway, seller payouts) is
intentionally left out here — see feature/payment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 14:03:15 +03:30

205 lines
8.3 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.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]
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]
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]
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]
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]
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]
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]
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)