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

1from django.db import transaction 

2from django.db.models import Sum, F 

3 

4from rest_framework import viewsets, status 

5from rest_framework.decorators import action 

6from rest_framework.response import Response 

7from rest_framework.exceptions import ValidationError 

8 

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 

13 

14 

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 """ 

20 

21 serializer_class = ShoppingBagSerializer 

22 

23 def get_queryset(self): 

24 try: 

25 user_filters = ShoppingBagService.get_user_identifier(self.request) 

26 

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

36 

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'] 

47 

48 # Get the inventory object and validate stock availability 

49 ShoppingBagService.validate_inventory_quantity( 

50 inventory, quantity_to_add 

51 ) 

52 

53 # Create filters for finding existing bag item 

54 filters = {'inventory': inventory, **user_filters} 

55 

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 ) 

60 

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']) 

70 

71 # Set the instance for the serializer 

72 serializer.instance = bag_item 

73 

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 ) 

83 

84 # If quantity is 0 or negative, delete the item 

85 if new_quantity <= 0: 

86 return self.perform_destroy(instance) 

87 

88 # Get inventory object for validation 

89 inventory = instance.inventory 

90 

91 # Calculate the change in quantity 

92 quantity_delta = new_quantity - instance.quantity 

93 

94 # If adding more items, validate stock availability 

95 if quantity_delta > 0: 

96 ShoppingBagService.validate_inventory_quantity( 

97 inventory, quantity_delta 

98 ) 

99 

100 # Save the updated instance 

101 serializer.save() 

102 

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 """ 

109 

110 # Delete the shopping bag item 

111 instance.delete() 

112 

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. 

117 

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) 

124 

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 ) 

132 

133 return Response( 

134 {'count': count}, 

135 status=status.HTTP_200_OK, 

136 ) 

137 

138 except ValidationError as e: 

139 return Response( 

140 e.detail, 

141 status=status.HTTP_400_BAD_REQUEST, 

142 ) 

143 

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. 

148 

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) 

155 

156 total_price = ( 

157 ShoppingBag.objects.filter(**user_filters).aggregate( 

158 total=Sum(F('inventory__price') * F('quantity')) 

159 )['total'] 

160 or 0 

161 ) 

162 

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 ) 

174 

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 )