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>
156 lines
6.2 KiB
Python
156 lines
6.2 KiB
Python
from django.contrib.gis.geos import Point
|
|
from drf_spectacular.utils import extend_schema_field
|
|
from rest_framework import serializers
|
|
|
|
from apps.core.media import presigned_media_url
|
|
from apps.locations.models import City, Neighborhood
|
|
from apps.locations.serializers import CitySerializer, NeighborhoodSerializer
|
|
|
|
from .models import Store, StoreBankAccount, StoreCategory, StoreWorkingHours
|
|
|
|
|
|
class StoreCategorySerializer(serializers.ModelSerializer):
|
|
icon_url = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = StoreCategory
|
|
fields = ('uuid', 'name', 'icon', 'icon_url', 'order')
|
|
|
|
@extend_schema_field(serializers.URLField(allow_null=True))
|
|
def get_icon_url(self, obj):
|
|
return presigned_media_url(obj.icon)
|
|
|
|
|
|
class StoreWorkingHoursSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = StoreWorkingHours
|
|
fields = ('weekday', 'opens_at', 'closes_at', 'is_closed')
|
|
|
|
|
|
class StoreListSerializer(serializers.ModelSerializer):
|
|
category = StoreCategorySerializer(read_only=True)
|
|
logo_url = serializers.SerializerMethodField()
|
|
cover_image_url = serializers.SerializerMethodField()
|
|
distance_km = serializers.SerializerMethodField()
|
|
latitude = serializers.FloatField(read_only=True)
|
|
longitude = serializers.FloatField(read_only=True)
|
|
|
|
class Meta:
|
|
model = Store
|
|
fields = (
|
|
'uuid', 'name', 'category', 'logo', 'logo_url', 'cover_image', 'cover_image_url',
|
|
'phone_number', 'rating_avg', 'rating_count', 'min_order_amount', 'delivery_fee',
|
|
'free_delivery_threshold', 'is_open', 'distance_km', 'latitude', 'longitude',
|
|
'max_delivery_time_minutes',
|
|
)
|
|
|
|
@extend_schema_field(serializers.URLField(allow_null=True))
|
|
def get_logo_url(self, obj):
|
|
return presigned_media_url(obj.logo)
|
|
|
|
@extend_schema_field(serializers.URLField(allow_null=True))
|
|
def get_cover_image_url(self, obj):
|
|
return presigned_media_url(obj.cover_image)
|
|
|
|
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
def get_distance_km(self, obj):
|
|
"""Only present when the request passed `lat`/`lng` (see StoreViewSet.get_queryset)."""
|
|
distance = getattr(obj, 'distance', None)
|
|
return round(distance.km, 2) if distance is not None else None
|
|
|
|
|
|
class StoreDetailSerializer(StoreListSerializer):
|
|
working_hours = StoreWorkingHoursSerializer(many=True, read_only=True)
|
|
|
|
class Meta(StoreListSerializer.Meta):
|
|
fields = StoreListSerializer.Meta.fields + (
|
|
'description', 'address',
|
|
'accepts_wallet', 'accepts_online', 'accepts_cash_on_delivery', 'working_hours',
|
|
)
|
|
|
|
|
|
class TopProductSerializer(serializers.Serializer):
|
|
name = serializers.CharField()
|
|
units_sold = serializers.IntegerField()
|
|
revenue = serializers.IntegerField()
|
|
|
|
|
|
class SalesChartPointSerializer(serializers.Serializer):
|
|
bucket = serializers.DateTimeField()
|
|
sales = serializers.IntegerField()
|
|
|
|
|
|
class SellerAnalyticsSerializer(serializers.Serializer):
|
|
period = serializers.CharField()
|
|
order_count = serializers.IntegerField()
|
|
total_sales = serializers.IntegerField()
|
|
cancellation_rate = serializers.FloatField()
|
|
average_order_value = serializers.IntegerField()
|
|
chart = SalesChartPointSerializer(many=True)
|
|
top_products = TopProductSerializer(many=True)
|
|
|
|
|
|
class StoreBankAccountSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = StoreBankAccount
|
|
fields = ('iban', 'account_holder_name')
|
|
|
|
|
|
class SellerStoreSerializer(serializers.ModelSerializer):
|
|
"""Read/write serializer for the authenticated seller's own store."""
|
|
|
|
category = StoreCategorySerializer(read_only=True)
|
|
category_uuid = serializers.PrimaryKeyRelatedField(
|
|
source='category', queryset=StoreCategory.objects.all(), write_only=True,
|
|
)
|
|
city = CitySerializer(read_only=True)
|
|
city_uuid = serializers.PrimaryKeyRelatedField(
|
|
source='city', queryset=City.objects.filter(is_active=True), write_only=True,
|
|
)
|
|
service_neighborhoods = NeighborhoodSerializer(many=True, read_only=True)
|
|
service_neighborhood_uuids = serializers.PrimaryKeyRelatedField(
|
|
source='service_neighborhoods', queryset=Neighborhood.objects.filter(is_active=True),
|
|
write_only=True, many=True, required=False,
|
|
)
|
|
latitude = serializers.FloatField(required=False)
|
|
longitude = serializers.FloatField(required=False)
|
|
bank_account = StoreBankAccountSerializer(read_only=True)
|
|
logo_url = serializers.SerializerMethodField()
|
|
cover_image_url = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Store
|
|
fields = (
|
|
'uuid', 'name', 'category', 'category_uuid', 'description', 'logo', 'logo_url',
|
|
'cover_image', 'cover_image_url',
|
|
'phone_number', 'city', 'city_uuid', 'address', 'latitude', 'longitude',
|
|
'service_neighborhoods', 'service_neighborhood_uuids', 'delivery_radius_km',
|
|
'max_delivery_time_minutes',
|
|
'min_order_amount', 'delivery_fee', 'free_delivery_threshold',
|
|
'accepts_wallet', 'accepts_online', 'accepts_cash_on_delivery',
|
|
'rating_avg', 'rating_count', 'status', 'is_open', 'bank_account', 'created_at',
|
|
)
|
|
read_only_fields = ('rating_avg', 'rating_count', 'status')
|
|
|
|
@extend_schema_field(serializers.URLField(allow_null=True))
|
|
def get_logo_url(self, obj):
|
|
return presigned_media_url(obj.logo)
|
|
|
|
@extend_schema_field(serializers.URLField(allow_null=True))
|
|
def get_cover_image_url(self, obj):
|
|
return presigned_media_url(obj.cover_image)
|
|
|
|
def _pop_location(self, validated_data):
|
|
lat = validated_data.pop('latitude', None)
|
|
lng = validated_data.pop('longitude', None)
|
|
if lat is not None and lng is not None:
|
|
validated_data['location'] = Point(lng, lat, srid=4326)
|
|
|
|
def create(self, validated_data):
|
|
self._pop_location(validated_data)
|
|
validated_data['owner'] = self.context['request'].user
|
|
return super().create(validated_data)
|
|
|
|
def update(self, instance, validated_data):
|
|
self._pop_location(validated_data)
|
|
return super().update(instance, validated_data)
|