diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index 3b545fb..f10f813 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -9,10 +9,10 @@ from django.core.exceptions import ObjectDoesNotExist import uuid import logging from functools import reduce +import itertools from math import ceil from datetime import timedelta from calendar import monthrange - from decimal import Decimal import uncloud_pay.stripe @@ -525,10 +525,12 @@ class Bill(models.Model): @property def billing_address(self): - # FIXME: make sure all the orders of a bill match the same billing address. orders = Order.objects.filter(bill=self) + # The genrate_for method makes sure all the orders of a bill share the + # same billing address. TODO: It would be nice to enforce that somehow... return orders[0].billing_address + # TODO: split this huuuge method! @staticmethod def generate_for(year, month, user): # /!\ We exclusively work on the specified year and month. @@ -587,22 +589,29 @@ class Bill(models.Model): prepaid_due_date = min(creation_date, starting_date) + BILL_PAYMENT_DELAY postpaid_due_date = max(creation_date, ending_date) + BILL_PAYMENT_DELAY - next_monthly_bill = Bill.objects.create(owner=user, + # There should not be any bill linked to orders with different + # billing addresses. + per_address_orders = itertools.groupby( + unpaid_orders['monthly_or_less'], + lambda o: o.billing_address) + + for addr, bill_orders in per_address_orders: + next_monthly_bill = Bill.objects.create(owner=user, creation_date=creation_date, starting_date=starting_date, # FIXME: this is a hack! ending_date=ending_date, due_date=postpaid_due_date) - # It is not possible to register many-to-many relationship before - # the two end-objects are saved in database. - for order in unpaid_orders['monthly_or_less']: - order.bill.add(next_monthly_bill) + # It is not possible to register many-to-many relationship before + # the two end-objects are saved in database. + for order in bill_orders: + order.bill.add(next_monthly_bill) - logger.info("Generated monthly bill {} (amount: {}) for user {}." + logger.info("Generated monthly bill {} (amount: {}) for user {}." .format(next_monthly_bill.uuid, next_monthly_bill.total, user)) - # Add to output. - generated_bills.append(next_monthly_bill) + # Add to output. + generated_bills.append(next_monthly_bill) # Handle yearly bills starting on working month. if len(unpaid_orders['yearly']) > 0: @@ -614,22 +623,29 @@ class Bill(models.Model): ending_date = next_yearly_bill_start_on.replace( year=next_yearly_bill_start_on.year+1) - timedelta(days=1) - next_yearly_bill = Bill.objects.create(owner=user, - creation_date=creation_date, - starting_date=next_yearly_bill_start_on, - ending_date=ending_date, - due_date=prepaid_due_date) + # There should not be any bill linked to orders with different + # billing addresses. + per_address_orders = itertools.groupby( + unpaid_orders['yearly'][next_yearly_bill_start_on], + lambda o: o.billing_address) - # It is not possible to register many-to-many relationship before - # the two end-objects are saved in database. - for order in unpaid_orders['yearly'][next_yearly_bill_start_on]: - order.bill.add(next_yearly_bill) + for addr, bill_orders in per_address_orders: + next_yearly_bill = Bill.objects.create(owner=user, + creation_date=creation_date, + starting_date=next_yearly_bill_start_on, + ending_date=ending_date, + due_date=prepaid_due_date) - logger.info("Generated yearly bill {} (amount: {}) for user {}." - .format(next_yearly_bill.uuid, next_yearly_bill.total, user)) + # It is not possible to register many-to-many relationship before + # the two end-objects are saved in database. + for order in bill_orders: + order.bill.add(next_yearly_bill) - # Add to output. - generated_bills.append(next_yearly_bill) + logger.info("Generated yearly bill {} (amount: {}) for user {}." + .format(next_yearly_bill.uuid, next_yearly_bill.total, user)) + + # Add to output. + generated_bills.append(next_yearly_bill) # Return generated (monthly + yearly) bills. return generated_bills diff --git a/uncloud_django_based/uncloud/uncloud_pay/tests.py b/uncloud_django_based/uncloud/uncloud_pay/tests.py index 5236c8a..9b23c68 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/tests.py +++ b/uncloud_django_based/uncloud/uncloud_pay/tests.py @@ -143,7 +143,6 @@ class ProductActivationTestCase(TestCase): starting_date=starting_date, recurring_period=RecurringPeriod.PER_MONTH, billing_address=self.billing_address) - order.save() product = GenericServiceProduct( custom_description="Test product", @@ -154,7 +153,7 @@ class ProductActivationTestCase(TestCase): product.save() # XXX: to be automated. - order.add_record(product.one_time_price, product.recurring_price(), product.description) + order.add_record(product.one_time_price, product.recurring_price, product.description) # Validate initial state: must be awaiting payment. self.assertEqual(product.status, UncloudStatus.AWAITING_PAYMENT) @@ -167,3 +166,63 @@ class ProductActivationTestCase(TestCase): GenericServiceProduct.objects.get(uuid=product.uuid).status, UncloudStatus.PENDING ) + +class BillingAddressTestCase(TestCase): + def setUp(self): + self.user = get_user_model().objects.create( + username='jdoe', + email='john.doe@domain.tld') + + self.billing_address_01 = BillingAddress.objects.create( + owner=self.user, + street="unknown1", + city="unknown1", + postal_code="unknown1", + country="CH") + + self.billing_address_02 = BillingAddress.objects.create( + owner=self.user, + street="unknown2", + city="unknown2", + postal_code="unknown2", + country="CH") + + def test_billing_with_single_address(self): + # Create new orders somewhere in the past so that we do not encounter + # auto-created initial bills. + starting_date = datetime.fromisoformat('2020-03-01') + + order_01 = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address_01) + order_02 = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address_01) + + # We need a single bill since we work with a single address. + bills = Bill.generate_for(2020, 4, self.user) + self.assertEqual(len(bills), 1) + + def test_billing_with_multiple_addresses(self): + # Create new orders somewhere in the past so that we do not encounter + # auto-created initial bills. + starting_date = datetime.fromisoformat('2020-03-01') + + order_01 = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address_01) + order_02 = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address_02) + + # We need different bills since we work with different addresses. + bills = Bill.generate_for(2020, 4, self.user) + self.assertEqual(len(bills), 2)