diff --git a/uncloud_pay/admin.py b/uncloud_pay/admin.py index 45714d6..14fc00f 100644 --- a/uncloud_pay/admin.py +++ b/uncloud_pay/admin.py @@ -11,7 +11,7 @@ from django.http import FileResponse from django.template.loader import render_to_string -from uncloud_pay.models import Bill, Order, BillRecord, BillingAddress, SampleOneTimeProduct, SampleRecurringProduct, SampleRecurringProductOneTimeFee +from uncloud_pay.models import Bill, Order, BillRecord, BillingAddress, Product class BillRecordInline(admin.TabularInline): @@ -91,7 +91,8 @@ admin.site.register(Order) admin.site.register(BillRecord) admin.site.register(BillingAddress) -for m in [ SampleOneTimeProduct, SampleRecurringProduct, SampleRecurringProductOneTimeFee ]: - admin.site.register(m) +#for m in [ SampleOneTimeProduct, SampleRecurringProduct, SampleRecurringProductOneTimeFee ]: + +admin.site.register(Product) #admin.site.register(Order, OrderAdmin) diff --git a/uncloud_pay/migrations/0017_order_config.py b/uncloud_pay/migrations/0017_order_config.py new file mode 100644 index 0000000..3afecee --- /dev/null +++ b/uncloud_pay/migrations/0017_order_config.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1 on 2020-09-28 19:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0016_auto_20200928_1858'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='config', + field=models.JSONField(default={}), + preserve_default=False, + ), + ] diff --git a/uncloud_pay/migrations/0018_order_product.py b/uncloud_pay/migrations/0018_order_product.py new file mode 100644 index 0000000..e4e6eb1 --- /dev/null +++ b/uncloud_pay/migrations/0018_order_product.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1 on 2020-09-28 19:08 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0017_order_config'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='product', + field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.product'), + preserve_default=False, + ), + ] diff --git a/uncloud_pay/migrations/0019_remove_product_owner.py b/uncloud_pay/migrations/0019_remove_product_owner.py new file mode 100644 index 0000000..05ea2a8 --- /dev/null +++ b/uncloud_pay/migrations/0019_remove_product_owner.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1 on 2020-09-28 19:14 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0018_order_product'), + ] + + operations = [ + migrations.RemoveField( + model_name='product', + name='owner', + ), + ] diff --git a/uncloud_pay/migrations/0020_auto_20200928_1915.py b/uncloud_pay/migrations/0020_auto_20200928_1915.py new file mode 100644 index 0000000..2190397 --- /dev/null +++ b/uncloud_pay/migrations/0020_auto_20200928_1915.py @@ -0,0 +1,29 @@ +# Generated by Django 3.1 on 2020-09-28 19:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0019_remove_product_owner'), + ] + + operations = [ + migrations.RemoveField( + model_name='product', + name='status', + ), + migrations.AddField( + model_name='product', + name='description', + field=models.CharField(default='', max_length=1024), + preserve_default=False, + ), + migrations.AddField( + model_name='product', + name='name', + field=models.CharField(default='', max_length=256), + preserve_default=False, + ), + ] diff --git a/uncloud_pay/migrations/0021_auto_20200928_1932.py b/uncloud_pay/migrations/0021_auto_20200928_1932.py new file mode 100644 index 0000000..85b8afe --- /dev/null +++ b/uncloud_pay/migrations/0021_auto_20200928_1932.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1 on 2020-09-28 19:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0020_auto_20200928_1915'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='default_currency', + field=models.CharField(choices=[('CHF', 'Swiss Franc'), ('EUR', 'Euro'), ('USD', 'US Dollar')], default='CHF', max_length=32), + ), + migrations.AddField( + model_name='product', + name='default_recurring_period', + field=models.IntegerField(choices=[(31536000, 'Per 365 days'), (2592000, 'Per 30 days'), (604800, 'Per Week'), (86400, 'Per Day'), (3600, 'Per Hour'), (60, 'Per Minute'), (1, 'Per Second'), (0, 'Onetime')], default=2592000), + ), + ] diff --git a/uncloud_pay/migrations/0022_auto_20200928_1932.py b/uncloud_pay/migrations/0022_auto_20200928_1932.py new file mode 100644 index 0000000..0969b79 --- /dev/null +++ b/uncloud_pay/migrations/0022_auto_20200928_1932.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-09-28 19:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0021_auto_20200928_1932'), + ] + + operations = [ + migrations.RenameField( + model_name='product', + old_name='default_currency', + new_name='currency', + ), + ] diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index b6f8f39..adc044f 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -80,6 +80,16 @@ class RecurringPeriod(models.IntegerChoices): PER_SECOND = 1, _('Per Second') ONE_TIME = 0, _('Onetime') +class Currency(models.TextChoices): + """ + We don't support months are years, because they vary in length. + This is not only complicated, but also unfair to the user, as the user pays the same + amount for different durations. + """ + CHF = 'CHF', _('Swiss Franc') + EUR = 'EUR', _('Euro') + USD = 'USD', _('US Dollar') + class CountryField(models.CharField): def __init__(self, *args, **kwargs): kwargs.setdefault('choices', COUNTRIES) @@ -283,6 +293,235 @@ class VATRate(models.Model): logger.debug("Did not find VAT rate for %s, returning 0" % country_code) return 0 +### +# Products + +class Product(UncloudModel): + """ + A product is something a user can order. To record the pricing, we + create order that define a state in time. + + 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. + + """ + + name = models.CharField(max_length=256) + description = models.CharField(max_length=1024) + + config = models.JSONField() + default_recurring_period = models.IntegerField(choices=RecurringPeriod.choices, default=RecurringPeriod.PER_30D) + currency = models.CharField(max_length=32, choices=Currency.choices, default=Currency.CHF) + + @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: + raise ValidationError("Cannot order without a billing address") + + if not when_to_start: + when_to_start = timezone.now() + + if not recurring_period: + recurring_period = self.default_recurring_period + + + # 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, + price=self.one_time_price, + recurring_period=RecurringPeriod.ONE_TIME, + description=str(self)) + self.orders.add(one_time_order) + else: + one_time_order = None + + if recurring_period != RecurringPeriod.ONE_TIME: + if one_time_order: + recurring_order = Order.objects.create(owner=self.owner, + billing_address=billing_address, + starting_date=when_to_start, + price=self.recurring_price, + recurring_period=recurring_period, + depends_on=one_time_order, + description=str(self)) + else: + recurring_order = Order.objects.create(owner=self.owner, + billing_address=billing_address, + starting_date=when_to_start, + price=self.recurring_price, + recurring_period=recurring_period, + description=str(self)) + self.orders.add(recurring_order) + + + # FIXME: this could/should be part of Order (?) + 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() + + if self.last_recurring_order: + if self.recurring_price < self.last_recurring_order.price: + + if when_to_start < self.last_recurring_order.next_cancel_or_downgrade_date: + when_to_start = start_after(self.last_recurring_order.next_cancel_or_downgrade_date) + + when_to_end = end_before(when_to_start) + + new_order = Order.objects.create(owner=self.owner, + 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.last_recurring_order) + + self.last_recurring_order.replace_with(new_order) + self.orders.add(new_order) + else: + self.create_order(when_to_start, recurring_period) + + + + @property + def recurring_price(self): + """ implement correct values in the child class """ + return 0 + + @property + def one_time_price(self): + """ implement correct values in the child class """ + return 0 + + + @property + def is_recurring(self): + return self.recurring_price > 0 + + @property + def billing_address(self): + return self.order.billing_address + + @staticmethod + def allowed_recurring_periods(): + return RecurringPeriod.choices + + # class Meta: + # abstract = True + + def discounted_price_by_period(self, requested_period): + """ + Each product has a standard recurring period for which + we define a pricing. I.e. VPN is usually year, VM is usually monthly. + + The user can opt-in to use a different period, which influences the price: + The longer a user commits, the higher the discount. + + Products can also be limited in the available periods. For instance + a VPN only makes sense to be bought for at least one day. + + Rules are as follows: + + given a standard recurring period of ..., changing to ... modifies price ... + + + # One month for free if buying / year, compared to a month: about 8.33% discount + per_year -> per_month -> /11 + per_month -> per_year -> *11 + + # Month has 30.42 days on average. About 7.9% discount to go monthly + per_month -> per_day -> /28 + per_day -> per_month -> *28 + + # Day has 24h, give one for free + per_day -> per_hour -> /23 + per_hour -> per_day -> /23 + + + Examples + + VPN @ 120CHF/y becomes + - 10.91 CHF/month (130.91 CHF/year) + - 0.39 CHF/day (142.21 CHF/year) + + VM @ 15 CHF/month becomes + - 165 CHF/month (13.75 CHF/month) + - 0.54 CHF/day (16.30 CHF/month) + + """ + + + if self.default_recurring_period == RecurringPeriod.PER_365D: + if requested_period == RecurringPeriod.PER_365D: + return self.recurring_price + if requested_period == RecurringPeriod.PER_30D: + return self.recurring_price/11. + if requested_period == RecurringPeriod.PER_DAY: + return self.recurring_price/11./28. + + elif self.default_recurring_period == RecurringPeriod.PER_30D: + if requested_period == RecurringPeriod.PER_365D: + return self.recurring_price*11 + if requested_period == RecurringPeriod.PER_30D: + return self.recurring_price + if requested_period == RecurringPeriod.PER_DAY: + return self.recurring_price/28. + + elif self.default_recurring_period == RecurringPeriod.PER_DAY: + if requested_period == RecurringPeriod.PER_365D: + return self.recurring_price*11*28 + if requested_period == RecurringPeriod.PER_30D: + return self.recurring_price*28 + if requested_period == RecurringPeriod.PER_DAY: + return self.recurring_price + else: + # FIXME: use the right type of exception here! + raise Exception("Did not implement the discounter for this case") + + + def save(self, *args, **kwargs): + # try: + # ba = BillingAddress.get_address_for(self.owner) + # except BillingAddress.DoesNotExist: + # raise ValidationError("User does not have a billing address") + + # if not ba.active: + # raise ValidationError("User does not have an active billing address") + + + # Verify the required JSON fields + + super().save(*args, **kwargs) + + + ### # Orders. @@ -317,7 +556,9 @@ class Order(models.Model): description = models.TextField() - # TODO: enforce ending_date - starting_date to be larger than recurring_period. + product = models.ForeignKey(Product, blank=False, null=False, on_delete=models.CASCADE) + config = models.JSONField() + creation_date = models.DateTimeField(auto_now_add=True) starting_date = models.DateTimeField(default=timezone.now) ending_date = models.DateTimeField(blank=True, null=True) @@ -850,237 +1091,6 @@ class BillRecord(models.Model): super().save(*args, **kwargs) -### -# Products - -class Product(UncloudModel): - """ - A product is something a user can order. To record the pricing, we - create order that define a state in time. - - 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) - - description = "Generic Product" - - status = models.CharField(max_length=32, - choices=UncloudStatus.choices, - default=UncloudStatus.AWAITING_PAYMENT) - - config = models.JSONField() - - # Default period for all products - default_recurring_period = RecurringPeriod.PER_30D - - @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: - raise ValidationError("Cannot order without a billing address") - - if not when_to_start: - when_to_start = timezone.now() - - if not recurring_period: - recurring_period = self.default_recurring_period - - - # 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, - price=self.one_time_price, - recurring_period=RecurringPeriod.ONE_TIME, - description=str(self)) - self.orders.add(one_time_order) - else: - one_time_order = None - - if recurring_period != RecurringPeriod.ONE_TIME: - if one_time_order: - recurring_order = Order.objects.create(owner=self.owner, - billing_address=billing_address, - starting_date=when_to_start, - price=self.recurring_price, - recurring_period=recurring_period, - depends_on=one_time_order, - description=str(self)) - else: - recurring_order = Order.objects.create(owner=self.owner, - billing_address=billing_address, - starting_date=when_to_start, - price=self.recurring_price, - recurring_period=recurring_period, - description=str(self)) - self.orders.add(recurring_order) - - - # FIXME: this could/should be part of Order (?) - 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() - - if self.last_recurring_order: - if self.recurring_price < self.last_recurring_order.price: - - if when_to_start < self.last_recurring_order.next_cancel_or_downgrade_date: - when_to_start = start_after(self.last_recurring_order.next_cancel_or_downgrade_date) - - when_to_end = end_before(when_to_start) - - new_order = Order.objects.create(owner=self.owner, - 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.last_recurring_order) - - self.last_recurring_order.replace_with(new_order) - self.orders.add(new_order) - else: - self.create_order(when_to_start, recurring_period) - - @property - def recurring_price(self): - """ implement correct values in the child class """ - return 0 - - @property - def one_time_price(self): - """ implement correct values in the child class """ - return 0 - - - @property - def is_recurring(self): - return self.recurring_price > 0 - - @property - def billing_address(self): - return self.order.billing_address - - @staticmethod - def allowed_recurring_periods(): - return RecurringPeriod.choices - - # class Meta: - # abstract = True - - def discounted_price_by_period(self, requested_period): - """ - Each product has a standard recurring period for which - we define a pricing. I.e. VPN is usually year, VM is usually monthly. - - The user can opt-in to use a different period, which influences the price: - The longer a user commits, the higher the discount. - - Products can also be limited in the available periods. For instance - a VPN only makes sense to be bought for at least one day. - - Rules are as follows: - - given a standard recurring period of ..., changing to ... modifies price ... - - - # One month for free if buying / year, compared to a month: about 8.33% discount - per_year -> per_month -> /11 - per_month -> per_year -> *11 - - # Month has 30.42 days on average. About 7.9% discount to go monthly - per_month -> per_day -> /28 - per_day -> per_month -> *28 - - # Day has 24h, give one for free - per_day -> per_hour -> /23 - per_hour -> per_day -> /23 - - - Examples - - VPN @ 120CHF/y becomes - - 10.91 CHF/month (130.91 CHF/year) - - 0.39 CHF/day (142.21 CHF/year) - - VM @ 15 CHF/month becomes - - 165 CHF/month (13.75 CHF/month) - - 0.54 CHF/day (16.30 CHF/month) - - """ - - - if self.default_recurring_period == RecurringPeriod.PER_365D: - if requested_period == RecurringPeriod.PER_365D: - return self.recurring_price - if requested_period == RecurringPeriod.PER_30D: - return self.recurring_price/11. - if requested_period == RecurringPeriod.PER_DAY: - return self.recurring_price/11./28. - - elif self.default_recurring_period == RecurringPeriod.PER_30D: - if requested_period == RecurringPeriod.PER_365D: - return self.recurring_price*11 - if requested_period == RecurringPeriod.PER_30D: - return self.recurring_price - if requested_period == RecurringPeriod.PER_DAY: - return self.recurring_price/28. - - elif self.default_recurring_period == RecurringPeriod.PER_DAY: - if requested_period == RecurringPeriod.PER_365D: - return self.recurring_price*11*28 - if requested_period == RecurringPeriod.PER_30D: - return self.recurring_price*28 - if requested_period == RecurringPeriod.PER_DAY: - return self.recurring_price - else: - # FIXME: use the right type of exception here! - raise Exception("Did not implement the discounter for this case") - - - def save(self, *args, **kwargs): - try: - ba = BillingAddress.get_address_for(self.owner) - except BillingAddress.DoesNotExist: - raise ValidationError("User does not have a billing address") - - if not ba.active: - raise ValidationError("User does not have an active billing address") - - super().save(*args, **kwargs) - - # Sample products included into uncloud class SampleOneTimeProduct(models.Model): """ diff --git a/uncloud_pay/tests-before-refactor.py b/uncloud_pay/tests-before-refactor.py new file mode 100644 index 0000000..49c51c6 --- /dev/null +++ b/uncloud_pay/tests-before-refactor.py @@ -0,0 +1,721 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model +from datetime import datetime, date, timedelta +from django.utils import timezone + +from .models import * +from uncloud_service.models import GenericServiceProduct + +class OrderTestCase(TestCase): + """ + The heart of ordering products + """ + + def setUp(self): + self.user = get_user_model().objects.create( + username='random_user', + email='jane.random@domain.tld') + + self.ba = BillingAddress.objects.create( + owner=self.user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="somewhere else", + active=True) + + def test_create_one_time_product(self): + """ + One time payment products cannot be updated - can they? + """ + + p = SampleOneTimeProduct.objects.create(owner=self.user) + + self.assertEqual(p.one_time_price, 5) + self.assertEqual(p.recurring_price, 0) + + +# class ProductTestCase(TestCase): +# """ +# Test products and products <-> order interaction +# """ + +# def setUp(self): +# self.user = get_user_model().objects.create( +# username='random_user', +# email='jane.random@domain.tld') + +# self.ba = BillingAddress.objects.create( +# owner=self.user, +# organization = 'Test org', +# street="unknown", +# city="unknown", +# postal_code="somewhere else", +# active=True) + +# def test_create_one_time_product(self): +# """ +# One time payment products cannot be updated - can they? +# """ + +# p = SampleOneTimeProduct.objects.create(owner=self.user) + +# self.assertEqual(p.one_time_price, 5) +# self.assertEqual(p.recurring_price, 0) + +# def test_create_product_without_active_billing_address(self): +# """ +# Fail to create a product without an active billing address +# """ + +# self.ba.active = False +# self.ba.save() + +# with self.assertRaises(ValidationError): +# p = SampleOneTimeProduct.objects.create(owner=self.user) + +# def test_create_product_without_billing_address(self): +# """ +# Fail to create a product without a billing address +# """ + +# user2 = get_user_model().objects.create( +# username='random_user2', +# email='jane.randomly@domain.tld') + +# with self.assertRaises(ValidationError): +# p = SampleOneTimeProduct.objects.create(owner=user2) + + +# def test_create_order_creates_correct_order_count(self): +# """ +# Ensure creating orders from product only creates 1 order +# """ + +# # One order +# p = SampleOneTimeProduct.objects.create(owner=self.user) +# 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(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(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,4))) + + # # FIXME: where is the assert? + + +class BillingAddressTestCase(TestCase): + def setUp(self): + self.user = get_user_model().objects.create( + username='random_user', + email='jane.random@domain.tld') + + + def test_user_no_address(self): + """ + Raise an error, when there is no address + """ + + self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist, + BillingAddress.get_address_for, + self.user) + + def test_user_only_inactive_address(self): + """ + Raise an error, when there is no active address + """ + + ba = BillingAddress.objects.create( + owner=self.user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="somewhere else", + active=False) + + self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist, + BillingAddress.get_address_for, + self.user) + + def test_find_active_address(self): + """ + Find the active address + """ + + ba = BillingAddress.objects.create( + owner=self.user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="unknown", + active=True) + + + self.assertEqual(BillingAddress.get_address_for(self.user), ba) + + def test_find_right_address_with_multiple_addresses(self): + """ + Find the active address only, skip inactive + """ + + ba = BillingAddress.objects.create( + owner=self.user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="unknown", + active=True) + + ba2 = BillingAddress.objects.create( + owner=self.user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="somewhere else", + active=False) + + + self.assertEqual(BillingAddress.get_address_for(self.user), ba) + + def test_change_addresses(self): + """ + Switch the active address + """ + + ba = BillingAddress.objects.create( + owner=self.user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="unknown", + active=True) + + self.assertEqual(BillingAddress.get_address_for(self.user), ba) + + ba.active=False + ba.save() + + ba2 = BillingAddress.objects.create( + owner=self.user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="somewhere else", + active=True) + + self.assertEqual(BillingAddress.get_address_for(self.user), ba2) + + + +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): + def setUp(self): + self.user = get_user_model().objects.create( + username='random_user', + email='jane.random@domain.tld') + + self.ba = BillingAddress.objects.create( + owner=self.user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="somewhere else", + active=True) + + def test_no_pro_rata_first_bill(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(2020,3,3)) + ending_date = timezone.make_aware(datetime.datetime(2020,3,31)) + time_diff = (ending_date - starting_date).total_seconds() + + product.create_order(starting_date) + + bills = Bill.create_next_bills_for_user(self.user, + ending_date=ending_date) + + + # We expect 1 bill for 1 billing address and 1 time frame + self.assertEqual(len(bills), 1) + + pro_rata_amount = time_diff / product.default_recurring_period.value + + self.assertNotEqual(bills[0].sum, pro_rata_amount * price) + self.assertEqual(bills[0].sum, price) + + + def test_downgrade_product(self): + """ + Test downgrading behaviour: + + We create a recurring product (recurring time: 30 days) and downgrade after 15 days. + + We create the bill right AFTER the end of the first order. + + Expected result: + + - First bill record for 30 days + - Second bill record starting after 30 days + - Bill contains two bill records + + """ + + user = self.user + + + + starting_price = 10 + downgrade_price = 5 + + + starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) + first_order_should_end_at = starting_date + datetime.timedelta(days=30) + change1_date = start_after(starting_date + datetime.timedelta(days=15)) + bill_ending_date = change1_date + datetime.timedelta(days=1) + + + product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price) + product.create_order(starting_date) + + product.rc_price = downgrade_price + product.save() + product.create_or_update_recurring_order(when_to_start=change1_date) + + bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) + + bill = bills[0] + bill_records = BillRecord.objects.filter(bill=bill) + + self.assertEqual(len(bill_records), 2) + + self.assertEqual(bill_records[0].starting_date, starting_date) + + self.assertEqual(bill_records[0].order.ending_date, first_order_should_end_at) + + # self.assertEqual(bill_records[0].ending_date, first_order_should_end_at) + + # self.assertEqual(bill_records[0].quantity, 1) + + # self.assertEqual(bill_records[1].quantity, 1) + # self.assertEqual(int(bill.sum), 15) + + def test_upgrade_product(self): + """ + Test upgrading behaviour + """ + + user = self.user + + # Create product + starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) + starting_price = 10 + product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price) + product.create_order(starting_date) + + change1_date = start_after(starting_date + datetime.timedelta(days=15)) + product.rc_price = 20 + product.save() + product.create_or_update_recurring_order(when_to_start=change1_date) + + bill_ending_date = change1_date + datetime.timedelta(days=1) + bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) + + bill = bills[0] + bill_records = BillRecord.objects.filter(bill=bill) + + self.assertEqual(len(bill_records), 2) + self.assertEqual(bill_records[0].quantity, .5) + + self.assertEqual(bill_records[0].ending_date, end_before(change1_date)) + + self.assertEqual(bill_records[1].quantity, 1) + self.assertEqual(bill_records[1].starting_date, change1_date) + + self.assertEqual(int(bill.sum), 25) + + + + # def test_bill_for_increasing_product(self): + # """ + # Modify a product, see one pro rata entry + # """ + + # # Create product + # starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) + # starting_price = 30.5 + # product = SampleRecurringProduct.objects.create(owner=self.user, + # rc_price=starting_price) + # product.create_order(starting_date) + + # recurring_period = product.default_recurring_period.value + + # # First change + # change1_date = timezone.make_aware(datetime.datetime(2019,4,17)) + # product.rc_price = 49.5 + # product.save() + # product.create_or_update_recurring_order(when_to_start=change1_date) + + # # Second change + # change2_date = timezone.make_aware(datetime.datetime(2019,5,8)) + # product.rc_price = 56.5 + # product.save() + # product.create_or_update_recurring_order(when_to_start=change2_date) + + # # Create bill one month after 2nd change + # bill_ending_date = timezone.make_aware(datetime.datetime(2019,6,30)) + # bills = Bill.create_next_bills_for_user(self.user, + # ending_date=bill_ending_date) + + # # only one bill in this test case + # bill = bills[0] + + # expected_amount = starting_price + + # d2 = starting_date + recurring_period + # duration2 = change1_date - d2 + + # expected_amount = 0 + + # # Expected bill sum & records: + # # 2019-03-03 - 2019-04-02 +30d: 30.5 + # # 2019-04-02 - 2019-04-17: +15d: 15.25 + # # 2019-04-17 - 2019-05-08: +21d: (21/30) * 49.5 + # # 2019-05-08 - 2019-06-07: +30d: 56.5 + # # 2019-06-07 - 2019-07-07: +30d: 56.5 + + + # self.assertEqual(bills[0].sum, price) + + # # expeted result: + # # 1x 5 chf bill record + # # 1x 5 chf bill record + # # 1x 10 partial bill record + + + +# 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)) diff --git a/uncloud_pay/tests.py b/uncloud_pay/tests.py index bee3545..a6ec43e 100644 --- a/uncloud_pay/tests.py +++ b/uncloud_pay/tests.py @@ -6,6 +6,36 @@ from django.utils import timezone from .models import * from uncloud_service.models import GenericServiceProduct +import json + +# class OrderTestCase(TestCase): +# """ +# The heart of ordering products +# """ + +# def setUp(self): +# self.user = get_user_model().objects.create( +# username='random_user', +# email='jane.random@domain.tld') + +# self.ba = BillingAddress.objects.create( +# owner=self.user, +# organization = 'Test org', +# street="unknown", +# city="unknown", +# postal_code="somewhere else", +# active=True) + +# def test_create_one_time_product(self): +# """ +# One time payment products cannot be updated - can they? +# """ + +# p = SampleOneTimeProduct.objects.create(owner=self.user) + +# self.assertEqual(p.one_time_price, 5) +# self.assertEqual(p.recurring_price, 0) + class ProductTestCase(TestCase): """ @@ -25,486 +55,534 @@ class ProductTestCase(TestCase): postal_code="somewhere else", active=True) - def test_create_one_time_product(self): + def test_create_product(self): """ - One time payment products cannot be updated - can they? + Create a sample product """ - p = SampleOneTimeProduct.objects.create(owner=self.user) - - self.assertEqual(p.one_time_price, 5) - self.assertEqual(p.recurring_price, 0) - - def test_create_product_without_active_billing_address(self): - """ - Fail to create a product without an active billing address - """ - - self.ba.active = False - self.ba.save() - - with self.assertRaises(ValidationError): - p = SampleOneTimeProduct.objects.create(owner=self.user) - - def test_create_product_without_billing_address(self): - """ - Fail to create a product without a billing address - """ - - user2 = get_user_model().objects.create( - username='random_user2', - email='jane.randomly@domain.tld') - - with self.assertRaises(ValidationError): - p = SampleOneTimeProduct.objects.create(owner=user2) - - - def test_create_order_creates_correct_order_count(self): - """ - Ensure creating orders from product only creates 1 order - """ - - # One order - p = SampleOneTimeProduct.objects.create(owner=self.user) - 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(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(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,4))) - - # FIXME: where is the assert? - - -class BillingAddressTestCase(TestCase): - def setUp(self): - self.user = get_user_model().objects.create( - username='random_user', - email='jane.random@domain.tld') - - - def test_user_no_address(self): - """ - Raise an error, when there is no address - """ - - self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist, - BillingAddress.get_address_for, - self.user) - - def test_user_only_inactive_address(self): - """ - Raise an error, when there is no active address - """ - - ba = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="somewhere else", - active=False) - - self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist, - BillingAddress.get_address_for, - self.user) - - def test_find_active_address(self): - """ - Find the active address - """ - - ba = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="unknown", - active=True) - - - self.assertEqual(BillingAddress.get_address_for(self.user), ba) - - def test_find_right_address_with_multiple_addresses(self): - """ - Find the active address only, skip inactive - """ - - ba = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="unknown", - active=True) - - ba2 = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="somewhere else", - active=False) - - - self.assertEqual(BillingAddress.get_address_for(self.user), ba) - - def test_change_addresses(self): - """ - Switch the active address - """ - - ba = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="unknown", - active=True) - - self.assertEqual(BillingAddress.get_address_for(self.user), ba) - - ba.active=False - ba.save() - - ba2 = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="somewhere else", - active=True) - - self.assertEqual(BillingAddress.get_address_for(self.user), ba2) - - - -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' + config = { + 'features': { + 'cores': + { 'min': 1, + 'max': 48, + 'one_time_price': 0, + 'recurring_price': 4 + }, + 'ram_gb': + { 'min': 1, + 'max': 256, + 'one_time_price': 0, + 'recurring_price': 3 + }, + }, } - 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)) + p = Product.objects.create(name="Testproduct", + description="Only for testing", + config=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)), - ] + # self.assertEqual(p.one_time_price, 5) + # self.assertEqual(p.recurring_price, 0) +# class ProductTestCase(TestCase): +# """ +# Test products and products <-> order interaction +# """ - def test_bill_one_time_one_bill_record(self): - """ - Ensure there is only 1 bill record per order - """ +# def setUp(self): +# self.user = get_user_model().objects.create( +# username='random_user', +# email='jane.random@domain.tld') - bill = Bill.create_next_bill_for_user_address(self.user_addr) +# self.ba = BillingAddress.objects.create( +# owner=self.user, +# organization = 'Test org', +# street="unknown", +# city="unknown", +# postal_code="somewhere else", +# active=True) - self.assertEqual(self.one_time_order.billrecord_set.count(), 1) +# def test_create_one_time_product(self): +# """ +# One time payment products cannot be updated - can they? +# """ - def test_bill_sum_onetime(self): - """ - Check the bill sum for a single one time order - """ +# p = SampleOneTimeProduct.objects.create(owner=self.user) - bill = Bill.create_next_bill_for_user_address(self.user_addr) - self.assertEqual(bill.sum, self.order_meta[1]['price']) +# self.assertEqual(p.one_time_price, 5) +# self.assertEqual(p.recurring_price, 0) + +# def test_create_product_without_active_billing_address(self): +# """ +# Fail to create a product without an active billing address +# """ + +# self.ba.active = False +# self.ba.save() + +# with self.assertRaises(ValidationError): +# p = SampleOneTimeProduct.objects.create(owner=self.user) + +# def test_create_product_without_billing_address(self): +# """ +# Fail to create a product without a billing address +# """ + +# user2 = get_user_model().objects.create( +# username='random_user2', +# email='jane.randomly@domain.tld') + +# with self.assertRaises(ValidationError): +# p = SampleOneTimeProduct.objects.create(owner=user2) - def test_bill_creates_record_for_recurring_order(self): - """ - Ensure there is only 1 bill record per order - """ +# def test_create_order_creates_correct_order_count(self): +# """ +# Ensure creating orders from product only creates 1 order +# """ - bill = Bill.create_next_bill_for_user_address(self.recurring_user_addr) +# # One order +# p = SampleOneTimeProduct.objects.create(owner=self.user) +# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3))) - self.assertEqual(self.recurring_order.billrecord_set.count(), 1) - self.assertEqual(bill.billrecord_set.count(), 1) +# 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(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(timezone.make_aware(datetime.datetime(2020,3,3))) + +# order_count = Order.objects.filter(owner=self.user).count() +# self.assertEqual(order_count, 4) - 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): + # def test_update_recurring_order(self): # """ - # Ensure that a bill only considers "active" orders + # Ensure creating orders from product only creates 1 order # """ - # self.assertEqual(1, 2) + # 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,4))) + + # # FIXME: where is the assert? -class ModifyProductTestCase(TestCase): - def setUp(self): - self.user = get_user_model().objects.create( - username='random_user', - email='jane.random@domain.tld') - - self.ba = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="somewhere else", - active=True) - - def test_no_pro_rata_first_bill(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(2020,3,3)) - ending_date = timezone.make_aware(datetime.datetime(2020,3,31)) - time_diff = (ending_date - starting_date).total_seconds() - - product.create_order(starting_date) - - bills = Bill.create_next_bills_for_user(self.user, - ending_date=ending_date) +# class BillingAddressTestCase(TestCase): +# def setUp(self): +# self.user = get_user_model().objects.create( +# username='random_user', +# email='jane.random@domain.tld') - # We expect 1 bill for 1 billing address and 1 time frame - self.assertEqual(len(bills), 1) +# def test_user_no_address(self): +# """ +# Raise an error, when there is no address +# """ - pro_rata_amount = time_diff / product.default_recurring_period.value +# self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist, +# BillingAddress.get_address_for, +# self.user) - self.assertNotEqual(bills[0].sum, pro_rata_amount * price) - self.assertEqual(bills[0].sum, price) +# def test_user_only_inactive_address(self): +# """ +# Raise an error, when there is no active address +# """ + +# ba = BillingAddress.objects.create( +# owner=self.user, +# organization = 'Test org', +# street="unknown", +# city="unknown", +# postal_code="somewhere else", +# active=False) + +# self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist, +# BillingAddress.get_address_for, +# self.user) + +# def test_find_active_address(self): +# """ +# Find the active address +# """ + +# ba = BillingAddress.objects.create( +# owner=self.user, +# organization = 'Test org', +# street="unknown", +# city="unknown", +# postal_code="unknown", +# active=True) - def test_downgrade_product(self): - """ - Test downgrading behaviour: +# self.assertEqual(BillingAddress.get_address_for(self.user), ba) - We create a recurring product (recurring time: 30 days) and downgrade after 15 days. +# def test_find_right_address_with_multiple_addresses(self): +# """ +# Find the active address only, skip inactive +# """ - We create the bill right AFTER the end of the first order. +# ba = BillingAddress.objects.create( +# owner=self.user, +# organization = 'Test org', +# street="unknown", +# city="unknown", +# postal_code="unknown", +# active=True) - Expected result: +# ba2 = BillingAddress.objects.create( +# owner=self.user, +# organization = 'Test org', +# street="unknown", +# city="unknown", +# postal_code="somewhere else", +# active=False) - - First bill record for 30 days - - Second bill record starting after 30 days - - Bill contains two bill records - """ +# self.assertEqual(BillingAddress.get_address_for(self.user), ba) - user = self.user +# def test_change_addresses(self): +# """ +# Switch the active address +# """ + +# ba = BillingAddress.objects.create( +# owner=self.user, +# organization = 'Test org', +# street="unknown", +# city="unknown", +# postal_code="unknown", +# active=True) + +# self.assertEqual(BillingAddress.get_address_for(self.user), ba) + +# ba.active=False +# ba.save() + +# ba2 = BillingAddress.objects.create( +# owner=self.user, +# organization = 'Test org', +# street="unknown", +# city="unknown", +# postal_code="somewhere else", +# active=True) + +# self.assertEqual(BillingAddress.get_address_for(self.user), ba2) - starting_price = 10 - downgrade_price = 5 +# 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)), +# ] - starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) - first_order_should_end_at = starting_date + datetime.timedelta(days=30) - change1_date = start_after(starting_date + datetime.timedelta(days=15)) - bill_ending_date = change1_date + datetime.timedelta(days=1) + +# 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']) - product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price) - product.create_order(starting_date) +# def test_bill_creates_record_for_recurring_order(self): +# """ +# Ensure there is only 1 bill record per order +# """ - product.rc_price = downgrade_price - product.save() - product.create_or_update_recurring_order(when_to_start=change1_date) +# bill = Bill.create_next_bill_for_user_address(self.recurring_user_addr) - bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) +# self.assertEqual(self.recurring_order.billrecord_set.count(), 1) +# self.assertEqual(bill.billrecord_set.count(), 1) - bill = bills[0] - bill_records = BillRecord.objects.filter(bill=bill) - self.assertEqual(len(bill_records), 2) +# 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 +# """ - self.assertEqual(bill_records[0].starting_date, starting_date) +# for ending_date in self.bill_dates: +# b = Bill.create_next_bill_for_user_address(self.recurring_user_addr, ending_date) +# b.close() - self.assertEqual(bill_records[0].order.ending_date, first_order_should_end_at) +# bill_count = Bill.objects.filter(owner=self.recurring_user).count() - # self.assertEqual(bill_records[0].ending_date, first_order_should_end_at) +# self.assertEqual(len(self.bill_dates), bill_count) - # self.assertEqual(bill_records[0].quantity, 1) +# def test_multi_addr_multi_bill(self): +# """ +# Ensure multiple bills are created if orders exist with different billing addresses +# """ - # self.assertEqual(bill_records[1].quantity, 1) - # self.assertEqual(int(bill.sum), 15) +# username="lotsofplaces" +# multi_addr_user = get_user_model().objects.create( +# username=username, +# email=f"{username}@example.org") - def test_upgrade_product(self): - """ - Test upgrading behaviour - """ +# user_addr1 = BillingAddress.objects.create( +# owner=multi_addr_user, +# organization = 'Test org', +# street="unknown", +# city="unknown", +# postal_code="unknown", +# active=True) - user = self.user +# 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)) - # Create product - starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) - starting_price = 10 - product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price) - product.create_order(starting_date) +# # Make this address inactive +# user_addr1.active = False +# user_addr1.save() - change1_date = start_after(starting_date + datetime.timedelta(days=15)) - product.rc_price = 20 - product.save() - product.create_or_update_recurring_order(when_to_start=change1_date) +# user_addr2 = BillingAddress.objects.create( +# owner=multi_addr_user, +# organization = 'Test2 org', +# street="unknown2", +# city="unknown2", +# postal_code="unknown2", +# active=True) - bill_ending_date = change1_date + datetime.timedelta(days=1) - bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) +# 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)) - bill = bills[0] - bill_records = BillRecord.objects.filter(bill=bill) - self.assertEqual(len(bill_records), 2) - self.assertEqual(bill_records[0].quantity, .5) +# bills = Bill.create_next_bills_for_user(multi_addr_user) - self.assertEqual(bill_records[0].ending_date, end_before(change1_date)) +# self.assertEqual(len(bills), 2) - self.assertEqual(bill_records[1].quantity, 1) - self.assertEqual(bill_records[1].starting_date, change1_date) - self.assertEqual(int(bill.sum), 25) +# # 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( +# username='random_user', +# email='jane.random@domain.tld') + +# self.ba = BillingAddress.objects.create( +# owner=self.user, +# organization = 'Test org', +# street="unknown", +# city="unknown", +# postal_code="somewhere else", +# active=True) + +# def test_no_pro_rata_first_bill(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(2020,3,3)) +# ending_date = timezone.make_aware(datetime.datetime(2020,3,31)) +# time_diff = (ending_date - starting_date).total_seconds() + +# product.create_order(starting_date) + +# bills = Bill.create_next_bills_for_user(self.user, +# ending_date=ending_date) + + +# # We expect 1 bill for 1 billing address and 1 time frame +# self.assertEqual(len(bills), 1) + +# pro_rata_amount = time_diff / product.default_recurring_period.value + +# self.assertNotEqual(bills[0].sum, pro_rata_amount * price) +# self.assertEqual(bills[0].sum, price) + + +# def test_downgrade_product(self): +# """ +# Test downgrading behaviour: + +# We create a recurring product (recurring time: 30 days) and downgrade after 15 days. + +# We create the bill right AFTER the end of the first order. + +# Expected result: + +# - First bill record for 30 days +# - Second bill record starting after 30 days +# - Bill contains two bill records + +# """ + +# user = self.user + + + +# starting_price = 10 +# downgrade_price = 5 + + +# starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) +# first_order_should_end_at = starting_date + datetime.timedelta(days=30) +# change1_date = start_after(starting_date + datetime.timedelta(days=15)) +# bill_ending_date = change1_date + datetime.timedelta(days=1) + + +# product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price) +# product.create_order(starting_date) + +# product.rc_price = downgrade_price +# product.save() +# product.create_or_update_recurring_order(when_to_start=change1_date) + +# bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) + +# bill = bills[0] +# bill_records = BillRecord.objects.filter(bill=bill) + +# self.assertEqual(len(bill_records), 2) + +# self.assertEqual(bill_records[0].starting_date, starting_date) + +# self.assertEqual(bill_records[0].order.ending_date, first_order_should_end_at) + +# # self.assertEqual(bill_records[0].ending_date, first_order_should_end_at) + +# # self.assertEqual(bill_records[0].quantity, 1) + +# # self.assertEqual(bill_records[1].quantity, 1) +# # self.assertEqual(int(bill.sum), 15) + +# def test_upgrade_product(self): +# """ +# Test upgrading behaviour +# """ + +# user = self.user + +# # Create product +# starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) +# starting_price = 10 +# product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price) +# product.create_order(starting_date) + +# change1_date = start_after(starting_date + datetime.timedelta(days=15)) +# product.rc_price = 20 +# product.save() +# product.create_or_update_recurring_order(when_to_start=change1_date) + +# bill_ending_date = change1_date + datetime.timedelta(days=1) +# bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) + +# bill = bills[0] +# bill_records = BillRecord.objects.filter(bill=bill) + +# self.assertEqual(len(bill_records), 2) +# self.assertEqual(bill_records[0].quantity, .5) + +# self.assertEqual(bill_records[0].ending_date, end_before(change1_date)) + +# self.assertEqual(bill_records[1].quantity, 1) +# self.assertEqual(bill_records[1].starting_date, change1_date) + +# self.assertEqual(int(bill.sum), 25)