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>
This commit is contained in:
parent
dcf63952b5
commit
5117fe9b05
8 changed files with 61 additions and 0 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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شود.'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue