Coverage for src/orders/serializers.py: 91%

65 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-08-04 12:59 +0300

1from rest_framework import serializers 

2 

3from src.orders.constants import CardFieldLengths 

4from src.orders.models import Order 

5from src.orders.services import OrderService, PaymentValidationService 

6from src.common.mixins import InventoryMixin 

7 

8 

9class OrderSerializer(serializers.ModelSerializer): 

10 """ 

11 Serializer for the Order model. 

12 

13 Key Features: 

14 - Serializes order details, including inventory, status, and user. 

15 - Excludes size and quantity fields to ensure only unique products are shown in order history. 

16 - Includes product_info (details about the product from inventory, with size removed). 

17 - Includes product_content_type and product_object_id for review integration (used by frontend to submit reviews for the correct product). 

18 - Calculates total price for the order item. 

19 """ 

20 

21 inventory = serializers.PrimaryKeyRelatedField( 

22 queryset=Order._meta.get_field('inventory').related_model.objects.all() 

23 ) 

24 product_info = serializers.SerializerMethodField() 

25 total_price = serializers.SerializerMethodField() 

26 status_display = serializers.CharField( 

27 source='get_status_display', read_only=True 

28 ) 

29 product_content_type = serializers.SerializerMethodField() 

30 product_object_id = serializers.SerializerMethodField() 

31 

32 class Meta: 

33 model = Order 

34 fields = [ 

35 'id', 

36 'user', 

37 'status', 

38 'status_display', 

39 'created_at', 

40 'order_group', 

41 'inventory', 

42 'product_info', 

43 'total_price', 

44 'product_content_type', 

45 'product_object_id', 

46 ] 

47 read_only_fields = [ 

48 'id', 

49 'created_at', 

50 'user', 

51 'order_group', 

52 'product_info', 

53 'total_price', 

54 'status_display', 

55 'product_content_type', 

56 'product_object_id', 

57 ] 

58 depth = 3 

59 

60 def get_product_info(self, obj): 

61 """ 

62 Returns product details for the order item, using the related inventory's product. 

63 Removes size from the product_info if present. 

64 """ 

65 info = InventoryMixin.get_product_info(obj) 

66 if isinstance(info, dict) and 'size' in info: 

67 info.pop('size') 

68 

69 return info 

70 

71 def get_total_price(self, obj): 

72 """ 

73 Calculates the total price for this order item (price * quantity). 

74 """ 

75 return InventoryMixin.get_total_price_per_product(obj) 

76 

77 def get_product_content_type(self, obj): 

78 """ 

79 Returns the model name of the related product (e.g., 'earwear'), used for review integration. 

80 """ 

81 inventory = obj.inventory 

82 if not inventory: 

83 return None 

84 

85 product = getattr(inventory, 'product', None) 

86 if not product: 

87 return None 

88 

89 return product._meta.model_name 

90 

91 def get_product_object_id(self, obj): 

92 """ 

93 Returns the primary key of the related product, used for review integration. 

94 """ 

95 inventory = obj.inventory 

96 if not inventory: 

97 return None 

98 

99 product = getattr(inventory, 'product', None) 

100 if not product: 

101 return None 

102 

103 return product.id 

104 

105 

106class OrderGroupSerializer(serializers.Serializer): 

107 """ 

108 Serializer for a group of orders (products purchased together in one checkout). 

109 Aggregates order info, total price, and product details for the group. 

110 """ 

111 

112 order_group = serializers.UUIDField(read_only=True) 

113 status = serializers.CharField(read_only=True) 

114 status_display = serializers.CharField(read_only=True) 

115 created_at = serializers.DateTimeField(read_only=True) 

116 total_price = serializers.FloatField(read_only=True) 

117 total_items = serializers.IntegerField(read_only=True) 

118 products = OrderSerializer(many=True, read_only=True) 

119 

120 def to_representation(self, instance): 

121 """ 

122 Customizes the output format for grouped orders. 

123 """ 

124 if isinstance(instance, dict) and 'orders' in instance: 

125 orders = instance['orders'] 

126 if not orders: 

127 return {} 

128 

129 first_order = orders[0] 

130 total_items = sum(order.quantity for order in orders) 

131 

132 return { 

133 'order_group': str(first_order.order_group), 

134 'status': first_order.status, 

135 'status_display': first_order.get_status_display(), 

136 'created_at': first_order.created_at, 

137 'total_price': OrderService.calculate_order_group_total( 

138 str(first_order.order_group), first_order.user 

139 ), 

140 'total_items': total_items, 

141 'products': OrderSerializer(orders, many=True).data, 

142 } 

143 

144 return super().to_representation(instance) 

145 

146 

147class OrderCreateSerializer(serializers.Serializer): 

148 """ 

149 Serializer for creating an order from payment data. 

150 Validates credit card and payment info before processing the order. 

151 """ 

152 

153 card_number = serializers.CharField( 

154 max_length=CardFieldLengths.CARD_NUMBER_MAX_LENGTH 

155 ) 

156 card_holder_name = serializers.CharField( 

157 max_length=CardFieldLengths.CARD_HOLDER_NAME_MAX_LENGTH 

158 ) 

159 expiry_date = serializers.CharField( 

160 max_length=CardFieldLengths.EXPIRY_DATE_MAX_LENGTH 

161 ) 

162 cvv = serializers.CharField(max_length=CardFieldLengths.CVV_MAX_LENGTH) 

163 

164 def validate(self, data): 

165 """ 

166 Uses PaymentValidationService to check all payment fields for validity. 

167 """ 

168 PaymentValidationService.validate_payment_data( 

169 { 

170 'card_number': data.get('card_number'), 

171 'card_holder_name': data.get('card_holder_name'), 

172 'cvv': data.get('cvv'), 

173 'expiry_date': data.get('expiry_date'), 

174 } 

175 ) 

176 

177 return data