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
parent e0cb6ac670
commit 9aabc66e57
6 changed files with 108 additions and 107 deletions

View File

@ -2,32 +2,9 @@ from functools import reduce
from datetime import datetime
from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from django.db.models import Q
from .models import Bill, Payment, PaymentMethod, Order
from django.utils import timezone
from django.core.exceptions import ObjectDoesNotExist
from calendar import monthrange
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
def get_payment_method_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
def beginning_of_month(year, month):
tz = timezone.get_current_timezone()
return datetime(year=year, month=month, day=1, tzinfo=tz)
@ -38,53 +15,6 @@ def end_of_month(year, month):
return datetime(year=year, month=month, day=days,
hour=23, minute=59, second=59, tzinfo=tz)
def generate_bills_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.
class ProductViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.ListModelMixin,

View File

@ -1,7 +1,6 @@
from django.core.management.base import BaseCommand
from uncloud_auth.models import User
from uncloud_pay.models import Order, Bill
from uncloud_pay.helpers import get_balance_for, get_payment_method_for
from uncloud_pay.models import Order, Bill, PaymentMethod, get_balance_for
from datetime import timedelta
from django.utils import timezone
@ -19,7 +18,7 @@ class Command(BaseCommand):
balance = get_balance_for(user)
if balance < 0:
print("User {} has negative balance ({}), charging.".format(user.username, balance))
payment_method = get_payment_method_for(user)
payment_method = PaymentMethod.get_primary_for(user)
if payment_method != None:
amount_to_be_charged = abs(balance)
charge_ok = payment_method.charge(amount_to_be_charged)

View File

@ -7,7 +7,7 @@ from django.core.exceptions import ObjectDoesNotExist
from datetime import timedelta, date
from django.utils import timezone
from uncloud_pay.helpers import generate_bills_for
from uncloud_pay.models import Bill
BILL_PAYMENT_DELAY=timedelta(days=10)
@ -28,7 +28,7 @@ class Command(BaseCommand):
for user in users:
now = timezone.now()
generate_bills_for(
Bill.generate_for(
year=now.year,
month=now.month,
user=user,

View File

@ -1,7 +1,6 @@
from django.core.management.base import BaseCommand
from uncloud_auth.models import User
from uncloud_pay.models import Order, Bill
from uncloud_pay.helpers import get_balance_for, get_payment_method_for
from uncloud_pay.models import Order, Bill, get_balance_for
from datetime import timedelta
from django.utils import timezone

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,

View File

@ -1,13 +1,6 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers
from .models import *
from .helpers import get_balance_for
from functools import reduce
from uncloud_vm.serializers import VMProductSerializer
from uncloud_vm.models import VMProduct
import uncloud_pay.stripe as stripe
###
# Users.
@ -19,8 +12,6 @@ class UserSerializer(serializers.ModelSerializer):
# Display current 'balance'
balance = serializers.SerializerMethodField('get_balance')
def __sum_balance(self, entries):
return reduce(lambda acc, entry: acc + entry.amount, entries, 0)
def get_balance(self, user):
return get_balance_for(user)
@ -92,6 +83,3 @@ class OrderSerializer(serializers.ModelSerializer):
model = Order
fields = ['uuid', 'creation_date', 'starting_date', 'ending_date',
'bill', 'recurring_period', 'records', 'recurring_price', 'one_time_price']
class ProductSerializer(serializers.Serializer):
vms = VMProductSerializer(many=True, read_only=True)