Compare commits
2 commits
aff0e77339
...
3deb360acb
| Author | SHA1 | Date | |
|---|---|---|---|
| 3deb360acb | |||
| 85fe57d1ca |
4 changed files with 56 additions and 4 deletions
|
|
@ -6,6 +6,8 @@
|
|||
- `GET /api/v1/stores/` — documented the `category`, `neighborhood`, `search`, `lat`, and `lng` query params for drf-spectacular so they render in swagger. The filtering itself already worked; only the OpenAPI schema was missing them (`apps/stores/views.py`).
|
||||
- `GET /api/v1/products/` — same issue: documented the existing `store`, `category`, and `search` query params for swagger (`apps/catalog/views.py`).
|
||||
- Cart items: `CartItemView` was registered on both `/api/v1/cart/items/` and `/api/v1/cart/items/{item_uuid}/`, so swagger showed POST/PATCH/DELETE on both paths — 3 of those 6 combinations crashed with a `TypeError` at runtime (`item_uuid` missing or unexpected). Split into `CartItemView` (POST only, list path) and `CartItemDetailView` (PATCH/DELETE only, detail path) so swagger only shows the combinations that actually work (`apps/orders/views.py`, `apps/orders/urls.py`).
|
||||
- Cancelling an order never restored the product stock that `checkout()` decremented, so cancelled orders' items stayed permanently out of stock. Added `_restock_order_items()`, run on the `CANCELLED` transition (`apps/orders/services.py`).
|
||||
- `POST /api/v1/checkout/` always checked out the customer's entire cart, even when it held items from stores the customer didn't intend to order from yet. It now checks out only the requested stores when `store_uuids` is given (default: whole cart, unchanged), and only clears those stores' items — the rest of the cart is left intact for a later checkout (`apps/orders/serializers.py`, `apps/orders/services.py`).
|
||||
|
||||
### Added
|
||||
- `GET /api/v1/cities/?search=` — search cities by name (`apps/locations/views.py`); previously no text search existed.
|
||||
|
|
|
|||
|
|
@ -109,6 +109,10 @@ class CheckoutSerializer(serializers.Serializer):
|
|||
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='')
|
||||
store_uuids = serializers.ListField(
|
||||
child=serializers.UUIDField(), required=False, allow_empty=False, default=None,
|
||||
help_text='بررسی و پرداخت فقط برای فروشگاههای انتخابشده از سبد خرید. در صورت خالی بودن، کل سبد خرید نهایی میشود.',
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs.get('delivery_type') == OrderGroup.DeliveryType.SCHEDULED and not attrs.get('scheduled_at'):
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from utils.exceptions import Conflict
|
|||
|
||||
from apps.catalog.models import Product
|
||||
|
||||
from .models import Cart, Notification, Order, OrderGroup, OrderItem, OrderStatusLog
|
||||
from .models import Cart, CartItem, Notification, Order, OrderGroup, OrderItem, OrderStatusLog
|
||||
|
||||
|
||||
def build_cart_groups(cart):
|
||||
|
|
@ -59,14 +59,27 @@ def _build_full_address(address):
|
|||
|
||||
|
||||
@transaction.atomic
|
||||
def checkout(user, address, delivery_type, scheduled_at, payment_method, notes):
|
||||
"""Splits the user's multi-store cart into one Order per store."""
|
||||
def checkout(user, address, delivery_type, scheduled_at, payment_method, notes, store_uuids=None):
|
||||
"""Splits the user's multi-store cart into one Order per store.
|
||||
|
||||
By default checks out the whole cart. If `store_uuids` is given, only those stores'
|
||||
groups are turned into orders and cleared from the cart — the rest of the cart (from
|
||||
other stores) is left untouched for a later checkout.
|
||||
"""
|
||||
cart = Cart.objects.filter(user=user).first()
|
||||
if not cart or not cart.items.exists():
|
||||
raise ValidationError({'cart': 'سبد خرید شما خالی است.'})
|
||||
|
||||
groups = build_cart_groups(cart)
|
||||
|
||||
if store_uuids is not None:
|
||||
wanted = {str(uuid) for uuid in store_uuids}
|
||||
found = {str(group['store'].uuid) for group in groups}
|
||||
missing = wanted - found
|
||||
if missing:
|
||||
raise ValidationError({'store_uuids': f'فروشگاه(های) {", ".join(missing)} در سبد خرید شما نیستند.'})
|
||||
groups = [group for group in groups if str(group['store'].uuid) in wanted]
|
||||
|
||||
for group in groups:
|
||||
store = group['store']
|
||||
if not group['meets_minimum_order']:
|
||||
|
|
@ -134,7 +147,8 @@ def checkout(user, address, delivery_type, scheduled_at, payment_method, notes):
|
|||
item.product.sold_count += item.quantity
|
||||
item.product.save(update_fields=['stock_quantity', 'sold_count', 'updated_at'])
|
||||
|
||||
cart.items.all().delete()
|
||||
checked_out_item_ids = [item.pk for group in groups for item in group['items']]
|
||||
CartItem.objects.filter(pk__in=checked_out_item_ids).delete()
|
||||
|
||||
return order_group
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,38 @@ class CheckoutTests(OrdersTestCase):
|
|||
self.product1.refresh_from_db()
|
||||
self.assertEqual(self.product1.stock_quantity, 48)
|
||||
|
||||
def test_checkout_can_be_limited_to_selected_stores(self):
|
||||
self.add_to_cart(self.product1, quantity=1)
|
||||
self.add_to_cart(self.product2, quantity=1)
|
||||
|
||||
response = self.client.post('/api/v1/checkout/', {
|
||||
'address_uuid': str(self.address.uuid),
|
||||
'payment_method': OrderGroup.PaymentMethod.CASH_ON_DELIVERY,
|
||||
'store_uuids': [str(self.store1.uuid)],
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 201, response.data)
|
||||
group = OrderGroup.objects.get(uuid=response.data['uuid'])
|
||||
self.assertEqual(group.orders.count(), 1)
|
||||
self.assertEqual(group.orders.get().store, self.store1)
|
||||
|
||||
# store2's cart item is left untouched for a later checkout.
|
||||
remaining_cart = Cart.objects.get(user=self.customer)
|
||||
self.assertEqual(remaining_cart.items.count(), 1)
|
||||
self.assertEqual(remaining_cart.items.get().product, self.product2)
|
||||
|
||||
def test_checkout_rejects_store_uuid_not_in_cart(self):
|
||||
self.add_to_cart(self.product1, quantity=1)
|
||||
|
||||
response = self.client.post('/api/v1/checkout/', {
|
||||
'address_uuid': str(self.address.uuid),
|
||||
'payment_method': OrderGroup.PaymentMethod.CASH_ON_DELIVERY,
|
||||
'store_uuids': [str(self.store2.uuid)],
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertTrue(Cart.objects.get(user=self.customer).items.exists())
|
||||
|
||||
def test_checkout_rejects_empty_cart(self):
|
||||
response = self.client.post('/api/v1/checkout/', {
|
||||
'address_uuid': str(self.address.uuid),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue