Move bill generation logic to Bill class, initial work for prepaid

This commit is contained in:
fnux 2020-03-04 10:55:12 +01:00
parent 9aabc66e57
commit 9e8149135b
2 changed files with 56 additions and 31 deletions

View file

@ -1,12 +1,12 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from uncloud_auth.models import User from uncloud_auth.models import User
from uncloud_pay.models import Order, Bill, get_balance_for from uncloud_pay.models import Bill
from datetime import timedelta from datetime import timedelta
from django.utils import timezone from django.utils import timezone
class Command(BaseCommand): class Command(BaseCommand):
help = 'Generate bills and charge customers if necessary.' help = 'Take action on overdue bills.'
def add_arguments(self, parser): def add_arguments(self, parser):
pass pass
@ -15,28 +15,9 @@ class Command(BaseCommand):
users = User.objects.all() users = User.objects.all()
print("Processing {} users.".format(users.count())) print("Processing {} users.".format(users.count()))
for user in users: for user in users:
balance = get_balance_for(user) for bill in Bill.get_overdue_for(user):
if balance < 0: print("/!\ Overdue bill for {}, {} with amount {}"
print("User {} has negative balance ({}), checking for overdue bills." .format(user.username, bill.uuid, bill.amount))
.format(user.username, balance)) # TODO: take action?
# Get bills DESCENDING by creation date (= latest at top).
bills = Bill.objects.filter(
owner=user,
due_date__lt=timezone.now()
).order_by('-creation_date')
overdue_balance = abs(balance)
overdue_bills = []
for bill in bills:
if overdue_balance < 0:
break # XXX: I'm (fnux) not fond of breaks!
overdue_balance -= bill.amount
overdue_bills.append(bill)
for bill in overdue_bills:
print("/!\ Overdue bill for {}, {} with amount {}"
.format(user.username, bill.uuid, bill.amount))
# TODO: take action?
print("=> Done.") print("=> Done.")

View file

@ -18,10 +18,14 @@ import uncloud_pay.stripe
from uncloud_pay.helpers import beginning_of_month, end_of_month from uncloud_pay.helpers import beginning_of_month, end_of_month
from decimal import Decimal from decimal import Decimal
# Define DecimalField properties, used to represent amounts of money. # Define DecimalField properties, used to represent amounts of money.
AMOUNT_MAX_DIGITS=10 AMOUNT_MAX_DIGITS=10
AMOUNT_DECIMALS=2 AMOUNT_DECIMALS=2
# Used to generate bill due dates.
BILL_PAYMENT_DELAY=timedelta(days=10)
# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types # See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
class RecurringPeriod(models.TextChoices): class RecurringPeriod(models.TextChoices):
ONE_TIME = 'ONCE', _('Onetime') ONE_TIME = 'ONCE', _('Onetime')
@ -86,6 +90,20 @@ class Payment(models.Model):
default='unknown') default='unknown')
timestamp = models.DateTimeField(editable=False, auto_now_add=True) timestamp = models.DateTimeField(editable=False, auto_now_add=True)
# WIP prepaid and service activation logic by fnux.
## We override save() in order to active products awaiting payment.
#def save(self, *args, **kwargs):
# # TODO: only run activation logic on creation, not on update.
# unpaid_bills_before_payment = Bill.get_unpaid_for(self.owner)
# super(Payment, self).save(*args, **kwargs) # Save payment in DB.
# unpaid_bills_after_payment = Bill.get_unpaid_for(self.owner)
# newly_paid_bills = list(
# set(unpaid_bills_before_payment) - set(unpaid_bills_after_payment))
# for bill in newly_paid_bills:
# bill.activate_orders()
class PaymentMethod(models.Model): class PaymentMethod(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
owner = models.ForeignKey(get_user_model(), owner = models.ForeignKey(get_user_model(),
@ -183,7 +201,7 @@ class Bill(models.Model):
return self.ending_date < timezone.now() return self.ending_date < timezone.now()
@staticmethod @staticmethod
def generate_for(year, month, user, allowed_delay): def generate_for(year, month, user):
# /!\ We exclusively work on the specified year and month. # /!\ We exclusively work on the specified year and month.
# Default values for next bill (if any). Only saved at the end of # Default values for next bill (if any). Only saved at the end of
@ -192,7 +210,7 @@ class Bill(models.Model):
starting_date=beginning_of_month(year, month), starting_date=beginning_of_month(year, month),
ending_date=end_of_month(year, month), ending_date=end_of_month(year, month),
creation_date=timezone.now(), creation_date=timezone.now(),
due_date=timezone.now() + allowed_delay) due_date=timezone.now() + BILL_PAYMENT_DELAY)
# Select all orders active on the request period. # Select all orders active on the request period.
orders = Order.objects.filter( orders = Order.objects.filter(
@ -231,6 +249,35 @@ class Bill(models.Model):
# Return None if no bill was created. # Return None if no bill was created.
return None return None
@staticmethod
def get_unpaid_for(user):
balance = get_balance_for(user)
unpaid_bills = []
# No unpaid bill if balance is positive.
if balance >= 0:
return []
else:
bills = Bill.objects.filter(
owner=user,
due_date__lt=timezone.now()
).order_by('-creation_date')
# Amount to be paid by the customer.
unpaid_balance = abs(balance)
for bill in bills:
if unpaid_balance < 0:
break
unpaid_balance -= bill.amount
unpaid_bills.append(bill)
return unpaid_bills
@staticmethod
def get_overdue_for(user):
unpaid_bills = Bill.get_unpaid_for(user)
return list(filter(lambda bill: bill.due_date > timezone.now(), unpaid_bills))
class BillRecord(): class BillRecord():
""" """
Entry of a bill, dynamically generated from order records. Entry of a bill, dynamically generated from order records.
@ -345,9 +392,6 @@ class Order(models.Model):
recurring_price=recurring_price, recurring_price=recurring_price,
description=description) description=description)
def generate_bill(self):
pass
class OrderRecord(models.Model): class OrderRecord(models.Model):
""" """
@ -398,7 +442,7 @@ class Product(models.Model):
status = models.CharField(max_length=32, status = models.CharField(max_length=32,
choices=ProductStatus.choices, choices=ProductStatus.choices,
default=ProductStatus.AWAITING_PAYMENT) default=ProductStatus.PENDING)
order = models.ForeignKey(Order, order = models.ForeignKey(Order,
on_delete=models.CASCADE, on_delete=models.CASCADE,