diff --git a/uncloud_pay/admin.py b/uncloud_pay/admin.py index 421b9b7..464ff1b 100644 --- a/uncloud_pay/admin.py +++ b/uncloud_pay/admin.py @@ -14,7 +14,8 @@ from uncloud_pay.models import Bill, Order, BillRecord, BillingAddress class BillRecordInline(admin.TabularInline): - model = Bill.bill_records.through +# model = Bill.bill_records.through + model = BillRecord # AT some point in the future: expose REPLACED and orders that depend on us # class OrderInline(admin.TabularInline): @@ -29,6 +30,10 @@ class BillAdmin(admin.ModelAdmin): # change_list_template = "uncloud_pay/change_list.html" def get_urls(self): + """ + Create URLs for PDF view + """ + info = "%s_%s" % (self.model._meta.app_label, self.model._meta.model_name) pat = lambda regex, fn: url(regex, self.admin_site.admin_view(fn), name='%s_%s' % (info, fn.__name__)) @@ -77,7 +82,9 @@ class BillAdmin(admin.ModelAdmin): admin.site.register(Bill, BillAdmin) -#admin.site.register(Order, OrderAdmin) admin.site.register(Order) admin.site.register(BillRecord) admin.site.register(BillingAddress) + + +#admin.site.register(Order, OrderAdmin) diff --git a/uncloud_pay/migrations/0006_remove_billrecord_quantity.py b/uncloud_pay/migrations/0006_remove_billrecord_quantity.py new file mode 100644 index 0000000..e8b50da --- /dev/null +++ b/uncloud_pay/migrations/0006_remove_billrecord_quantity.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1 on 2020-08-08 19:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0005_auto_20200808_1954'), + ] + + operations = [ + migrations.RemoveField( + model_name='billrecord', + name='quantity', + ), + ] diff --git a/uncloud_pay/migrations/0007_remove_bill_bill_records.py b/uncloud_pay/migrations/0007_remove_bill_bill_records.py new file mode 100644 index 0000000..6ba9563 --- /dev/null +++ b/uncloud_pay/migrations/0007_remove_bill_bill_records.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1 on 2020-08-08 20:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0006_remove_billrecord_quantity'), + ] + + operations = [ + migrations.RemoveField( + model_name='bill', + name='bill_records', + ), + ] diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 12ded8d..0c3dcf6 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -340,6 +340,10 @@ class Order(models.Model): def is_recurring(self): return not self.recurring_period == RecurringPeriod.ONE_TIME + @property + def is_one_time(self): + return not self.is_recurring + @property def is_terminated(self): return self.ending_date != None and self.ending_date < timezone.now() @@ -382,10 +386,6 @@ class Bill(models.Model): # what is valid for? should this be "final"? valid = models.BooleanField(default=True) - # Mapping to BillRecords - # https://stackoverflow.com/questions/4443190/djangos-manytomany-relationship-with-additional-fields - bill_records = models.ManyToManyField(Order, through="BillRecord") - class Meta: constraints = [ models.UniqueConstraint(fields=['owner', @@ -399,13 +399,14 @@ class Bill(models.Model): @property def sum(self): - pass + return 0 +# for self.billrecord_set. @classmethod - def create_next_bill_for_user(cls, user): - last_bill = cls.objects.filter(owner=user).order_by('id').last() - all_orders = Order.objects.filter(owner=user).order_by('id') + def create_next_bill_for_user(cls, owner): + last_bill = cls.objects.filter(owner=owner).order_by('id').last() + all_orders = Order.objects.filter(owner=owner).order_by('id') first_order = all_orders.first() @@ -420,16 +421,21 @@ class Bill(models.Model): ending_date = end_of_month(starting_date) - bill = cls() + bill, created = cls.objects.get_or_create( + owner=owner, + starting_date=starting_date, + ending_date=ending_date) for order in all_orders: # check if order needs to be billed # check if order has previous billing record - # one time orders - if not order.is_recurring: - pass - + if order.is_one_time: + if order.billrecord_set.count() == 0: + br = BillRecord.objects.create(bill=bill, + order=order, + starting_date=starting_date, + ending_date=ending_date) pass return bill @@ -515,20 +521,13 @@ class BillRecord(models.Model): bill = models.ForeignKey(Bill, on_delete=models.CASCADE) order = models.ForeignKey(Order, on_delete=models.CASCADE) - # How many times the order has been used in this record - quantity = models.DecimalField(max_digits=19, decimal_places=10) - - # quantity can actually be derived from starting/ending date - - # The timeframe the bill record is for can (and probably often will) differ - # from the bill time - creation_date = models.DateTimeField(auto_now_add=True) starting_date = models.DateTimeField() ending_date = models.DateTimeField() - def quantity2(self): - if self.order.recurring_period == RecurringPeriod.ONE_TIME: + def quantity(self): + """ Determine the quantity by the duration""" + if self.order.is_recurring: return 1 record_delta = self.ending_date - self.starting_date @@ -537,10 +536,7 @@ class BillRecord(models.Model): def sum(self): - if self.order.recurring_period == RecurringPeriod.ONE_TIME: - return 1 - - return self.quantity * 1 + return self.order.price * self.quantity def __str__(self): return f"{self.bill}: {self.quantity} x {self.order}" diff --git a/uncloud_pay/tests.py b/uncloud_pay/tests.py index 7754d5b..d2cdc34 100644 --- a/uncloud_pay/tests.py +++ b/uncloud_pay/tests.py @@ -5,236 +5,284 @@ from datetime import datetime, date, timedelta from .models import * from uncloud_service.models import GenericServiceProduct -class NotABillingTC(TestCase): -#class BillingTestCase(TestCase): - def setUp(self): - self.user = get_user_model().objects.create( - username='jdoe', - email='john.doe@domain.tld') - self.billing_address = BillingAddress.objects.create( - owner=self.user, - street="unknown", - city="unknown", - postal_code="unknown") - - def test_basic_monthly_billing(self): - one_time_price = 10 - recurring_price = 20 - description = "Test Product 1" - - # Three months: full, full, partial. -# starting_date = datetime.fromisoformat('2020-03-01') - starting_date = datetime(2020,3,1) - ending_date = datetime(2020,5,8) - - # Create order to be billed. - order = Order.objects.create( - owner=self.user, - starting_date=starting_date, - ending_date=ending_date, - recurring_period=RecurringPeriod.PER_30D, - recurring_price=recurring_price, - one_time_price=one_time_price, - description=description, - billing_address=self.billing_address) - - # Generate & check bill for first month: full recurring_price + setup. - first_month_bills = order.generate_initial_bill() - self.assertEqual(len(first_month_bills), 1) - self.assertEqual(first_month_bills[0].amount, one_time_price + recurring_price) - - # Generate & check bill for second month: full recurring_price. - second_month_bills = Bill.generate_for(2020, 4, self.user) - self.assertEqual(len(second_month_bills), 1) - self.assertEqual(second_month_bills[0].amount, recurring_price) - - # Generate & check bill for third and last month: partial recurring_price. - third_month_bills = Bill.generate_for(2020, 5, self.user) - self.assertEqual(len(third_month_bills), 1) - # 31 days in May. - self.assertEqual(float(third_month_bills[0].amount), - round(round((7/31), AMOUNT_DECIMALS) * recurring_price, AMOUNT_DECIMALS)) - - # Check that running Bill.generate_for() twice does not create duplicates. - self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0) - - def test_basic_yearly_billing(self): - one_time_price = 10 - recurring_price = 150 - description = "Test Product 1" - - starting_date = datetime.fromisoformat('2020-03-31T08:05:23') - - # Create order to be billed. - order = Order.objects.create( - owner=self.user, - starting_date=starting_date, - recurring_period=RecurringPeriod.PER_365D, - recurring_price=recurring_price, - one_time_price=one_time_price, - description=description, - billing_address=self.billing_address) - - # Generate & check bill for first year: recurring_price + setup. - first_year_bills = order.generate_initial_bill() - self.assertEqual(len(first_year_bills), 1) - self.assertEqual(first_year_bills[0].starting_date.date(), - date.fromisoformat('2020-03-31')) - self.assertEqual(first_year_bills[0].ending_date.date(), - date.fromisoformat('2021-03-30')) - self.assertEqual(first_year_bills[0].amount, - recurring_price + one_time_price) - - # Generate & check bill for second year: recurring_price. - second_year_bills = Bill.generate_for(2021, 3, self.user) - self.assertEqual(len(second_year_bills), 1) - self.assertEqual(second_year_bills[0].starting_date.date(), - date.fromisoformat('2021-03-31')) - self.assertEqual(second_year_bills[0].ending_date.date(), - date.fromisoformat('2022-03-30')) - self.assertEqual(second_year_bills[0].amount, recurring_price) - - # Check that running Bill.generate_for() twice does not create duplicates. - self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0) - self.assertEqual(len(Bill.generate_for(2020, 4, self.user)), 0) - self.assertEqual(len(Bill.generate_for(2020, 2, self.user)), 0) - self.assertEqual(len(Bill.generate_for(2021, 3, self.user)), 0) - - def test_basic_hourly_billing(self): - one_time_price = 10 - recurring_price = 1.4 - description = "Test Product 1" - - starting_date = datetime.fromisoformat('2020-03-31T08:05:23') - ending_date = datetime.fromisoformat('2020-04-01T11:13:32') - - # Create order to be billed. - order = Order.objects.create( - owner=self.user, - starting_date=starting_date, - ending_date=ending_date, - recurring_period=RecurringPeriod.PER_HOUR, - recurring_price=recurring_price, - one_time_price=one_time_price, - description=description, - billing_address=self.billing_address) - - # Generate & check bill for first month: recurring_price + setup. - first_month_bills = order.generate_initial_bill() - self.assertEqual(len(first_month_bills), 1) - self.assertEqual(float(first_month_bills[0].amount), - round(16 * recurring_price, AMOUNT_DECIMALS) + one_time_price) - - # Generate & check bill for first month: recurring_price. - second_month_bills = Bill.generate_for(2020, 4, self.user) - self.assertEqual(len(second_month_bills), 1) - self.assertEqual(float(second_month_bills[0].amount), - round(12 * recurring_price, AMOUNT_DECIMALS)) class ProductActivationTestCase(TestCase): def setUp(self): self.user = get_user_model().objects.create( - username='jdoe', - email='john.doe@domain.tld') + username='jdoe', + email='john.doe@domain.tld') self.billing_address = BillingAddress.objects.create( - owner=self.user, - street="unknown", - city="unknown", - postal_code="unknown") + owner=self.user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="unknown") + + 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' + } + + + def test_bill_one_time_order(self): + one_time_price = 10 + recurring_price = 150 + description = "Test Product 1" - def test_product_activation(self): - starting_date = datetime.fromisoformat('2020-03-01') - one_time_price = 0 - recurring_price = 1 - description = "Test Product" order = Order.objects.create( owner=self.user, - starting_date=starting_date, - recurring_period=RecurringPeriod.PER_30D, - recurring_price=recurring_price, - one_time_price=one_time_price, - description=description, + 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=self.billing_address) - product = GenericServiceProduct( - custom_description=description, - custom_one_time_price=one_time_price, - custom_recurring_price=recurring_price, - owner=self.user, - order=order) - product.save() - # Validate initial state: must be awaiting payment. - self.assertEqual(product.status, UncloudStatus.AWAITING_PAYMENT) + bill = Bill.create_next_bill_for_user(self.user) - # Pay initial bill, check that product is activated. - order.generate_initial_bill() - amount = product.order.bills[0].amount - payment = Payment(owner=self.user, amount=amount) - payment.save() - self.assertEqual( - GenericServiceProduct.objects.get(uuid=product.uuid).status, - UncloudStatus.PENDING - ) + self.assertEqual(order.billrecord_set.count(), 1) -class BillingAddressTestCase(TestCase): - def setUp(self): - self.user = get_user_model().objects.create( - username='jdoe', - email='john.doe@domain.tld') + self.assertEqual(bill.sum, self.order_meta[1]['price']) - self.billing_address_01 = BillingAddress.objects.create( - owner=self.user, - street="unknown1", - city="unknown1", - postal_code="unknown1", - country="CH") - self.billing_address_02 = BillingAddress.objects.create( - owner=self.user, - street="unknown2", - city="unknown2", - postal_code="unknown2", - country="CH") - def test_billing_with_single_address(self): - # Create new orders somewhere in the past so that we do not encounter - # auto-created initial bills. - starting_date = datetime.fromisoformat('2020-03-01') - order_01 = Order.objects.create( - owner=self.user, - starting_date=starting_date, - recurring_period=RecurringPeriod.PER_30D, - billing_address=self.billing_address_01) - order_02 = Order.objects.create( - owner=self.user, - starting_date=starting_date, - recurring_period=RecurringPeriod.PER_30D, - billing_address=self.billing_address_01) +# class NotABillingTC(TestCase): +# #class BillingTestCase(TestCase): +# def setUp(self): +# self.user = get_user_model().objects.create( +# username='jdoe', +# email='john.doe@domain.tld') +# self.billing_address = BillingAddress.objects.create( +# owner=self.user, +# street="unknown", +# city="unknown", +# postal_code="unknown") - # We need a single bill since we work with a single address. - bills = Bill.generate_for(2020, 4, self.user) - self.assertEqual(len(bills), 1) +# def test_basic_monthly_billing(self): +# one_time_price = 10 +# recurring_price = 20 +# description = "Test Product 1" - def test_billing_with_multiple_addresses(self): - # Create new orders somewhere in the past so that we do not encounter - # auto-created initial bills. - starting_date = datetime.fromisoformat('2020-03-01') +# # Three months: full, full, partial. +# # starting_date = datetime.fromisoformat('2020-03-01') +# starting_date = datetime(2020,3,1) +# ending_date = datetime(2020,5,8) - order_01 = Order.objects.create( - owner=self.user, - starting_date=starting_date, - recurring_period=RecurringPeriod.PER_30D, - billing_address=self.billing_address_01) - order_02 = Order.objects.create( - owner=self.user, - starting_date=starting_date, - recurring_period=RecurringPeriod.PER_30D, - billing_address=self.billing_address_02) +# # Create order to be billed. +# order = Order.objects.create( +# owner=self.user, +# starting_date=starting_date, +# ending_date=ending_date, +# recurring_period=RecurringPeriod.PER_30D, +# recurring_price=recurring_price, +# one_time_price=one_time_price, +# description=description, +# billing_address=self.billing_address) - # We need different bills since we work with different addresses. - bills = Bill.generate_for(2020, 4, self.user) - self.assertEqual(len(bills), 2) +# # Generate & check bill for first month: full recurring_price + setup. +# first_month_bills = order.generate_initial_bill() +# self.assertEqual(len(first_month_bills), 1) +# self.assertEqual(first_month_bills[0].amount, one_time_price + recurring_price) + +# # Generate & check bill for second month: full recurring_price. +# second_month_bills = Bill.generate_for(2020, 4, self.user) +# self.assertEqual(len(second_month_bills), 1) +# self.assertEqual(second_month_bills[0].amount, recurring_price) + +# # Generate & check bill for third and last month: partial recurring_price. +# third_month_bills = Bill.generate_for(2020, 5, self.user) +# self.assertEqual(len(third_month_bills), 1) +# # 31 days in May. +# self.assertEqual(float(third_month_bills[0].amount), +# round(round((7/31), AMOUNT_DECIMALS) * recurring_price, AMOUNT_DECIMALS)) + +# # Check that running Bill.generate_for() twice does not create duplicates. +# self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0) + +# def test_basic_yearly_billing(self): +# one_time_price = 10 +# recurring_price = 150 +# description = "Test Product 1" + +# starting_date = datetime.fromisoformat('2020-03-31T08:05:23') + +# # Create order to be billed. +# order = Order.objects.create( +# owner=self.user, +# starting_date=starting_date, +# recurring_period=RecurringPeriod.PER_365D, +# recurring_price=recurring_price, +# one_time_price=one_time_price, +# description=description, +# billing_address=self.billing_address) + +# # Generate & check bill for first year: recurring_price + setup. +# first_year_bills = order.generate_initial_bill() +# self.assertEqual(len(first_year_bills), 1) +# self.assertEqual(first_year_bills[0].starting_date.date(), +# date.fromisoformat('2020-03-31')) +# self.assertEqual(first_year_bills[0].ending_date.date(), +# date.fromisoformat('2021-03-30')) +# self.assertEqual(first_year_bills[0].amount, +# recurring_price + one_time_price) + +# # Generate & check bill for second year: recurring_price. +# second_year_bills = Bill.generate_for(2021, 3, self.user) +# self.assertEqual(len(second_year_bills), 1) +# self.assertEqual(second_year_bills[0].starting_date.date(), +# date.fromisoformat('2021-03-31')) +# self.assertEqual(second_year_bills[0].ending_date.date(), +# date.fromisoformat('2022-03-30')) +# self.assertEqual(second_year_bills[0].amount, recurring_price) + +# # Check that running Bill.generate_for() twice does not create duplicates. +# self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0) +# self.assertEqual(len(Bill.generate_for(2020, 4, self.user)), 0) +# self.assertEqual(len(Bill.generate_for(2020, 2, self.user)), 0) +# self.assertEqual(len(Bill.generate_for(2021, 3, self.user)), 0) + +# def test_basic_hourly_billing(self): +# one_time_price = 10 +# recurring_price = 1.4 +# description = "Test Product 1" + +# starting_date = datetime.fromisoformat('2020-03-31T08:05:23') +# ending_date = datetime.fromisoformat('2020-04-01T11:13:32') + +# # Create order to be billed. +# order = Order.objects.create( +# owner=self.user, +# starting_date=starting_date, +# ending_date=ending_date, +# recurring_period=RecurringPeriod.PER_HOUR, +# recurring_price=recurring_price, +# one_time_price=one_time_price, +# description=description, +# billing_address=self.billing_address) + +# # Generate & check bill for first month: recurring_price + setup. +# first_month_bills = order.generate_initial_bill() +# self.assertEqual(len(first_month_bills), 1) +# self.assertEqual(float(first_month_bills[0].amount), +# round(16 * recurring_price, AMOUNT_DECIMALS) + one_time_price) + +# # Generate & check bill for first month: recurring_price. +# second_month_bills = Bill.generate_for(2020, 4, self.user) +# self.assertEqual(len(second_month_bills), 1) +# self.assertEqual(float(second_month_bills[0].amount), +# round(12 * recurring_price, AMOUNT_DECIMALS)) + +# class ProductActivationTestCase(TestCase): +# def setUp(self): +# self.user = get_user_model().objects.create( +# username='jdoe', +# email='john.doe@domain.tld') + +# self.billing_address = BillingAddress.objects.create( +# owner=self.user, +# street="unknown", +# city="unknown", +# postal_code="unknown") + +# def test_product_activation(self): +# starting_date = datetime.fromisoformat('2020-03-01') +# one_time_price = 0 +# recurring_price = 1 +# description = "Test Product" + +# order = Order.objects.create( +# owner=self.user, +# starting_date=starting_date, +# recurring_period=RecurringPeriod.PER_30D, +# recurring_price=recurring_price, +# one_time_price=one_time_price, +# description=description, +# billing_address=self.billing_address) + +# product = GenericServiceProduct( +# custom_description=description, +# custom_one_time_price=one_time_price, +# custom_recurring_price=recurring_price, +# owner=self.user, +# order=order) +# product.save() + +# # Validate initial state: must be awaiting payment. +# self.assertEqual(product.status, UncloudStatus.AWAITING_PAYMENT) + +# # Pay initial bill, check that product is activated. +# order.generate_initial_bill() +# amount = product.order.bills[0].amount +# payment = Payment(owner=self.user, amount=amount) +# payment.save() +# self.assertEqual( +# GenericServiceProduct.objects.get(uuid=product.uuid).status, +# UncloudStatus.PENDING +# ) + +# class BillingAddressTestCase(TestCase): +# def setUp(self): +# self.user = get_user_model().objects.create( +# username='jdoe', +# email='john.doe@domain.tld') + +# self.billing_address_01 = BillingAddress.objects.create( +# owner=self.user, +# street="unknown1", +# city="unknown1", +# postal_code="unknown1", +# country="CH") + +# self.billing_address_02 = BillingAddress.objects.create( +# owner=self.user, +# street="unknown2", +# city="unknown2", +# postal_code="unknown2", +# country="CH") + +# def test_billing_with_single_address(self): +# # Create new orders somewhere in the past so that we do not encounter +# # auto-created initial bills. +# starting_date = datetime.fromisoformat('2020-03-01') + +# order_01 = Order.objects.create( +# owner=self.user, +# starting_date=starting_date, +# recurring_period=RecurringPeriod.PER_30D, +# billing_address=self.billing_address_01) +# order_02 = Order.objects.create( +# owner=self.user, +# starting_date=starting_date, +# recurring_period=RecurringPeriod.PER_30D, +# billing_address=self.billing_address_01) + +# # We need a single bill since we work with a single address. +# bills = Bill.generate_for(2020, 4, self.user) +# self.assertEqual(len(bills), 1) + +# def test_billing_with_multiple_addresses(self): +# # Create new orders somewhere in the past so that we do not encounter +# # auto-created initial bills. +# starting_date = datetime.fromisoformat('2020-03-01') + +# order_01 = Order.objects.create( +# owner=self.user, +# starting_date=starting_date, +# recurring_period=RecurringPeriod.PER_30D, +# billing_address=self.billing_address_01) +# order_02 = Order.objects.create( +# owner=self.user, +# starting_date=starting_date, +# recurring_period=RecurringPeriod.PER_30D, +# billing_address=self.billing_address_02) + +# # We need different bills since we work with different addresses. +# bills = Bill.generate_for(2020, 4, self.user) +# self.assertEqual(len(bills), 2)