Fix various billing issues discovered by testing
This commit is contained in:
		
					parent
					
						
							
								fe0e6d98bf
							
						
					
				
			
			
				commit
				
					
						623d3ae5c4
					
				
			
		
					 1 changed files with 34 additions and 24 deletions
				
			
		| 
						 | 
				
			
			@ -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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue