From 623d3ae5c464dd99b171ae1556d8fc45963b1f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Mon, 9 Mar 2020 11:30:31 +0100 Subject: [PATCH] Fix various billing issues discovered by testing --- uncloud/uncloud_pay/models.py | 58 ++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/uncloud/uncloud_pay/models.py b/uncloud/uncloud_pay/models.py index 639dd1d..65bf6ef 100644 --- a/uncloud/uncloud_pay/models.py +++ b/uncloud/uncloud_pay/models.py @@ -9,6 +9,7 @@ from django.core.exceptions import ObjectDoesNotExist import django.db.models.signals as signals import uuid +import logging from functools import reduce from math import ceil from datetime import timedelta @@ -18,20 +19,28 @@ import uncloud_pay.stripe from uncloud_pay.helpers import beginning_of_month, end_of_month from decimal import Decimal +import decimal # Define DecimalField properties, used to represent amounts of money. AMOUNT_MAX_DIGITS=10 AMOUNT_DECIMALS=2 +# FIXME: check why we need +1 here. +decimal.getcontext().prec = AMOUNT_DECIMALS + 1 + # Used to generate bill due dates. BILL_PAYMENT_DELAY=timedelta(days=10) +# Initialize logger. +logger = logging.getLogger(__name__) + # See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types class RecurringPeriod(models.TextChoices): ONE_TIME = 'ONCE', _('Onetime') PER_YEAR = 'YEAR', _('Per Year') PER_MONTH = 'MONTH', _('Per Month') PER_MINUTE = 'MINUTE', _('Per Minute') + PER_WEEK = 'WEEK', _('Per Week') PER_DAY = 'DAY', _('Per Day') PER_HOUR = 'HOUR', _('Per Hour') PER_SECOND = 'SECOND', _('Per Second') @@ -249,13 +258,16 @@ class Bill(models.Model): # 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() + next_yearly_bill_start_on = order.starting_date elif previous_bill.ending_date <= ending_date: - next_yearly_bill_start_on = (previous_bill.ending_date + timedelta(days=1)).date() + next_yearly_bill_start_on = (previous_bill.ending_date + timedelta(days=1)) # 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: + # We want to group orders by date but keep using datetimes. + next_yearly_bill_start_on = next_yearly_bill_start_on.replace( + minute=0, hour=0, second=0, microsecond=0) bucket = unpaid_orders['yearly'].get(next_yearly_bill_start_on) if bucket == None: unpaid_orders['yearly'][next_yearly_bill_start_on] = [order] @@ -273,7 +285,7 @@ class Bill(models.Model): next_monthly_bill = Bill.objects.create(owner=user, creation_date=creation_date, - starting_date=starting_date.datetime(), # FIXME: this is a hack! + starting_date=starting_date, # FIXME: this is a hack! ending_date=ending_date, due_date=postpaid_due_date) @@ -282,8 +294,7 @@ class Bill(models.Model): for order in unpaid_orders['monthly_or_less']: order.bill.add(next_monthly_bill) - # TODO: use logger. - print("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. @@ -295,9 +306,10 @@ class Bill(models.Model): # 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) + prepaid_due_date = min(creation_date, next_yearly_bill_start_on) + BILL_PAYMENT_DELAY + # Bump by one year, remove one day. + 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, @@ -310,8 +322,7 @@ class Bill(models.Model): 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 {}." + logger.info("Generated yearly bill {} (amount: {}) for user {}." .format(next_yearly_bill.uuid, next_yearly_bill.total, user)) # Add to output. @@ -361,7 +372,7 @@ class BillRecord(): self.recurring_period = order_record.recurring_period self.description = order_record.description - if self.order.starting_date > self.bill.starting_date: + if self.order.starting_date >= self.bill.starting_date: self.one_time_price = order_record.one_time_price else: self.one_time_price = 0 @@ -370,7 +381,7 @@ class BillRecord(): def recurring_count(self): # Compute billing delta. billed_until = self.bill.ending_date - if self.order.ending_date != None and self.order.ending_date < self.order.ending_date: + if self.order.ending_date != None and self.order.ending_date <= self.bill.ending_date: billed_until = self.order.ending_date billed_from = self.bill.starting_date @@ -387,10 +398,9 @@ class BillRecord(): # TODO: refactor this thing? # TODO: weekly 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) + # XXX: Should always be one => we do not bill for more than one year. + # TODO: check billed_delta is ~365 days. + return 1 elif self.recurring_period == RecurringPeriod.PER_MONTH: days = ceil(billed_delta / timedelta(days=1)) @@ -404,28 +414,28 @@ class BillRecord(): (_, days_in_month) = monthrange( self.bill.starting_date.year, self.bill.starting_date.month) - return Decimal(days / days_in_month) + return days / days_in_month elif self.recurring_period == RecurringPeriod.PER_WEEK: weeks = ceil(billed_delta / timedelta(week=1)) - return Decimal(weeks) + return weeks elif self.recurring_period == RecurringPeriod.PER_DAY: days = ceil(billed_delta / timedelta(days=1)) - return Decimal(days) + return days elif self.recurring_period == RecurringPeriod.PER_HOUR: hours = ceil(billed_delta / timedelta(hours=1)) - return Decimal(hours) + return hours elif self.recurring_period == RecurringPeriod.PER_SECOND: seconds = ceil(billed_delta / timedelta(seconds=1)) - return Decimal(seconds) + return seconds elif self.recurring_period == RecurringPeriod.ONE_TIME: - return Decimal(0) + return 0 else: raise Exception('Unsupported recurring period: {}.'. format(record.recurring_period)) @property def amount(self): - return self.recurring_price * self.recurring_count + self.one_time_price + return Decimal(float(self.recurring_price) * self.recurring_count) + self.one_time_price ### # Orders. @@ -440,7 +450,7 @@ class Order(models.Model): # TODO: enforce ending_date - starting_date to be larger than recurring_period. creation_date = models.DateTimeField(auto_now_add=True) - starting_date = models.DateTimeField(auto_now_add=True) + starting_date = models.DateTimeField() ending_date = models.DateTimeField(blank=True, null=True)