Coverage for products/utils/purchase_product.py: 97%
80 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-13 14:00 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-13 14:00 -0600
1from decimal import Decimal
2from datetime import date
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
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)
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
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
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)
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)
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
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)
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 )