winofy-backend/apps/orders/serializers.py
Ali Asadi 5117fe9b05 Add Store.max_delivery_time_minutes, return it wherever store info appears
Seller sets this at registration (default 60 min). Exposed on store
list/detail, the seller's own store, cart groups (GET /api/v1/cart/),
and order/order-group responses, so the checkout page's estimated
delivery time no longer needs a frontend mock.

Also document CartSerializer's groups/grand_total for swagger (were
untyped SerializerMethodFields showing as opaque strings) since that's
the endpoint carrying this new field to the checkout page.

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

137 lines
5.5 KiB
Python

from drf_spectacular.utils import extend_schema_field
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
@extend_schema_field(StoreCartGroupSerializer(many=True))
def get_groups(self, cart):
return StoreCartGroupSerializer(self._build_groups(cart), many=True, context=self.context).data
@extend_schema_field(serializers.IntegerField())
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='')
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