winofy-backend/apps/orders/serializers.py
Ali Asadi 85fe57d1ca Allow checkout to target selected stores instead of the whole cart
POST /api/v1/checkout/ always converted the customer's entire
multi-store cart into orders. Add an optional store_uuids field to
CheckoutSerializer — when given, checkout() only processes those
stores' cart groups and only clears their items, leaving the rest of
the cart intact for a later checkout. Omitted, behavior is unchanged
(whole cart checks out).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 15:26:16 +03:30

138 lines
5.7 KiB
Python

from rest_framework import serializers
from apps.catalog.models import Product
from apps.catalog.serializers import ProductListSerializer
from apps.locations.models import Address
from apps.stores.serializers import StoreListSerializer
from .models import CartItem, Notification, Order, OrderGroup, OrderItem, OrderStatusLog
class CartItemSerializer(serializers.ModelSerializer):
product = ProductListSerializer(read_only=True)
line_total = serializers.IntegerField(read_only=True)
class Meta:
model = CartItem
fields = ('uuid', 'product', 'quantity', 'line_total')
class CartItemWriteSerializer(serializers.Serializer):
product_uuid = serializers.PrimaryKeyRelatedField(
source='product', queryset=Product.objects.filter(is_active=True),
)
quantity = serializers.IntegerField(min_value=1, default=1)
class StoreCartGroupSerializer(serializers.Serializer):
"""One store's slice of a multi-store cart (see the C07 mock)."""
store = StoreListSerializer(read_only=True)
items = CartItemSerializer(many=True, read_only=True)
items_subtotal = serializers.IntegerField(read_only=True)
delivery_fee = serializers.IntegerField(read_only=True)
total = serializers.IntegerField(read_only=True)
meets_minimum_order = serializers.BooleanField(read_only=True)
class CartSerializer(serializers.Serializer):
"""Groups the authenticated user's cart items by store for the multi-store-cart UI."""
groups = serializers.SerializerMethodField()
grand_total = serializers.SerializerMethodField()
def _build_groups(self, cart):
if not hasattr(self, '_groups_cache'):
from .services import build_cart_groups
self._groups_cache = build_cart_groups(cart)
return self._groups_cache
def get_groups(self, cart):
return StoreCartGroupSerializer(self._build_groups(cart), many=True, context=self.context).data
def get_grand_total(self, cart):
return sum(group['total'] for group in self._build_groups(cart))
class OrderItemSerializer(serializers.ModelSerializer):
line_total = serializers.IntegerField(read_only=True)
class Meta:
model = OrderItem
fields = ('uuid', 'product', 'product_name_snapshot', 'unit_price_snapshot', 'quantity', 'line_total')
class OrderStatusLogSerializer(serializers.ModelSerializer):
class Meta:
model = OrderStatusLog
fields = ('from_status', 'to_status', 'note', 'created_at')
class OrderSerializer(serializers.ModelSerializer):
store = StoreListSerializer(read_only=True)
items = OrderItemSerializer(many=True, read_only=True)
status_logs = OrderStatusLogSerializer(many=True, read_only=True)
total_amount = serializers.IntegerField(read_only=True)
is_cancellable = serializers.SerializerMethodField()
class Meta:
model = Order
fields = (
'uuid', 'store', 'status', 'items', 'items_subtotal', 'delivery_fee',
'commission_amount', 'seller_payout_amount', 'total_amount', 'cancel_reason',
'courier_name', 'courier_phone', 'placed_at', 'delivered_at', 'status_logs',
'is_cancellable',
)
def get_is_cancellable(self, order):
return order.status in Order.CANCELLABLE_STATUSES
class OrderGroupSerializer(serializers.ModelSerializer):
orders = OrderSerializer(many=True, read_only=True)
class Meta:
model = OrderGroup
fields = (
'uuid', 'recipient_name', 'recipient_phone', 'full_address',
'delivery_type', 'scheduled_at', 'payment_method', 'payment_status',
'notes', 'total_amount', 'orders', 'created_at',
)
class CheckoutSerializer(serializers.Serializer):
address_uuid = serializers.PrimaryKeyRelatedField(source='address', queryset=Address.objects.all())
delivery_type = serializers.ChoiceField(choices=OrderGroup.DeliveryType.choices, default=OrderGroup.DeliveryType.EXPRESS)
scheduled_at = serializers.DateTimeField(required=False, allow_null=True, default=None)
payment_method = serializers.ChoiceField(choices=OrderGroup.PaymentMethod.choices)
notes = serializers.CharField(required=False, allow_blank=True, default='')
store_uuids = serializers.ListField(
child=serializers.UUIDField(), required=False, allow_empty=False, default=None,
help_text='بررسی و پرداخت فقط برای فروشگاه‌های انتخاب‌شده از سبد خرید. در صورت خالی بودن، کل سبد خرید نهایی می‌شود.',
)
def validate(self, attrs):
if attrs.get('delivery_type') == OrderGroup.DeliveryType.SCHEDULED and not attrs.get('scheduled_at'):
raise serializers.ValidationError({'scheduled_at': 'برای ارسال زمان‌بندی شده، زمان را مشخص کنید.'})
address = attrs['address']
request = self.context['request']
if address.user_id != request.user.pk:
raise serializers.ValidationError({'address_uuid': 'این آدرس متعلق به شما نیست.'})
return attrs
class OrderCancelSerializer(serializers.Serializer):
reason = serializers.CharField(required=False, allow_blank=True, default='')
class OrderStatusActionSerializer(serializers.Serializer):
note = serializers.CharField(required=False, allow_blank=True, default='')
courier_name = serializers.CharField(required=False, allow_blank=True, default='')
courier_phone = serializers.CharField(required=False, allow_blank=True, default='')
class NotificationSerializer(serializers.ModelSerializer):
class Meta:
model = Notification
fields = ('uuid', 'type', 'title', 'body', 'related_order', 'is_read', 'created_at')
read_only_fields = fields