Pay: move some model-related methods from helpers to models

Otherwise we end up in circular dependency hell
This commit is contained in:
fnux 2020-03-04 09:39:18 +01:00
commit 9aabc66e57
6 changed files with 108 additions and 107 deletions

View file

@ -1,17 +1,23 @@
from django.db import models
from functools import reduce
from django.db.models import Q
from django.contrib.auth import get_user_model
from django.core.validators import MinValueValidator
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.dispatch import receiver
from django.core.exceptions import ObjectDoesNotExist
import django.db.models.signals as signals
import uuid
from functools import reduce
from math import ceil
from datetime import timedelta
from calendar import monthrange
import uncloud_pay.stripe
from uncloud_pay.helpers import beginning_of_month, end_of_month
from decimal import Decimal
import uuid
# Define DecimalField properties, used to represent amounts of money.
AMOUNT_MAX_DIGITS=10
AMOUNT_DECIMALS=2
@ -26,6 +32,34 @@ class RecurringPeriod(models.TextChoices):
PER_HOUR = 'HOUR', _('Per Hour')
PER_SECOND = 'SECOND', _('Per Second')
# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
class ProductStatus(models.TextChoices):
PENDING = 'PENDING', _('Pending')
AWAITING_PAYMENT = 'AWAITING_PAYMENT', _('Awaiting payment')
BEING_CREATED = 'BEING_CREATED', _('Being created')
ACTIVE = 'ACTIVE', _('Active')
DELETED = 'DELETED', _('Deleted')
###
# Users.
def get_balance_for(user):
bills = reduce(
lambda acc, entry: acc + entry.total,
Bill.objects.filter(owner=user),
0)
payments = reduce(
lambda acc, entry: acc + entry.amount,
Payment.objects.filter(owner=user),
0)
return payments - bills
class StripeCustomer(models.Model):
owner = models.OneToOneField( get_user_model(),
primary_key=True,
on_delete=models.CASCADE)
stripe_id = models.CharField(max_length=32)
###
# Payments and Payment Methods.
@ -100,15 +134,19 @@ class PaymentMethod(models.Model):
else:
raise Exception('Cannot charge negative amount.')
def get_primary_for(user):
methods = PaymentMethod.objects.filter(owner=user)
for method in methods:
# Do we want to do something with non-primary method?
if method.primary:
return method
return None
class Meta:
unique_together = [['owner', 'primary']]
class StripeCustomer(models.Model):
owner = models.OneToOneField( get_user_model(),
primary_key=True,
on_delete=models.CASCADE)
stripe_id = models.CharField(max_length=32)
###
# Bills & Payments.
@ -144,6 +182,55 @@ class Bill(models.Model):
# A bill is final when its ending date is passed.
return self.ending_date < timezone.now()
@staticmethod
def generate_for(year, month, user, allowed_delay):
# /!\ We exclusively work on the specified year and month.
# 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() + allowed_delay)
# Select all orders active on the request period.
orders = Order.objects.filter(
Q(ending_date__gt=next_bill.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 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)
# Commit next_bill if it there are 'unpaid' orders.
if len(unpaid_orders) > 0:
next_bill.save()
# 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)
# TODO: use logger.
print("Generated bill {} (amount: {}) for user {}."
.format(next_bill.uuid, next_bill.total, user))
return next_bill
# Return None if no bill was created.
return None
class BillRecord():
"""
Entry of a bill, dynamically generated from order records.
@ -258,6 +345,10 @@ class Order(models.Model):
recurring_price=recurring_price,
description=description)
def generate_bill(self):
pass
class OrderRecord(models.Model):
"""
Order records store billing informations for products: the actual product
@ -305,15 +396,9 @@ class Product(models.Model):
description = ""
status = models.CharField(max_length=256,
choices = (
('pending', 'Pending'),
('being_created', 'Being created'),
('active', 'Active'),
('deleted', 'Deleted')
),
default='pending'
)
status = models.CharField(max_length=32,
choices=ProductStatus.choices,
default=ProductStatus.AWAITING_PAYMENT)
order = models.ForeignKey(Order,
on_delete=models.CASCADE,