Coverage for src/shopping_bags/views.py: 88%
65 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-08-04 12:59 +0300
« prev ^ index » next coverage.py v7.9.2, created at 2025-08-04 12:59 +0300
1from django.db import transaction
2from django.db.models import Sum, F
4from rest_framework import viewsets, status
5from rest_framework.decorators import action
6from rest_framework.response import Response
7from rest_framework.exceptions import ValidationError
9from src.shopping_bags.models import ShoppingBag
10from src.shopping_bags.serializers import ShoppingBagSerializer
11from src.shopping_bags.services import ShoppingBagService
12from src.shopping_bags.constants import ShoppingBagErrorMessages
15class ShoppingBagViewSet(viewsets.ModelViewSet):
16 """
17 This ViewSet provides full CRUD operations for shopping bag items and additional
18 custom actions for getting bag count and total price.
19 """
21 serializer_class = ShoppingBagSerializer
23 def get_queryset(self):
24 try:
25 user_filters = ShoppingBagService.get_user_identifier(self.request)
27 return ShoppingBag.objects.filter(**user_filters).select_related(
28 # select_related() performs a SQL JOIN to fetch related objects
29 # This reduces the number of database queries
30 'inventory',
31 'user',
32 )
33 except ValidationError:
34 # Return empty queryset if user identification fails
35 return ShoppingBag.objects.none()
37 @transaction.atomic
38 def perform_create(self, serializer):
39 """
40 This method handles the creation of shopping bag items.
41 """
42 # Get user identification filters
43 user_filters = ShoppingBagService.get_user_identifier(self.request)
44 validated_data = serializer.validated_data
45 inventory = validated_data['inventory']
46 quantity_to_add = validated_data['quantity']
48 # Get the inventory object and validate stock availability
49 ShoppingBagService.validate_inventory_quantity(
50 inventory, quantity_to_add
51 )
53 # Create filters for finding existing bag item
54 filters = {'inventory': inventory, **user_filters}
56 # Get or create the bag item
57 bag_item, created = ShoppingBagService.get_or_create_bag_item(
58 filters=filters, defaults={'quantity': quantity_to_add}
59 )
61 if not created:
62 # If item already exists, add to existing quantity
63 new_total_quantity = bag_item.quantity + quantity_to_add
64 # Re-validate to ensure we don't exceed stock
65 ShoppingBagService.validate_inventory_quantity(
66 inventory, quantity_to_add
67 )
68 bag_item.quantity = new_total_quantity
69 bag_item.save(update_fields=['quantity'])
71 # Set the instance for the serializer
72 serializer.instance = bag_item
74 @transaction.atomic
75 def perform_update(self, serializer):
76 """
77 This method handles updating shopping bag item quantities.
78 """
79 instance = self.get_object()
80 new_quantity = serializer.validated_data.get(
81 'quantity', instance.quantity
82 )
84 # If quantity is 0 or negative, delete the item
85 if new_quantity <= 0:
86 return self.perform_destroy(instance)
88 # Get inventory object for validation
89 inventory = instance.inventory
91 # Calculate the change in quantity
92 quantity_delta = new_quantity - instance.quantity
94 # If adding more items, validate stock availability
95 if quantity_delta > 0:
96 ShoppingBagService.validate_inventory_quantity(
97 inventory, quantity_delta
98 )
100 # Save the updated instance
101 serializer.save()
103 @transaction.atomic
104 def perform_destroy(self, instance):
105 """
106 This method handles the deletion of shopping bag items and restores
107 the corresponding inventory quantity.
108 """
110 # Delete the shopping bag item
111 instance.delete()
113 @action(detail=False, methods=['get'], url_path='count')
114 def get_bag_count(self, request):
115 """
116 Get the total count of items in the shopping bag.
118 This custom action calculates the total quantity of all items in the user's
119 shopping bag, regardless of product type.
120 """
121 try:
122 # Get user identification filters
123 user_filters = ShoppingBagService.get_user_identifier(request)
125 # Aggregate the sum of all quantities in the user's bag
126 count = (
127 ShoppingBag.objects.filter(**user_filters).aggregate(
128 total=Sum('quantity')
129 )['total']
130 or 0
131 )
133 return Response(
134 {'count': count},
135 status=status.HTTP_200_OK,
136 )
138 except ValidationError as e:
139 return Response(
140 e.detail,
141 status=status.HTTP_400_BAD_REQUEST,
142 )
144 @action(detail=False, methods=['get'], url_path='total-price')
145 def get_total_price(self, request):
146 """
147 Get the total price of all items in the shopping bag.
149 This custom action calculates the total cost of all items in the user's
150 shopping bag by multiplying each item's price by its quantity.
151 """
152 try:
153 # Get user identification filters
154 user_filters = ShoppingBagService.get_user_identifier(request)
156 total_price = (
157 ShoppingBag.objects.filter(**user_filters).aggregate(
158 total=Sum(F('inventory__price') * F('quantity'))
159 )['total']
160 or 0
161 )
163 return Response(
164 {
165 'total_price': round(total_price, 2),
166 },
167 status=status.HTTP_200_OK,
168 )
169 except ValidationError as e:
170 return Response(
171 e.detail,
172 status=status.HTTP_400_BAD_REQUEST,
173 )
175 except Exception:
176 # Handle any unexpected errors during price calculation
177 return Response(
178 {
179 'error': ShoppingBagErrorMessages.ERROR_TOTAL_PRICE,
180 },
181 status=status.HTTP_500_INTERNAL_SERVER_ERROR,
182 )