diff --git a/uncloud_pay/migrations/0025_billrecord_is_recurring_record.py b/uncloud_pay/migrations/0025_billrecord_is_recurring_record.py new file mode 100644 index 0000000..5e3e141 --- /dev/null +++ b/uncloud_pay/migrations/0025_billrecord_is_recurring_record.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1 on 2020-09-28 20:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0024_auto_20200928_1945'), + ] + + operations = [ + migrations.AddField( + model_name='billrecord', + name='is_recurring_record', + field=models.BooleanField(default=False), + preserve_default=False, + ), + ] diff --git a/uncloud_pay/migrations/0026_order_should_be_billed.py b/uncloud_pay/migrations/0026_order_should_be_billed.py new file mode 100644 index 0000000..c32c688 --- /dev/null +++ b/uncloud_pay/migrations/0026_order_should_be_billed.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-09-28 20:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0025_billrecord_is_recurring_record'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='should_be_billed', + field=models.BooleanField(default=True), + ), + ] diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 988bd2c..5f90b02 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -592,6 +592,8 @@ class Order(models.Model): null=True) + should_be_billed = models.BooleanField(default=True) + @property def earliest_ending_date(self): """ @@ -702,24 +704,22 @@ class Order(models.Model): if self.ending_date and self.ending_date < self.starting_date: raise ValidationError("End date cannot be before starting date") - # do not check this if we upgrade - # if self.ending_date and self.ending_date < self.earliest_ending_date: - # raise ValidationError("Ending date is before minimum duration (starting_date + recurring period)") - super().save(*args, **kwargs) def create_bill_record(self, bill): br = None - if self.is_one_time: - if self.billrecord_set.count() == 0: - br = BillRecord.objects.create(bill=bill, - order=self, - starting_date=self.starting_date, - ending_date=self.ending_date) - else: - br = BillRecord.objects.filter(bill=bill, order=self).first() + if self.one_time_price != 0 and self.billrecord_set.count() == 0: + br = BillRecord.objects.create(bill=bill, + order=self, + starting_date=self.starting_date, + ending_date=self.starting_date, + is_recurring_record=False) + + if self.recurring_price != 0: + br = BillRecord.objects.filter(bill=bill, order=self, is_recurring_record=True).first() + if br: self.update_bill_record_for_recurring_order(br, bill) @@ -752,7 +752,7 @@ class Order(models.Model): Create a new bill record """ - last_bill_record = BillRecord.objects.filter(order=self).order_by('id').last() + last_bill_record = BillRecord.objects.filter(order=self, is_recurring_record=True).order_by('id').last() starting_date=self.starting_date @@ -772,9 +772,11 @@ class Order(models.Model): ending_date = self.get_ending_date_for_bill(bill) return BillRecord.objects.create(bill=bill, - order=self, - starting_date=starting_date, - ending_date=ending_date) + order=self, + starting_date=starting_date, + ending_date=ending_date, + is_recurring_record=True) + def save(self, *args, **kwargs): one_time_price = 0 @@ -799,7 +801,6 @@ class Order(models.Model): self.one_time_price = one_time_price self.recurring_price = recurring_price - super().save(*args, **kwargs) @@ -1102,10 +1103,12 @@ class BillRecord(models.Model): starting_date = models.DateTimeField() ending_date = models.DateTimeField() + is_recurring_record = models.BooleanField(blank=False, null=False) + @property def quantity(self): """ Determine the quantity by the duration""" - if self.order.is_one_time: + if not self.is_recurring_record: return 1 record_delta = self.ending_date - self.starting_date @@ -1114,10 +1117,18 @@ class BillRecord(models.Model): @property def sum(self): - return self.order.price * Decimal(self.quantity) + if self.is_recurring_record: + return self.order.recurring_price * Decimal(self.quantity) + else: + return self.order.one_time_price def __str__(self): - return f"{self.bill}: {self.quantity} x {self.order}" + if self.is_recurring_record: + bill_line = f"{self.starting_date} - {self.ending_date}: {self.quantity} x {self.order}" + else: + bill_line = f"{self.starting_date}: {self.order}" + + return bill_line def save(self, *args, **kwargs): if self.ending_date < self.starting_date: diff --git a/uncloud_pay/tests.py b/uncloud_pay/tests.py index c341706..dc02232 100644 --- a/uncloud_pay/tests.py +++ b/uncloud_pay/tests.py @@ -8,6 +8,25 @@ from uncloud_service.models import GenericServiceProduct import json +chocolate_product_config = { + 'features': { + 'gramm': + { 'min': 100, + 'max': 5000, + 'one_time_price': 0.2, + 'recurring_price': 0 + }, + }, +} + +chocolate_order_config = { + 'features': { + 'gramm': 500, + } +} + +chocolate_one_time_price = chocolate_order_config['features']['gramm'] * chocolate_product_config['features']['gramm']['one_time_price'] + vm_sample_product_config = { 'features': { 'cores': @@ -97,6 +116,190 @@ class OrderTestCase(TestCase): self.assertEqual(o.recurring_price, 16) + +class BillTestCase(TestCase): + """ + Test aspects of billing / creating a bill + """ + + def setUp(self): + self.user_without_address = get_user_model().objects.create( + username='no_home_person', + email='far.away@domain.tld') + + self.user = get_user_model().objects.create( + username='jdoe', + email='john.doe@domain.tld') + + self.recurring_user = get_user_model().objects.create( + username='recurrent_product_user', + email='jane.doe@domain.tld') + + self.user_addr = BillingAddress.objects.create( + owner=self.user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="unknown", + active=True) + + self.recurring_user_addr = BillingAddress.objects.create( + owner=self.recurring_user, + organization = 'Test org', + street="Somewhere", + city="Else", + postal_code="unknown", + active=True) + + self.order_meta = {} + self.order_meta[1] = { + 'starting_date': timezone.make_aware(datetime.datetime(2020,3,3)), + 'ending_date': timezone.make_aware(datetime.datetime(2020,4,17)), + 'price': 15, + 'description': 'One chocolate bar' + } + + self.chocolate = Product.objects.create(name="Swiss Chocolate", + description="Not only for testing, but for joy", + config=chocolate_product_config) + + # self.recurring_order = Order.objects.create( + # owner=self.recurring_user, + # starting_date=timezone.make_aware(datetime.datetime(2020,3,3)), + # recurring_period=RecurringPeriod.PER_30D, + # price=15, + # description="A pretty VM", + # billing_address=BillingAddress.get_address_for(self.recurring_user) + # ) + + # used for generating multiple bills + self.bill_dates = [ + timezone.make_aware(datetime.datetime(2020,3,31)), + timezone.make_aware(datetime.datetime(2020,4,30)), + timezone.make_aware(datetime.datetime(2020,5,31)), + ] + + + def order_chocolate(self): + return Order.objects.create( + owner=self.user, + recurring_period=RecurringPeriod.ONE_TIME, + product=self.chocolate, + billing_address=BillingAddress.get_address_for(self.user), + starting_date=self.order_meta[1]['starting_date'], + ending_date=self.order_meta[1]['ending_date'], + config=chocolate_order_config) + + + + def test_bill_one_time_one_bill_record(self): + """ + Ensure there is only 1 bill record per order + """ + + order = self.order_chocolate() + + bill = Bill.create_next_bill_for_user_address(self.user_addr) + + self.assertEqual(order.billrecord_set.count(), 1) + + def test_bill_sum_onetime(self): + """ + Check the bill sum for a single one time order + """ + + order = self.order_chocolate() + bill = Bill.create_next_bill_for_user_address(self.user_addr) + self.assertEqual(bill.sum, chocolate_one_time_price) + + +# def test_bill_creates_record_for_recurring_order(self): +# """ +# Ensure there is only 1 bill record per order +# """ + +# bill = Bill.create_next_bill_for_user_address(self.recurring_user_addr) + +# self.assertEqual(self.recurring_order.billrecord_set.count(), 1) +# self.assertEqual(bill.billrecord_set.count(), 1) + + +# def test_new_bill_after_closing(self): +# """ +# After closing a bill and the user has a recurring product, +# the next bill run should create e new bill +# """ + +# for ending_date in self.bill_dates: +# b = Bill.create_next_bill_for_user_address(self.recurring_user_addr, ending_date) +# b.close() + +# bill_count = Bill.objects.filter(owner=self.recurring_user).count() + +# self.assertEqual(len(self.bill_dates), bill_count) + +# def test_multi_addr_multi_bill(self): +# """ +# Ensure multiple bills are created if orders exist with different billing addresses +# """ + +# username="lotsofplaces" +# multi_addr_user = get_user_model().objects.create( +# username=username, +# email=f"{username}@example.org") + +# user_addr1 = BillingAddress.objects.create( +# owner=multi_addr_user, +# organization = 'Test org', +# street="unknown", +# city="unknown", +# postal_code="unknown", +# active=True) + +# order1 = Order.objects.create( +# owner=multi_addr_user, +# starting_date=self.order_meta[1]['starting_date'], +# ending_date=self.order_meta[1]['ending_date'], +# recurring_period=RecurringPeriod.ONE_TIME, +# price=self.order_meta[1]['price'], +# description=self.order_meta[1]['description'], +# billing_address=BillingAddress.get_address_for(self.user)) + +# # Make this address inactive +# user_addr1.active = False +# user_addr1.save() + +# user_addr2 = BillingAddress.objects.create( +# owner=multi_addr_user, +# organization = 'Test2 org', +# street="unknown2", +# city="unknown2", +# postal_code="unknown2", +# active=True) + +# order2 = Order.objects.create( +# owner=multi_addr_user, +# starting_date=self.order_meta[1]['starting_date'], +# ending_date=self.order_meta[1]['ending_date'], +# recurring_period=RecurringPeriod.ONE_TIME, +# price=self.order_meta[1]['price'], +# description=self.order_meta[1]['description'], +# billing_address=BillingAddress.get_address_for(self.user)) + + +# bills = Bill.create_next_bills_for_user(multi_addr_user) + +# 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 ProductTestCase(TestCase): # """ # Test products and products <-> order interaction @@ -292,175 +495,6 @@ class OrderTestCase(TestCase): -# class BillTestCase(TestCase): -# def setUp(self): -# self.user_without_address = get_user_model().objects.create( -# username='no_home_person', -# email='far.away@domain.tld') - -# self.user = get_user_model().objects.create( -# username='jdoe', -# email='john.doe@domain.tld') - -# self.recurring_user = get_user_model().objects.create( -# username='recurrent_product_user', -# email='jane.doe@domain.tld') - -# self.user_addr = BillingAddress.objects.create( -# owner=self.user, -# organization = 'Test org', -# street="unknown", -# city="unknown", -# postal_code="unknown", -# active=True) - -# self.recurring_user_addr = BillingAddress.objects.create( -# owner=self.recurring_user, -# organization = 'Test org', -# street="Somewhere", -# city="Else", -# postal_code="unknown", -# active=True) - -# self.order_meta = {} -# self.order_meta[1] = { -# 'starting_date': timezone.make_aware(datetime.datetime(2020,3,3)), -# 'ending_date': timezone.make_aware(datetime.datetime(2020,4,17)), -# 'price': 15, -# 'description': 'One chocolate bar' -# } - -# self.one_time_order = Order.objects.create( -# owner=self.user, -# starting_date=self.order_meta[1]['starting_date'], -# ending_date=self.order_meta[1]['ending_date'], -# recurring_period=RecurringPeriod.ONE_TIME, -# price=self.order_meta[1]['price'], -# description=self.order_meta[1]['description'], -# billing_address=BillingAddress.get_address_for(self.user)) - -# self.recurring_order = Order.objects.create( -# owner=self.recurring_user, -# starting_date=timezone.make_aware(datetime.datetime(2020,3,3)), -# recurring_period=RecurringPeriod.PER_30D, -# price=15, -# description="A pretty VM", -# billing_address=BillingAddress.get_address_for(self.recurring_user) -# ) - -# # used for generating multiple bills -# self.bill_dates = [ -# timezone.make_aware(datetime.datetime(2020,3,31)), -# timezone.make_aware(datetime.datetime(2020,4,30)), -# timezone.make_aware(datetime.datetime(2020,5,31)), -# ] - - - -# def test_bill_one_time_one_bill_record(self): -# """ -# Ensure there is only 1 bill record per order -# """ - -# bill = Bill.create_next_bill_for_user_address(self.user_addr) - -# self.assertEqual(self.one_time_order.billrecord_set.count(), 1) - -# def test_bill_sum_onetime(self): -# """ -# Check the bill sum for a single one time order -# """ - -# bill = Bill.create_next_bill_for_user_address(self.user_addr) -# self.assertEqual(bill.sum, self.order_meta[1]['price']) - - -# def test_bill_creates_record_for_recurring_order(self): -# """ -# Ensure there is only 1 bill record per order -# """ - -# bill = Bill.create_next_bill_for_user_address(self.recurring_user_addr) - -# self.assertEqual(self.recurring_order.billrecord_set.count(), 1) -# self.assertEqual(bill.billrecord_set.count(), 1) - - -# def test_new_bill_after_closing(self): -# """ -# After closing a bill and the user has a recurring product, -# the next bill run should create e new bill -# """ - -# for ending_date in self.bill_dates: -# b = Bill.create_next_bill_for_user_address(self.recurring_user_addr, ending_date) -# b.close() - -# bill_count = Bill.objects.filter(owner=self.recurring_user).count() - -# self.assertEqual(len(self.bill_dates), bill_count) - -# def test_multi_addr_multi_bill(self): -# """ -# Ensure multiple bills are created if orders exist with different billing addresses -# """ - -# username="lotsofplaces" -# multi_addr_user = get_user_model().objects.create( -# username=username, -# email=f"{username}@example.org") - -# user_addr1 = BillingAddress.objects.create( -# owner=multi_addr_user, -# organization = 'Test org', -# street="unknown", -# city="unknown", -# postal_code="unknown", -# active=True) - -# order1 = Order.objects.create( -# owner=multi_addr_user, -# starting_date=self.order_meta[1]['starting_date'], -# ending_date=self.order_meta[1]['ending_date'], -# recurring_period=RecurringPeriod.ONE_TIME, -# price=self.order_meta[1]['price'], -# description=self.order_meta[1]['description'], -# billing_address=BillingAddress.get_address_for(self.user)) - -# # Make this address inactive -# user_addr1.active = False -# user_addr1.save() - -# user_addr2 = BillingAddress.objects.create( -# owner=multi_addr_user, -# organization = 'Test2 org', -# street="unknown2", -# city="unknown2", -# postal_code="unknown2", -# active=True) - -# order2 = Order.objects.create( -# owner=multi_addr_user, -# starting_date=self.order_meta[1]['starting_date'], -# ending_date=self.order_meta[1]['ending_date'], -# recurring_period=RecurringPeriod.ONE_TIME, -# price=self.order_meta[1]['price'], -# description=self.order_meta[1]['description'], -# billing_address=BillingAddress.get_address_for(self.user)) - - -# bills = Bill.create_next_bills_for_user(multi_addr_user) - -# 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):