From 9211894b2320001462702c8b0a620e42a53ff0bb Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Thu, 27 Aug 2020 14:45:37 +0200 Subject: [PATCH] implement basic logic for updating a recurring order Signed-off-by: Nico Schottelius --- uncloud_pay/models.py | 63 ++++++++++++++++++++++++++++++++----------- uncloud_pay/tests.py | 61 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 103 insertions(+), 21 deletions(-) diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 1f9b61a..1d95ae1 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -608,6 +608,20 @@ class BillRecord(models.Model): # Products class Product(UncloudModel): + """ + A product is something a user orders. To record the pricing, we + create order that define a state in time. + + A product can *depend* on other products. + + A product can have *one* one_time_order and/or *one* + recurring_order. + + If either of them needs to be updated, a new order of the same + type will be created and links to the previous order. + + """ + owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, editable=False) @@ -623,7 +637,23 @@ class Product(UncloudModel): # Default period for all products default_recurring_period = RecurringPeriod.PER_30D - def create_order_at(self, when_to_start=None, recurring_period=None): + @property + def recurring_orders(self): + return self.orders.order_by('id').exclude(recurring_period=RecurringPeriod.ONE_TIME) + + @property + def last_recurring_order(self): + return self.recurring_orders.last() + + @property + def one_time_orders(self): + return self.orders.order_by('id').filter(recurring_period=RecurringPeriod.ONE_TIME) + + @property + def last_one_time_order(self): + return self.one_time_orders.last() + + def create_order(self, when_to_start=None, recurring_period=None): billing_address = BillingAddress.get_address_for(self.owner) if not billing_address: @@ -636,7 +666,8 @@ class Product(UncloudModel): recurring_period = self.default_recurring_period - if self.one_time_price > 0: + # Create one time order if we did not create one already + if self.one_time_price > 0 and not self.last_one_time_order: one_time_order = Order.objects.create(owner=self.owner, billing_address=billing_address, starting_date=when_to_start, @@ -667,32 +698,32 @@ class Product(UncloudModel): def create_or_update_recurring_order(self, when_to_start=None, recurring_period=None): + + if not self.recurring_price: + return + + if not recurring_period: + recurring_period = self.default_recurring_period + if not when_to_start: when_to_start = timezone.now() -# current_recurring_order = Order.objects.filter( - # NEXT: find the latest order, use that one... - # Update order = create new order - if self.order: - previous_order = self.order + if self.last_recurring_order: when_to_end = end_before(when_to_start) new_order = Order.objects.create(owner=self.owner, - billing_address=self.order.billing_address, + billing_address=self.last_recurring_order.billing_address, starting_date=when_to_start, price=self.recurring_price, recurring_period=recurring_period, description=str(self), - replaces=self.order) - - print(new_order) - self.order.end_date = when_to_end - self.order.save() - - self.order = new_order + replaces=self.last_recurring_order) + self.last_recurring_order.end_date = when_to_end + self.orders.add(new_order) else: - return self.create_order_at(when_to_start, recurring_period) + # This might be a bug as it might (re-)create the one time order + self.create_order(when_to_start, recurring_period) @property def recurring_price(self): diff --git a/uncloud_pay/tests.py b/uncloud_pay/tests.py index 0ee5abc..6586f3a 100644 --- a/uncloud_pay/tests.py +++ b/uncloud_pay/tests.py @@ -65,26 +65,37 @@ class ProductTestCase(TestCase): # One order p = SampleOneTimeProduct.objects.create(owner=self.user) - p.create_order_at(timezone.make_aware(datetime.datetime(2020,3,3))) + p.create_order(timezone.make_aware(datetime.datetime(2020,3,3))) order_count = Order.objects.filter(owner=self.user).count() self.assertEqual(order_count, 1) # One more order p = SampleRecurringProduct.objects.create(owner=self.user) - p.create_order_at(timezone.make_aware(datetime.datetime(2020,3,3))) + p.create_order(timezone.make_aware(datetime.datetime(2020,3,3))) order_count = Order.objects.filter(owner=self.user).count() self.assertEqual(order_count, 2) # Should create 2 orders p = SampleRecurringProductOneTimeFee.objects.create(owner=self.user) - p.create_order_at(timezone.make_aware(datetime.datetime(2020,3,3))) + p.create_order(timezone.make_aware(datetime.datetime(2020,3,3))) order_count = Order.objects.filter(owner=self.user).count() self.assertEqual(order_count, 4) + def test_update_recurring_order(self): + """ + Ensure creating orders from product only creates 1 order + """ + + p = SampleRecurringProduct.objects.create(owner=self.user) + p.create_order(timezone.make_aware(datetime.datetime(2020,3,3))) + + p.create_or_update_recurring_order(timezone.make_aware(datetime.datetime(2020,3,3))) + + class BillingAddressTestCase(TestCase): def setUp(self): self.user = get_user_model().objects.create( @@ -351,6 +362,15 @@ class BillTestCase(TestCase): self.assertEqual(len(bills), 2) + # TO BE IMPLEMENTED -- once orders can be marked as "done" / "inactive" / "not for billing" + # def test_skip_disabled_orders(self): + # """ + # Ensure that a bill only considers "active" orders + # """ + + # self.assertEqual(1, 2) + + class ModifyProductTestCase(TestCase): def setUp(self): self.user = get_user_model().objects.create( @@ -367,7 +387,7 @@ class ModifyProductTestCase(TestCase): def test_no_pro_rata_first_bill(self): """ - The bill should NOT contain a partial amount + The bill should NOT contain a partial amount -- this is a BILL TEST :-) """ price = 5 @@ -380,7 +400,7 @@ class ModifyProductTestCase(TestCase): ending_date = timezone.make_aware(datetime.datetime(2020,3,31)) time_diff = (ending_date - starting_date).total_seconds() - product.create_order_at(starting_date) + product.create_order(starting_date) bills = Bill.create_next_bills_for_user(self.user, ending_date=ending_date) @@ -394,6 +414,37 @@ class ModifyProductTestCase(TestCase): self.assertNotEqual(bills[0].sum, pro_rata_amount * price) self.assertEqual(bills[0].sum, price) + def test_bill_for_modified_product(self): + """ + The bill should NOT contain a partial amount -- this is a BILL TEST :-) + """ + + price = 5 + + # Standard 30d recurring product + product = SampleRecurringProduct.objects.create(owner=self.user, + rc_price=price) + + starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) + ending_date = timezone.make_aware(datetime.datetime(2019,3,31)) + change_date = timezone.make_aware(datetime.datetime(2019,4,17)) + + product.create_order(starting_date) + + bills = Bill.create_next_bills_for_user(self.user, + ending_date=ending_date) + + product.rc_price = 10 + product.save() + + product.create_or_update_recurring_order(when_to_start=change_date) + + # expeted result: + # 1x 5 chf bill record + # 1x 5 chf bill record + # 1x 10 partial bill record + +