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 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_product_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': 4 }, }, } vm_order_config = { 'features': { 'cores': 2, 'ram_gb': 2 } } vm_order_downgrade_config = { 'features': { 'cores': 1, 'ram_gb': 1 } } vm_order_upgrade_config = { 'features': { 'cores': 4, 'ram_gb': 4 } } 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_product(self): """ Create a sample product """ p = Product.objects.create(name="Testproduct", description="Only for testing", config=vm_product_config) 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_order_product(self): """ Order a product, ensure the order has correct price setup """ p = Product.objects.create(name="Testproduct", description="Only for testing", config=vm_product_config) o = Order.objects.create(owner=self.user, billing_address=self.ba, product=p, config=vm_order_config) self.assertEqual(o.one_time_price, 0) self.assertEqual(o.recurring_price, 16) def test_change_order(self): """ Change an order and ensure that - a new order is created - the price is correct in the new order """ p = Product.objects.create(name="Testproduct", description="Only for testing", config=vm_product_config) order1 = Order.objects.create(owner=self.user, billing_address=self.ba, product=p, config=vm_order_config) self.assertEqual(order1.one_time_price, 0) self.assertEqual(order1.recurring_price, 16) class ModifyOrderTestCase(TestCase): """ Test typical order flows like - cancelling - downgrading - upgrading """ 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) self.product = Product.objects.create(name="Testproduct", description="Only for testing", config=vm_product_config) def test_change_order(self): """ Test changing an order Expected result: - Old order should be closed before new order starts - New order should start at starting data """ user = self.user starting_price = 16 downgrade_price = 8 starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) ending1_date = starting_date + datetime.timedelta(days=15) change1_date = start_after(ending1_date) bill_ending_date = change1_date + datetime.timedelta(days=1) order1 = Order.objects.create(owner=self.user, billing_address=BillingAddress.get_address_for(self.user), product=self.product, config=vm_order_config, starting_date=starting_date) order1.update_order(vm_order_downgrade_config, starting_date=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].ending_date, ending1_date) self.assertEqual(bill_records[1].starting_date, change1_date) # 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 = 16 downgrade_price = 8 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) order1 = Order.objects.create(owner=self.user, billing_address=BillingAddress.get_address_for(self.user), product=self.product, config=vm_order_config, starting_date=starting_date) order1.update_order(vm_order_downgrade_config, starting_date=change1_date) # product = Product.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 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.vm = Product.objects.create(name="Super Fast VM", description="Zooooom", config=vm_product_config) # 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 order_vm(self, owner=None): if not owner: owner = self.recurring_user return Order.objects.create( owner=owner, product=self.vm, config=vm_order_config, billing_address=BillingAddress.get_address_for(self.recurring_user), starting_date=timezone.make_aware(datetime.datetime(2020,3,3)), ) 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 """ order = self.order_vm() bill = Bill.create_next_bill_for_user_address(self.recurring_user_addr) self.assertEqual(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 """ order = self.order_vm() 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, # 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) # 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 # """ # 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 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))