Coverage for products/utils/purchase_product.py: 97%

80 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1from decimal import Decimal 

2from datetime import date 

3 

4from accounting.models import ProductCharge 

5from discounts.models import VolumeDiscount, MembershipDiscount 

6from products.utils import get_install_purchase_method 

7from ..models import ProductPriceVariation, ProductPurchase 

8from ..serializers import ProductPurchaseSerializer 

9 

10 

11class ProductPurchaseCreationHandler: 

12 def __init__(self, serializer): 

13 assert isinstance(serializer, ProductPurchaseSerializer) 

14 self.data = serializer.validated_data 

15 self.membership = self.data.get("membership", None) 

16 self.email = self.data.get("email", None) 

17 self.product = self.data.get("product") 

18 self.price_breakdown = {"base_price": str(self.product.price)} 

19 self.unit_price = self.product.price 

20 self.total_price = self.product.price 

21 self.purchase = ProductPurchase(**self.data) 

22 

23 def create(self, create_object: bool = True): 

24 """ 

25 Complete Purchase creation flow 

26 """ 

27 if self.membership: 27 ↛ 32line 27 didn't jump to line 32, because the condition on line 27 was never false

28 self._apply_price_variations() 

29 if not self.product.skip_discounts: 

30 self._apply_memberships_discounts() 

31 self._apply_volume_discounts() 

32 self._set_prices() 

33 if not create_object: 

34 return self.purchase 

35 self.purchase.save() 

36 self._create_charges() 

37 serialized_data = self._install_products() 

38 return self.purchase, serialized_data 

39 

40 def _apply_price_variations(self): 

41 """Filter and apply price variations appliable for this product-membership""" 

42 appliable_variations = ProductPriceVariation.objects.appliable_for(self.membership, self.product) 

43 appliable_variations = appliable_variations.filter( 

44 min_amount__lte=self.purchase.quantity, max_amount__gte=self.purchase.quantity 

45 ) 

46 if not appliable_variations: 

47 return None 

48 variations_modifier = 0 

49 self.price_breakdown["variations"] = [] 

50 for variation in appliable_variations: 

51 price_variation = variation.price_variation 

52 variations_modifier += price_variation 

53 self.price_breakdown["variations"].append( 

54 {"variation": variation.random_slug, "amount": str(price_variation)} 

55 ) 

56 self.price_breakdown["variations_modifier"] = str(variations_modifier) 

57 self.unit_price += variations_modifier 

58 

59 def _apply_volume_discounts(self): 

60 """Apply volume discounts for current purchase""" 

61 appliable_discounts = VolumeDiscount.objects.appliable_for(self.purchase) 

62 if not appliable_discounts: 

63 return None 

64 discounts = self.price_breakdown.get("discounts", []) 

65 total_discount_amount = self.price_breakdown.get("discount_amount", 0) 

66 for discount in appliable_discounts: 

67 discount_amount = discount.get_discount_amount(self.unit_price) 

68 total_discount_amount += discount_amount 

69 discounts.append( 

70 { 

71 "type": "volume", 

72 "discount": discount.random_slug, 

73 "amount": str(discount_amount), 

74 } 

75 ) 

76 self.price_breakdown["discounts"] = discounts 

77 self.price_breakdown["discount_amount"] = str(total_discount_amount) 

78 

79 def _apply_memberships_discounts(self): 

80 """Apply membership discounts for product category""" 

81 appliable_discounts = MembershipDiscount.objects.appliable_for(self.product.category, self.membership) 

82 if not appliable_discounts: 

83 return None 

84 discounts = self.price_breakdown.get("discounts", []) 

85 total_discount_amount = self.price_breakdown.get("discount_amount", 0) 

86 for discount in appliable_discounts: 

87 discount_amount = discount.get_discount_amount(self.unit_price) 

88 total_discount_amount += discount_amount 

89 discounts.append( 

90 { 

91 "type": "membership", 

92 "discount": discount.random_slug, 

93 "amount": str(discount_amount), 

94 } 

95 ) 

96 self.price_breakdown["discounts"] = discounts 

97 self.price_breakdown["discount_amount"] = str(total_discount_amount) 

98 

99 def _set_prices(self): 

100 """Set unit and total prices""" 

101 self.unit_price = self.unit_price - Decimal(self.price_breakdown.get("discount_amount", 0)) 

102 self.total_price = self.unit_price * self.purchase.quantity 

103 self.purchase.price_breakdown = self.price_breakdown 

104 self.purchase.total_price = self.total_price 

105 

106 def _install_products(self): 

107 """Install products to selected owners, if not owner relation defined, install for buyer membership""" 

108 method = get_install_purchase_method(self.product) 

109 return method(self.purchase) 

110 

111 def _create_charges(self): 

112 """Create purchase charges objects""" 

113 if self.membership: 113 ↛ 124line 113 didn't jump to line 124, because the condition on line 113 was never false

114 self.charge = ProductCharge.objects.create( 

115 membership=self.membership, 

116 product=self.product, 

117 purchase=self.purchase, 

118 quantity=self.purchase.quantity, 

119 date=date.today(), 

120 amount=self.total_price, 

121 concept=f"{self.purchase.quantity} {self.product.name}", 

122 ) 

123 else: 

124 self.charge = ProductCharge.objects.create( 

125 organization=self.product.organization, 

126 product=self.product, 

127 purchase=self.purchase, 

128 quantity=self.purchase.quantity, 

129 date=date.today(), 

130 amount=self.total_price, 

131 concept=f"{self.purchase.quantity} {self.product.name}", 

132 )