From d089d06264719c0652102084b883cad6666d3cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Thu, 5 Mar 2020 16:22:41 +0100 Subject: [PATCH] Initial yearly billing implementation --- uncloud/uncloud_pay/models.py | 118 ++++++++++++++++++++++++++-------- uncloud/uncloud_vm/models.py | 5 +- 2 files changed, 94 insertions(+), 29 deletions(-) diff --git a/uncloud/uncloud_pay/models.py b/uncloud/uncloud_pay/models.py index 24512a5..639dd1d 100644 --- a/uncloud/uncloud_pay/models.py +++ b/uncloud/uncloud_pay/models.py @@ -219,51 +219,106 @@ class Bill(models.Model): @staticmethod def generate_for(year, month, user): # /!\ We exclusively work on the specified year and month. + generated_bills = [] - # Default values for next bill (if any). Only saved at the end of - # this method, if relevant. - next_bill = Bill(owner=user, - starting_date=beginning_of_month(year, month), - ending_date=end_of_month(year, month), - creation_date=timezone.now(), - due_date=timezone.now() + BILL_PAYMENT_DELAY) + # Default values for next bill (if any). + starting_date=beginning_of_month(year, month) + ending_date=end_of_month(year, month) + creation_date=timezone.now() - # Select all orders active on the request period. + # Select all orders active on the request period (i.e. starting on or after starting_date). orders = Order.objects.filter( - Q(ending_date__gt=next_bill.starting_date) | Q(ending_date__isnull=True), + Q(ending_date__gte=starting_date) | Q(ending_date__isnull=True), owner=user) # Check if there is already a bill covering the order and period pair: # * Get latest bill by ending_date: previous_bill.ending_date - # * If previous_bill.ending_date is before next_bill.ending_date, a new - # bill has to be generated. - unpaid_orders = [] + # * For monthly bills: if previous_bill.ending_date is before + # (next_bill) ending_date, a new bill has to be generated. + # * For yearly bill: if previous_bill.ending_date is on working + # month, generate new bill. + unpaid_orders = { 'monthly_or_less': [], 'yearly': {}} for order in orders: try: previous_bill = order.bill.latest('ending_date') except ObjectDoesNotExist: previous_bill = None - if previous_bill == None or previous_bill.ending_date < next_bill.ending_date: - unpaid_orders.append(order) + # FIXME: control flow is confusing in this block. + if order.recurring_period == RecurringPeriod.PER_YEAR: + # We ignore anything smaller than a day in here. + next_yearly_bill_start_on = None + if previous_bill == None: + next_yearly_bill_start_on = (order.starting_date + timedelta(days=1)).date() + elif previous_bill.ending_date <= ending_date: + next_yearly_bill_start_on = (previous_bill.ending_date + timedelta(days=1)).date() - # Commit next_bill if it there are 'unpaid' orders. - if len(unpaid_orders) > 0: - next_bill.save() + # Store for bill generation. One bucket per day of month with a starting bill. + # bucket is a reference here, no need to reassign. + if next_yearly_bill_start_on: + bucket = unpaid_orders['yearly'].get(next_yearly_bill_start_on) + if bucket == None: + unpaid_orders['yearly'][next_yearly_bill_start_on] = [order] + else: + unpaid_orders['yearly'][next_yearly_bill_start_on] = bucket + [order] + else: + if previous_bill == None or previous_bill.ending_date <= ending_date: + unpaid_orders['monthly_or_less'].append(order) + + # Handle working month's billing. + if len(unpaid_orders['monthly_or_less']) > 0: + # TODO: PREPAID billing is not supported yet. + 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, + creation_date=creation_date, + starting_date=starting_date.datetime(), # 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: - order.bill.add(next_bill) + for order in unpaid_orders['monthly_or_less']: + order.bill.add(next_monthly_bill) # TODO: use logger. - print("Generated bill {} (amount: {}) for user {}." - .format(next_bill.uuid, next_bill.total, user)) + print("Generated monthly bill {} (amount: {}) for user {}." + .format(next_monthly_bill.uuid, next_monthly_bill.total, user)) - return next_bill + # Add to output. + generated_bills.append(next_monthly_bill) - # Return None if no bill was created. - return None + # Handle yearly bills starting on working month. + if len(unpaid_orders['yearly']) > 0: + + # For every starting date, generate new bill. + for next_yearly_bill_start_on in unpaid_orders['yearly']: + # No postpaid for yearly payments. + prepaid_due_date = min(creation_date.date(), next_yearly_bill_start_on) + BILL_PAYMENT_DELAY + # FIXME: a year is not exactly 365 days... + ending_date = next_yearly_bill_start_on + timedelta(days=365) + + 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) + + # 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) + + # TODO: use logger. + print("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 @staticmethod def get_unpaid_for(user): @@ -323,7 +378,7 @@ class BillRecord(): billed_from = self.order.starting_date if billed_from > billed_until: - # TODO: think about and check edges cases. This should not be + # TODO: think about and check edge cases. This should not be # possible. raise Exception('Impossible billing delta!') @@ -331,11 +386,15 @@ class BillRecord(): # TODO: refactor this thing? # TODO: weekly - # TODO: yearly - if self.recurring_period == RecurringPeriod.PER_MONTH: + if self.recurring_period == RecurringPeriod.PER_YEAR: + # Should always be one, but let's be careful. + # FIXME: a year is not exactly 365 days... + years = (billed_delta / timedelta(days=365)) + return Decimal(years) + elif self.recurring_period == RecurringPeriod.PER_MONTH: days = ceil(billed_delta / timedelta(days=1)) - # XXX: we assume monthly bills for now. + # Monthly bills always cover one single month. if (self.bill.starting_date.year != self.bill.starting_date.year or self.bill.starting_date.month != self.bill.ending_date.month): raise Exception('Bill {} covers more than one month. Cannot bill PER_MONTH.'. @@ -346,6 +405,9 @@ class BillRecord(): self.bill.starting_date.year, self.bill.starting_date.month) return Decimal(days / days_in_month) + elif self.recurring_period == RecurringPeriod.PER_WEEK: + weeks = ceil(billed_delta / timedelta(week=1)) + return Decimal(weeks) elif self.recurring_period == RecurringPeriod.PER_DAY: days = ceil(billed_delta / timedelta(days=1)) return Decimal(days) diff --git a/uncloud/uncloud_vm/models.py b/uncloud/uncloud_vm/models.py index 60dfc0a..573b5f5 100644 --- a/uncloud/uncloud_vm/models.py +++ b/uncloud/uncloud_vm/models.py @@ -72,6 +72,8 @@ class VMProduct(Product): return self.cores * 3 + self.ram_in_gb * 4 elif recurring_period == RecurringPeriod.PER_HOUR: return self.cores * 4.0/(30 * 24) + self.ram_in_gb * 4.5/(30* 24) + elif recurring_period == RecurringPeriod.PER_YEAR: + return (self.cores * 2.5 + self.ram_in_gb * 3.5) * 12 else: raise Exception('Invalid recurring period for VM Product pricing.') @@ -88,7 +90,8 @@ class VMProduct(Product): @staticmethod def allowed_recurring_periods(): return list(filter( - lambda pair: pair[0] in [RecurringPeriod.PER_MONTH, RecurringPeriod.PER_HOUR], + lambda pair: pair[0] in [RecurringPeriod.PER_YEAR, + RecurringPeriod.PER_MONTH, RecurringPeriod.PER_HOUR], RecurringPeriod.choices)) class VMWithOSProduct(VMProduct):