implement basic logic for updating a recurring order
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
This commit is contained in:
parent
b8b15704a3
commit
9211894b23
2 changed files with 103 additions and 21 deletions
|
@ -608,6 +608,20 @@ class BillRecord(models.Model):
|
||||||
# Products
|
# Products
|
||||||
|
|
||||||
class Product(UncloudModel):
|
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(),
|
owner = models.ForeignKey(get_user_model(),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
editable=False)
|
editable=False)
|
||||||
|
@ -623,7 +637,23 @@ class Product(UncloudModel):
|
||||||
# Default period for all products
|
# Default period for all products
|
||||||
default_recurring_period = RecurringPeriod.PER_30D
|
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)
|
billing_address = BillingAddress.get_address_for(self.owner)
|
||||||
|
|
||||||
if not billing_address:
|
if not billing_address:
|
||||||
|
@ -636,7 +666,8 @@ class Product(UncloudModel):
|
||||||
recurring_period = self.default_recurring_period
|
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,
|
one_time_order = Order.objects.create(owner=self.owner,
|
||||||
billing_address=billing_address,
|
billing_address=billing_address,
|
||||||
starting_date=when_to_start,
|
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):
|
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:
|
if not when_to_start:
|
||||||
when_to_start = timezone.now()
|
when_to_start = timezone.now()
|
||||||
|
|
||||||
# current_recurring_order = Order.objects.filter(
|
if self.last_recurring_order:
|
||||||
# NEXT: find the latest order, use that one...
|
|
||||||
# Update order = create new order
|
|
||||||
if self.order:
|
|
||||||
previous_order = self.order
|
|
||||||
when_to_end = end_before(when_to_start)
|
when_to_end = end_before(when_to_start)
|
||||||
|
|
||||||
new_order = Order.objects.create(owner=self.owner,
|
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,
|
starting_date=when_to_start,
|
||||||
price=self.recurring_price,
|
price=self.recurring_price,
|
||||||
recurring_period=recurring_period,
|
recurring_period=recurring_period,
|
||||||
description=str(self),
|
description=str(self),
|
||||||
replaces=self.order)
|
replaces=self.last_recurring_order)
|
||||||
|
|
||||||
print(new_order)
|
|
||||||
self.order.end_date = when_to_end
|
|
||||||
self.order.save()
|
|
||||||
|
|
||||||
self.order = new_order
|
|
||||||
|
|
||||||
|
self.last_recurring_order.end_date = when_to_end
|
||||||
|
self.orders.add(new_order)
|
||||||
else:
|
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
|
@property
|
||||||
def recurring_price(self):
|
def recurring_price(self):
|
||||||
|
|
|
@ -65,26 +65,37 @@ class ProductTestCase(TestCase):
|
||||||
|
|
||||||
# One order
|
# One order
|
||||||
p = SampleOneTimeProduct.objects.create(owner=self.user)
|
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()
|
order_count = Order.objects.filter(owner=self.user).count()
|
||||||
self.assertEqual(order_count, 1)
|
self.assertEqual(order_count, 1)
|
||||||
|
|
||||||
# One more order
|
# One more order
|
||||||
p = SampleRecurringProduct.objects.create(owner=self.user)
|
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()
|
order_count = Order.objects.filter(owner=self.user).count()
|
||||||
self.assertEqual(order_count, 2)
|
self.assertEqual(order_count, 2)
|
||||||
|
|
||||||
# Should create 2 orders
|
# Should create 2 orders
|
||||||
p = SampleRecurringProductOneTimeFee.objects.create(owner=self.user)
|
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()
|
order_count = Order.objects.filter(owner=self.user).count()
|
||||||
self.assertEqual(order_count, 4)
|
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):
|
class BillingAddressTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = get_user_model().objects.create(
|
self.user = get_user_model().objects.create(
|
||||||
|
@ -351,6 +362,15 @@ class BillTestCase(TestCase):
|
||||||
self.assertEqual(len(bills), 2)
|
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):
|
class ModifyProductTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = get_user_model().objects.create(
|
self.user = get_user_model().objects.create(
|
||||||
|
@ -367,7 +387,7 @@ class ModifyProductTestCase(TestCase):
|
||||||
|
|
||||||
def test_no_pro_rata_first_bill(self):
|
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
|
price = 5
|
||||||
|
@ -380,7 +400,7 @@ class ModifyProductTestCase(TestCase):
|
||||||
ending_date = timezone.make_aware(datetime.datetime(2020,3,31))
|
ending_date = timezone.make_aware(datetime.datetime(2020,3,31))
|
||||||
time_diff = (ending_date - starting_date).total_seconds()
|
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,
|
bills = Bill.create_next_bills_for_user(self.user,
|
||||||
ending_date=ending_date)
|
ending_date=ending_date)
|
||||||
|
@ -394,6 +414,37 @@ class ModifyProductTestCase(TestCase):
|
||||||
self.assertNotEqual(bills[0].sum, pro_rata_amount * price)
|
self.assertNotEqual(bills[0].sum, pro_rata_amount * price)
|
||||||
self.assertEqual(bills[0].sum, 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue