Compare commits

...

2 commits

Author SHA1 Message Date
ea9474633b Merge pull request 'Add Store.max_delivery_time_minutes, return it wherever store info appears' (#8) from feature/delivery-time into master
Reviewed-on: #8
2026-09-05 08:15:58 -04:00
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
8 changed files with 61 additions and 0 deletions

View file

@ -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).

View file

@ -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))

View file

@ -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)

View file

@ -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شود.'),
),
]

View file

@ -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)

View file

@ -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',

View file

@ -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)

View file

@ -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)