Merge branch 'uncloud-product-activation' into 'master'
Handle product activation on payment See merge request uncloud/uncloud!6
This commit is contained in:
commit
7afb3f8793
16 changed files with 352 additions and 38 deletions
|
@ -44,6 +44,8 @@ router.register(r'vm/vm', vmviews.VMProductViewSet, basename='vmproduct')
|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
router.register(r'service/matrix', serviceviews.MatrixServiceProductViewSet, basename='matrixserviceproduct')
|
router.register(r'service/matrix', serviceviews.MatrixServiceProductViewSet, basename='matrixserviceproduct')
|
||||||
|
router.register(r'service/generic', serviceviews.GenericServiceProductViewSet, basename='genericserviceproduct')
|
||||||
|
|
||||||
|
|
||||||
# Net
|
# Net
|
||||||
router.register(r'net/vpn', netviews.VPNNetworkViewSet, basename='vpnnet')
|
router.register(r'net/vpn', netviews.VPNNetworkViewSet, basename='vpnnet')
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.0.5 on 2020-04-17 05:51
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('uncloud_net', '0002_auto_20200409_1225'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vpnnetwork',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,6 +1,6 @@
|
||||||
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, PaymentMethod, get_balance_for
|
from uncloud_pay.models import Order, Bill, PaymentMethod, get_balance_for_user
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -15,7 +15,7 @@ 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)
|
balance = get_balance_for_user(user)
|
||||||
if balance < 0:
|
if balance < 0:
|
||||||
print("User {} has negative balance ({}), charging.".format(user.username, balance))
|
print("User {} has negative balance ({}), charging.".format(user.username, balance))
|
||||||
payment_method = PaymentMethod.get_primary_for(user)
|
payment_method = PaymentMethod.get_primary_for(user)
|
||||||
|
|
|
@ -92,19 +92,19 @@ 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.
|
||||||
## We override save() in order to active products awaiting payment.
|
def save(self, *args, **kwargs):
|
||||||
#def save(self, *args, **kwargs):
|
# _state.adding is switched to false after super(...) call.
|
||||||
# # TODO: only run activation logic on creation, not on update.
|
being_created = self._state.adding
|
||||||
# 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(
|
unpaid_bills_before_payment = Bill.get_unpaid_for(self.owner)
|
||||||
# set(unpaid_bills_before_payment) - set(unpaid_bills_after_payment))
|
super(Payment, self).save(*args, **kwargs) # Save payment in DB.
|
||||||
# for bill in newly_paid_bills:
|
unpaid_bills_after_payment = Bill.get_unpaid_for(self.owner)
|
||||||
# bill.activate_orders()
|
|
||||||
|
|
||||||
|
newly_paid_bills = list(
|
||||||
|
set(unpaid_bills_before_payment) - set(unpaid_bills_after_payment))
|
||||||
|
for bill in newly_paid_bills:
|
||||||
|
bill.activate_products()
|
||||||
|
|
||||||
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)
|
||||||
|
@ -201,6 +201,12 @@ class Bill(models.Model):
|
||||||
|
|
||||||
valid = models.BooleanField(default=True)
|
valid = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
# Trigger product activation if bill paid at creation (from balance).
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super(Bill, self).save(*args, **kwargs)
|
||||||
|
if not self in Bill.get_unpaid_for(self.owner):
|
||||||
|
self.activate_products()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reference(self):
|
def reference(self):
|
||||||
return "{}-{}".format(
|
return "{}-{}".format(
|
||||||
|
@ -227,6 +233,15 @@ class Bill(models.Model):
|
||||||
# A bill is final when its ending date is passed.
|
# A bill is final when its ending date is passed.
|
||||||
return self.ending_date < timezone.now()
|
return self.ending_date < timezone.now()
|
||||||
|
|
||||||
|
def activate_products(self):
|
||||||
|
for order in self.order_set.all():
|
||||||
|
# FIXME: using __something might not be a good idea.
|
||||||
|
for product_class in Product.__subclasses__():
|
||||||
|
for product in product_class.objects.filter(order=order):
|
||||||
|
if product.status == UncloudStatus.AWAITING_PAYMENT:
|
||||||
|
product.status = UncloudStatus.PENDING
|
||||||
|
product.save()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_for(year, month, user):
|
def generate_for(year, month, user):
|
||||||
# /!\ We exclusively work on the specified year and month.
|
# /!\ We exclusively work on the specified year and month.
|
||||||
|
@ -248,7 +263,7 @@ class Bill(models.Model):
|
||||||
# (next_bill) ending_date, a new bill has to be generated.
|
# (next_bill) ending_date, a new bill has to be generated.
|
||||||
# * For yearly bill: if previous_bill.ending_date is on working
|
# * For yearly bill: if previous_bill.ending_date is on working
|
||||||
# month, generate new bill.
|
# month, generate new bill.
|
||||||
unpaid_orders = { 'monthly_or_less': [], 'yearly': {}}
|
unpaid_orders = { 'monthly_or_less': [], 'yearly': {} }
|
||||||
for order in orders:
|
for order in orders:
|
||||||
try:
|
try:
|
||||||
previous_bill = order.bill.latest('ending_date')
|
previous_bill = order.bill.latest('ending_date')
|
||||||
|
@ -276,7 +291,7 @@ class Bill(models.Model):
|
||||||
else:
|
else:
|
||||||
unpaid_orders['yearly'][next_yearly_bill_start_on] = bucket + [order]
|
unpaid_orders['yearly'][next_yearly_bill_start_on] = bucket + [order]
|
||||||
else:
|
else:
|
||||||
if previous_bill == None or previous_bill.ending_date <= ending_date:
|
if previous_bill == None or previous_bill.ending_date < ending_date:
|
||||||
unpaid_orders['monthly_or_less'].append(order)
|
unpaid_orders['monthly_or_less'].append(order)
|
||||||
|
|
||||||
# Handle working month's billing.
|
# Handle working month's billing.
|
||||||
|
@ -335,24 +350,23 @@ class Bill(models.Model):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_unpaid_for(user):
|
def get_unpaid_for(user):
|
||||||
balance = get_balance_for(user)
|
balance = get_balance_for_user(user)
|
||||||
unpaid_bills = []
|
unpaid_bills = []
|
||||||
# No unpaid bill if balance is positive.
|
# No unpaid bill if balance is positive.
|
||||||
if balance >= 0:
|
if balance >= 0:
|
||||||
return []
|
return unpaid_bills
|
||||||
else:
|
else:
|
||||||
bills = Bill.objects.filter(
|
bills = Bill.objects.filter(
|
||||||
owner=user,
|
owner=user,
|
||||||
due_date__lt=timezone.now()
|
|
||||||
).order_by('-creation_date')
|
).order_by('-creation_date')
|
||||||
|
|
||||||
# Amount to be paid by the customer.
|
# Amount to be paid by the customer.
|
||||||
unpaid_balance = abs(balance)
|
unpaid_balance = abs(balance)
|
||||||
for bill in bills:
|
for bill in bills:
|
||||||
if unpaid_balance < 0:
|
if unpaid_balance <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
unpaid_balance -= bill.amount
|
unpaid_balance -= bill.total
|
||||||
unpaid_bills.append(bill)
|
unpaid_bills.append(bill)
|
||||||
|
|
||||||
return unpaid_bills
|
return unpaid_bills
|
||||||
|
@ -464,6 +478,11 @@ class Order(models.Model):
|
||||||
choices = RecurringPeriod.choices,
|
choices = RecurringPeriod.choices,
|
||||||
default = RecurringPeriod.PER_MONTH)
|
default = RecurringPeriod.PER_MONTH)
|
||||||
|
|
||||||
|
# Trigger initial bill generation at order creation.
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super(Order, self).save(*args, **kwargs)
|
||||||
|
Bill.generate_for(self.starting_date.year, self.starting_date.month, self.owner)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def records(self):
|
def records(self):
|
||||||
return OrderRecord.objects.filter(order=self)
|
return OrderRecord.objects.filter(order=self)
|
||||||
|
@ -477,6 +496,10 @@ class Order(models.Model):
|
||||||
return reduce(lambda acc, record: acc + record.recurring_price, self.records, 0)
|
return reduce(lambda acc, record: acc + record.recurring_price, self.records, 0)
|
||||||
|
|
||||||
# Used by uncloud_pay tests.
|
# Used by uncloud_pay tests.
|
||||||
|
@property
|
||||||
|
def bills(self):
|
||||||
|
return Bill.objects.filter(order=self)
|
||||||
|
|
||||||
def add_record(self, one_time_price, recurring_price, description):
|
def add_record(self, one_time_price, recurring_price, description):
|
||||||
OrderRecord.objects.create(order=self,
|
OrderRecord.objects.create(order=self,
|
||||||
one_time_price=one_time_price,
|
one_time_price=one_time_price,
|
||||||
|
@ -531,11 +554,11 @@ class Product(UncloudModel):
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
editable=False)
|
editable=False)
|
||||||
|
|
||||||
description = ""
|
description = "Generic Product"
|
||||||
|
|
||||||
status = models.CharField(max_length=32,
|
status = models.CharField(max_length=32,
|
||||||
choices=UncloudStatus.choices,
|
choices=UncloudStatus.choices,
|
||||||
default=UncloudStatus.PENDING)
|
default=UncloudStatus.AWAITING_PAYMENT)
|
||||||
|
|
||||||
order = models.ForeignKey(Order,
|
order = models.ForeignKey(Order,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -556,7 +579,7 @@ class Product(UncloudModel):
|
||||||
if being_created:
|
if being_created:
|
||||||
record = OrderRecord(
|
record = OrderRecord(
|
||||||
one_time_price=self.one_time_price,
|
one_time_price=self.one_time_price,
|
||||||
recurring_price=self.recurring_price(self.recurring_period),
|
recurring_price=self.recurring_price(recurring_period=self.recurring_period),
|
||||||
description=self.description)
|
description=self.description)
|
||||||
self.order.orderrecord_set.add(record, bulk=False)
|
self.order.orderrecord_set.add(record, bulk=False)
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.contrib.auth import get_user_model
|
||||||
from datetime import datetime, date, timedelta
|
from datetime import datetime, date, timedelta
|
||||||
|
|
||||||
from .models import *
|
from .models import *
|
||||||
|
from uncloud_service.models import GenericServiceProduct
|
||||||
|
|
||||||
class BillingTestCase(TestCase):
|
class BillingTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -10,9 +11,6 @@ class BillingTestCase(TestCase):
|
||||||
username='jdoe',
|
username='jdoe',
|
||||||
email='john.doe@domain.tld')
|
email='john.doe@domain.tld')
|
||||||
|
|
||||||
def test_truth(self):
|
|
||||||
self.assertEqual(1+1, 2)
|
|
||||||
|
|
||||||
def test_basic_monthly_billing(self):
|
def test_basic_monthly_billing(self):
|
||||||
one_time_price = 10
|
one_time_price = 10
|
||||||
recurring_price = 20
|
recurring_price = 20
|
||||||
|
@ -31,7 +29,7 @@ class BillingTestCase(TestCase):
|
||||||
order.add_record(one_time_price, recurring_price, description)
|
order.add_record(one_time_price, recurring_price, description)
|
||||||
|
|
||||||
# Generate & check bill for first month: full recurring_price + setup.
|
# Generate & check bill for first month: full recurring_price + setup.
|
||||||
first_month_bills = Bill.generate_for(2020, 3, self.user)
|
first_month_bills = order.bills # Initial bill generated at order creation.
|
||||||
self.assertEqual(len(first_month_bills), 1)
|
self.assertEqual(len(first_month_bills), 1)
|
||||||
self.assertEqual(first_month_bills[0].total, one_time_price + recurring_price)
|
self.assertEqual(first_month_bills[0].total, one_time_price + recurring_price)
|
||||||
|
|
||||||
|
@ -65,7 +63,7 @@ class BillingTestCase(TestCase):
|
||||||
order.add_record(one_time_price, recurring_price, description)
|
order.add_record(one_time_price, recurring_price, description)
|
||||||
|
|
||||||
# Generate & check bill for first year: recurring_price + setup.
|
# Generate & check bill for first year: recurring_price + setup.
|
||||||
first_year_bills = Bill.generate_for(2020, 3, self.user)
|
first_year_bills = order.bills # Initial bill generated at order creation.
|
||||||
self.assertEqual(len(first_year_bills), 1)
|
self.assertEqual(len(first_year_bills), 1)
|
||||||
self.assertEqual(first_year_bills[0].starting_date.date(),
|
self.assertEqual(first_year_bills[0].starting_date.date(),
|
||||||
date.fromisoformat('2020-03-31'))
|
date.fromisoformat('2020-03-31'))
|
||||||
|
@ -106,7 +104,7 @@ class BillingTestCase(TestCase):
|
||||||
order.add_record(one_time_price, recurring_price, description)
|
order.add_record(one_time_price, recurring_price, description)
|
||||||
|
|
||||||
# Generate & check bill for first month: recurring_price + setup.
|
# Generate & check bill for first month: recurring_price + setup.
|
||||||
first_month_bills = Bill.generate_for(2020, 3, self.user)
|
first_month_bills = order.bills
|
||||||
self.assertEqual(len(first_month_bills), 1)
|
self.assertEqual(len(first_month_bills), 1)
|
||||||
self.assertEqual(float(first_month_bills[0].total),
|
self.assertEqual(float(first_month_bills[0].total),
|
||||||
round(16 * recurring_price, AMOUNT_DECIMALS) + one_time_price)
|
round(16 * recurring_price, AMOUNT_DECIMALS) + one_time_price)
|
||||||
|
@ -116,3 +114,41 @@ class BillingTestCase(TestCase):
|
||||||
self.assertEqual(len(second_month_bills), 1)
|
self.assertEqual(len(second_month_bills), 1)
|
||||||
self.assertEqual(float(second_month_bills[0].total),
|
self.assertEqual(float(second_month_bills[0].total),
|
||||||
round(12 * recurring_price, AMOUNT_DECIMALS))
|
round(12 * recurring_price, AMOUNT_DECIMALS))
|
||||||
|
|
||||||
|
class ProductActivationTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = get_user_model().objects.create(
|
||||||
|
username='jdoe',
|
||||||
|
email='john.doe@domain.tld')
|
||||||
|
|
||||||
|
def test_product_activation(self):
|
||||||
|
starting_date = datetime.fromisoformat('2020-03-01')
|
||||||
|
|
||||||
|
order = Order.objects.create(
|
||||||
|
owner=self.user,
|
||||||
|
starting_date=starting_date,
|
||||||
|
recurring_period=RecurringPeriod.PER_MONTH)
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
product = GenericServiceProduct(
|
||||||
|
custom_description="Test product",
|
||||||
|
custom_one_time_price=0,
|
||||||
|
custom_recurring_price=20,
|
||||||
|
owner=self.user,
|
||||||
|
order=order)
|
||||||
|
product.save()
|
||||||
|
|
||||||
|
# XXX: to be automated.
|
||||||
|
order.add_record(product.one_time_price, product.recurring_price(), product.description)
|
||||||
|
|
||||||
|
# Validate initial state: must be awaiting payment.
|
||||||
|
self.assertEqual(product.status, UncloudStatus.AWAITING_PAYMENT)
|
||||||
|
|
||||||
|
# Pay initial bill, check that product is activated.
|
||||||
|
amount = product.order.bills[0].total
|
||||||
|
payment = Payment(owner=self.user, amount=amount)
|
||||||
|
payment.save()
|
||||||
|
self.assertEqual(
|
||||||
|
GenericServiceProduct.objects.get(uuid=product.uuid).status,
|
||||||
|
UncloudStatus.PENDING
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Generated by Django 3.0.5 on 2020-04-18 06:41
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('uncloud_pay', '0005_auto_20200413_0924'),
|
||||||
|
('uncloud_service', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='matrixserviceproduct',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GenericServiceProduct',
|
||||||
|
fields=[
|
||||||
|
('extra_data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True)),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('status', models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32)),
|
||||||
|
('custom_description', models.TextField()),
|
||||||
|
('custom_recurring_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])),
|
||||||
|
('custom_one_time_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])),
|
||||||
|
('order', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.Order')),
|
||||||
|
('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,8 +1,9 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from uncloud_pay.models import Product, RecurringPeriod
|
from uncloud_pay.models import Product, RecurringPeriod, AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS
|
||||||
from uncloud_vm.models import VMProduct, VMDiskImageProduct
|
from uncloud_vm.models import VMProduct, VMDiskImageProduct
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
|
|
||||||
class MatrixServiceProduct(Product):
|
class MatrixServiceProduct(Product):
|
||||||
monthly_managment_fee = 20
|
monthly_managment_fee = 20
|
||||||
|
@ -16,7 +17,7 @@ class MatrixServiceProduct(Product):
|
||||||
domain = models.CharField(max_length=255, default='domain.tld')
|
domain = models.CharField(max_length=255, default='domain.tld')
|
||||||
|
|
||||||
# Default recurring price is PER_MONT, see Product class.
|
# Default recurring price is PER_MONT, see Product class.
|
||||||
def recurring_price(self):
|
def recurring_price(self, recurring_period=RecurringPeriod.PER_MONTH):
|
||||||
return self.monthly_managment_fee
|
return self.monthly_managment_fee
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -33,3 +34,30 @@ class MatrixServiceProduct(Product):
|
||||||
@property
|
@property
|
||||||
def one_time_price(self):
|
def one_time_price(self):
|
||||||
return 30
|
return 30
|
||||||
|
|
||||||
|
class GenericServiceProduct(Product):
|
||||||
|
custom_description = models.TextField()
|
||||||
|
custom_recurring_price = models.DecimalField(default=0.0,
|
||||||
|
max_digits=AMOUNT_MAX_DIGITS,
|
||||||
|
decimal_places=AMOUNT_DECIMALS,
|
||||||
|
validators=[MinValueValidator(0)])
|
||||||
|
custom_one_time_price = models.DecimalField(default=0.0,
|
||||||
|
max_digits=AMOUNT_MAX_DIGITS,
|
||||||
|
decimal_places=AMOUNT_DECIMALS,
|
||||||
|
validators=[MinValueValidator(0)])
|
||||||
|
|
||||||
|
def recurring_price(self, recurring_period=RecurringPeriod.PER_MONTH):
|
||||||
|
# FIXME: handle recurring_period somehow.
|
||||||
|
return self.custom_recurring_price
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
return self.custom_description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def one_time_price(self):
|
||||||
|
return self.custom_one_time_price
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def allowed_recurring_periods():
|
||||||
|
return RecurringPeriod.choices
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import MatrixServiceProduct
|
from .models import *
|
||||||
from uncloud_vm.serializers import ManagedVMProductSerializer
|
from uncloud_vm.serializers import ManagedVMProductSerializer
|
||||||
from uncloud_vm.models import VMProduct
|
from uncloud_vm.models import VMProduct
|
||||||
from uncloud_pay.models import RecurringPeriod
|
from uncloud_pay.models import RecurringPeriod
|
||||||
|
@ -15,3 +15,14 @@ class MatrixServiceProductSerializer(serializers.ModelSerializer):
|
||||||
model = MatrixServiceProduct
|
model = MatrixServiceProduct
|
||||||
fields = ['uuid', 'order', 'owner', 'status', 'vm', 'domain', 'recurring_period']
|
fields = ['uuid', 'order', 'owner', 'status', 'vm', 'domain', 'recurring_period']
|
||||||
read_only_fields = ['uuid', 'order', 'owner', 'status']
|
read_only_fields = ['uuid', 'order', 'owner', 'status']
|
||||||
|
|
||||||
|
class GenericServiceProductSerializer(serializers.ModelSerializer):
|
||||||
|
# Custom field used at creation (= ordering) only.
|
||||||
|
recurring_period = serializers.ChoiceField(
|
||||||
|
choices=GenericServiceProduct.allowed_recurring_periods())
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = GenericServiceProduct
|
||||||
|
fields = ['uuid', 'order', 'owner', 'status', 'custom_recurring_price',
|
||||||
|
'custom_description', 'custom_one_time_price', 'recurring_period']
|
||||||
|
read_only_fields = ['uuid', 'order', 'owner', 'status']
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
from rest_framework import viewsets, permissions
|
from rest_framework import viewsets, permissions
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from .models import MatrixServiceProduct
|
from .models import *
|
||||||
from .serializers import MatrixServiceProductSerializer
|
from .serializers import *
|
||||||
|
|
||||||
from uncloud_pay.helpers import ProductViewSet
|
from uncloud_pay.helpers import ProductViewSet
|
||||||
from uncloud_pay.models import Order
|
from uncloud_pay.models import Order
|
||||||
|
@ -47,7 +48,10 @@ class MatrixServiceProductViewSet(ProductViewSet):
|
||||||
# Create base order.)
|
# Create base order.)
|
||||||
order = Order.objects.create(
|
order = Order.objects.create(
|
||||||
recurring_period=order_recurring_period,
|
recurring_period=order_recurring_period,
|
||||||
owner=request.user)
|
owner=request.user,
|
||||||
|
starting_date=timezone.now()
|
||||||
|
)
|
||||||
|
order.save()
|
||||||
|
|
||||||
# Create unerderlying VM.
|
# Create unerderlying VM.
|
||||||
data = serializer.validated_data.pop('vm')
|
data = serializer.validated_data.pop('vm')
|
||||||
|
@ -65,3 +69,45 @@ class MatrixServiceProductViewSet(ProductViewSet):
|
||||||
vm=vm)
|
vm=vm)
|
||||||
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
class GenericServiceProductViewSet(ProductViewSet):
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
serializer_class = GenericServiceProductSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return GenericServiceProduct.objects.filter(owner=self.request.user)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def create(self, request):
|
||||||
|
# Extract serializer data.
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
order_recurring_period = serializer.validated_data.pop("recurring_period")
|
||||||
|
|
||||||
|
# Create base order.
|
||||||
|
order = Order.objects.create(
|
||||||
|
recurring_period=order_recurring_period,
|
||||||
|
owner=request.user,
|
||||||
|
starting_date=timezone.now()
|
||||||
|
)
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
# Create service.
|
||||||
|
print(serializer.validated_data)
|
||||||
|
service = serializer.save(order=order, owner=request.user)
|
||||||
|
|
||||||
|
# XXX: Move this to some kind of on_create hook in parent
|
||||||
|
# Product class?
|
||||||
|
order.add_record(
|
||||||
|
service.one_time_price,
|
||||||
|
service.recurring_price,
|
||||||
|
service.description)
|
||||||
|
|
||||||
|
# XXX: Move this to some kind of on_create hook in parent
|
||||||
|
# Product class?
|
||||||
|
order.add_record(
|
||||||
|
service.one_time_price,
|
||||||
|
service.recurring_price,
|
||||||
|
service.description)
|
||||||
|
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
|
@ -7,7 +7,7 @@ import django.db.models.deletion
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('uncloud_vm', '0003_remove_vmhost_vms'),
|
('uncloud_vm', '0004_remove_vmproduct_vmid'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 3.0.5 on 2020-04-17 05:51
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('uncloud_vm', '0008_auto_20200403_1727'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vmproduct',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vmsnapshotproduct',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Generated by Django 3.0.5 on 2020-04-18 06:41
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('uncloud_vm', '0009_auto_20200417_0551'),
|
||||||
|
('uncloud_vm', '0010_auto_20200413_0924'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.0.5 on 2020-04-18 06:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('uncloud_vm', '0011_merge_20200418_0641'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vmdiskproduct',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32),
|
||||||
|
),
|
||||||
|
]
|
|
@ -116,7 +116,7 @@ class VMProductViewSet(ProductViewSet):
|
||||||
order_recurring_period = serializer.validated_data.pop("recurring_period")
|
order_recurring_period = serializer.validated_data.pop("recurring_period")
|
||||||
|
|
||||||
# Create base order.
|
# Create base order.
|
||||||
order = Order.objects.create(
|
order = Order(
|
||||||
recurring_period=order_recurring_period,
|
recurring_period=order_recurring_period,
|
||||||
owner=request.user,
|
owner=request.user,
|
||||||
starting_date=timezone.now()
|
starting_date=timezone.now()
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.0.5 on 2020-04-17 05:51
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ungleich_service', '0004_auto_20200403_1727'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='matrixserviceproduct',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Generated by Django 3.0.5 on 2020-04-17 08:02
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('uncloud_pay', '0005_auto_20200417_0551'),
|
||||||
|
('ungleich_service', '0005_auto_20200417_0551'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GenericServiceProduct',
|
||||||
|
fields=[
|
||||||
|
('extra_data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True)),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('status', models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32)),
|
||||||
|
('custom_description', models.TextField()),
|
||||||
|
('custom_recurring_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])),
|
||||||
|
('custom_one_time_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])),
|
||||||
|
('order', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.Order')),
|
||||||
|
('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
Loading…
Reference in a new issue