From 5117fe9b059f3e2c0c298cdf035818688ac854a8 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Wed, 2 Sep 2026 14:15:19 +0330 Subject: [PATCH] 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 --- CHANGELOG.md | 2 ++ apps/orders/serializers.py | 3 +++ apps/orders/tests/test_cart.py | 9 +++++++++ .../0003_store_max_delivery_time_minutes.py | 18 ++++++++++++++++++ apps/stores/models.py | 3 +++ apps/stores/serializers.py | 2 ++ apps/stores/tests/test_seller_store.py | 13 +++++++++++++ apps/stores/tests/test_store_list.py | 11 +++++++++++ 8 files changed, 61 insertions(+) create mode 100644 apps/stores/migrations/0003_store_max_delivery_time_minutes.py diff --git a/CHANGELOG.md b/CHANGELOG.md index fdb69be..fafbd7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ - Test coverage for `StoreViewSet` list filtering (`apps/stores/tests/test_store_list.py`) — previously untested. - Test coverage for `ProductViewSet` list filtering (`apps/catalog/tests/test_product_list.py`) — previously untested. - Test coverage for `CityViewSet`/`NeighborhoodViewSet` list filtering (`apps/locations/tests/test_location_list.py`) — previously untested. +- New `Store.max_delivery_time_minutes` field, set by the seller at registration (default 60), returned wherever a store appears in a response — store list/detail, the seller's own store, cart groups (`GET /api/v1/cart/`), and order/order-group serializers — so the checkout page's estimated-delivery-time UI no longer needs a frontend mock (`apps/stores/models.py`, `apps/stores/serializers.py`, migration `0003_store_max_delivery_time_minutes`). +- Documented `CartSerializer`'s `groups`/`grand_total` fields for swagger (`apps/orders/serializers.py`) — they were `SerializerMethodField`s with no schema, showing as opaque strings instead of the actual nested structure. ### Changed - Replaced hand-rolled `get_queryset` filtering with `django-filter` `FilterSet` classes (`apps/{stores,catalog,locations,reviews}/filters.py`) and `filterset_fields` (`apps/orders` status). `django-filter` was already installed and set as `DEFAULT_FILTER_BACKENDS` but unused everywhere; params are now auto-documented in swagger by drf-spectacular's django-filter integration, so the manual `OpenApiParameter` declarations for those fields were removed (stores keeps `lat`/`lng` manual since geo-distance isn't a plain filter). diff --git a/apps/orders/serializers.py b/apps/orders/serializers.py index e06ebd3..012ab4e 100644 --- a/apps/orders/serializers.py +++ b/apps/orders/serializers.py @@ -1,3 +1,4 @@ +from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from apps.catalog.models import Product @@ -47,9 +48,11 @@ class CartSerializer(serializers.Serializer): 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)) diff --git a/apps/orders/tests/test_cart.py b/apps/orders/tests/test_cart.py index 64167bb..5d50ded 100644 --- a/apps/orders/tests/test_cart.py +++ b/apps/orders/tests/test_cart.py @@ -15,6 +15,15 @@ class CartTests(OrdersTestCase): self.assertEqual(len(response.data['groups']), 2) self.assertEqual(response.data['grand_total'], (28_500 * 2 + 15_000) + (85_000 + 20_000)) + def test_cart_group_includes_store_max_delivery_time(self): + self.store1.max_delivery_time_minutes = 35 + self.store1.save(update_fields=['max_delivery_time_minutes']) + self.add_to_cart(self.product1, quantity=1) + + response = self.client.get('/api/v1/cart/') + + self.assertEqual(response.data['groups'][0]['store']['max_delivery_time_minutes'], 35) + def test_adding_same_product_twice_increments_quantity(self): self.add_to_cart(self.product1, quantity=1) self.add_to_cart(self.product1, quantity=2) diff --git a/apps/stores/migrations/0003_store_max_delivery_time_minutes.py b/apps/stores/migrations/0003_store_max_delivery_time_minutes.py new file mode 100644 index 0000000..7314436 --- /dev/null +++ b/apps/stores/migrations/0003_store_max_delivery_time_minutes.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.2 on 2026-09-02 10:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stores', '0002_alter_store_cover_image_alter_store_logo_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='store', + name='max_delivery_time_minutes', + field=models.PositiveSmallIntegerField(default=60, help_text='حداکثر زمان ارسال (دقیقه)، هنگام ثبت فروشگاه توسط فروشنده مشخص می\u200cشود.'), + ), + ] diff --git a/apps/stores/models.py b/apps/stores/models.py index 9014527..40d46dd 100644 --- a/apps/stores/models.py +++ b/apps/stores/models.py @@ -43,6 +43,9 @@ class Store(BaseModel): help_text='محله‌هایی که این فروشگاه به آن‌ها ارسال دارد.', ) delivery_radius_km = models.DecimalField(max_digits=5, decimal_places=2, default=5) + max_delivery_time_minutes = models.PositiveSmallIntegerField( + default=60, help_text='حداکثر زمان ارسال (دقیقه)، هنگام ثبت فروشگاه توسط فروشنده مشخص می‌شود.', + ) min_order_amount = models.PositiveBigIntegerField(default=0) delivery_fee = models.PositiveBigIntegerField(default=0) diff --git a/apps/stores/serializers.py b/apps/stores/serializers.py index 4f1169f..2471092 100644 --- a/apps/stores/serializers.py +++ b/apps/stores/serializers.py @@ -41,6 +41,7 @@ class StoreListSerializer(serializers.ModelSerializer): '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)) @@ -124,6 +125,7 @@ class SellerStoreSerializer(serializers.ModelSerializer): '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', diff --git a/apps/stores/tests/test_seller_store.py b/apps/stores/tests/test_seller_store.py index 7f7cf38..687a7d9 100644 --- a/apps/stores/tests/test_seller_store.py +++ b/apps/stores/tests/test_seller_store.py @@ -21,11 +21,24 @@ class SellerStoreTests(OrdersTestCase): 'address': 'خیابان ولیعصر', 'latitude': 35.75, 'longitude': 51.4, + 'max_delivery_time_minutes': 40, }) self.assertEqual(response.status_code, 201, response.data) self.assertTrue(Store.objects.filter(owner=self.new_seller).exists()) self.assertAlmostEqual(response.data['latitude'], 35.75) self.assertAlmostEqual(response.data['longitude'], 51.4) + self.assertEqual(response.data['max_delivery_time_minutes'], 40) + + def test_max_delivery_time_minutes_defaults_when_not_provided(self): + self.client.force_authenticate(user=self.new_seller) + response = self.client.post('/api/v1/seller/store/', { + 'name': 'کافه من', + 'category_uuid': str(self.category.uuid), + 'city_uuid': str(self.city.uuid), + 'address': 'خیابان ولیعصر', + }) + self.assertEqual(response.status_code, 201, response.data) + self.assertEqual(response.data['max_delivery_time_minutes'], 60) def test_seller_cannot_create_a_second_store(self): self.client.force_authenticate(user=self.seller1) diff --git a/apps/stores/tests/test_store_list.py b/apps/stores/tests/test_store_list.py index bc1e325..dfba36d 100644 --- a/apps/stores/tests/test_store_list.py +++ b/apps/stores/tests/test_store_list.py @@ -75,3 +75,14 @@ class StoreListFilterTests(OrdersTestCase): self.assertEqual(response.status_code, 200) self.assertAlmostEqual(response.data['latitude'], 35.75) self.assertAlmostEqual(response.data['longitude'], 51.4) + + def test_max_delivery_time_minutes_included_in_list_and_retrieve(self): + self.store1.max_delivery_time_minutes = 45 + self.store1.save(update_fields=['max_delivery_time_minutes']) + + list_response = self.client.get('/api/v1/stores/') + store = next(s for s in list_response.data['results'] if s['uuid'] == str(self.store1.uuid)) + self.assertEqual(store['max_delivery_time_minutes'], 45) + + detail_response = self.client.get(f'/api/v1/stores/{self.store1.uuid}/') + self.assertEqual(detail_response.data['max_delivery_time_minutes'], 45)