forked from uncloud/uncloud
remove big mistake: orders from product
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
This commit is contained in:
parent
d8a7964fed
commit
1aead50170
8 changed files with 287 additions and 35 deletions
|
@ -84,6 +84,14 @@ python manage.py migrate
|
|||
* URLs
|
||||
- api/ - the rest API
|
||||
* uncloud Products
|
||||
** Product features
|
||||
- Dependencies on other products
|
||||
- Minimum parameters (min cpu, min ram, etc).
|
||||
- Can also realise the dcl vm
|
||||
- dualstack vm = VM + IPv4 + SSD
|
||||
- Need to have a non-misguiding name for the "bare VM"
|
||||
- Should support network boot (?)
|
||||
|
||||
** VPN
|
||||
*** How to add a new VPN Host
|
||||
**** Install wireguard to the host
|
||||
|
@ -219,7 +227,85 @@ VPNNetworks can be managed by all authenticated users.
|
|||
- Product
|
||||
***** TODO Recurring product support
|
||||
****** TODO Support replacing orders for updates
|
||||
****** TODO [#A] Finish split of bill creation
|
||||
****** TODO [#A] Test the new functions in the Order class
|
||||
****** DONE [#A] Finish split of bill creation
|
||||
CLOSED: [2020-09-11 Fri 23:19]
|
||||
****** TODO Test the new functions in the Order class
|
||||
****** Define the correct order replacement logic
|
||||
Assumption:
|
||||
- recurringperiods are 30days
|
||||
******* Case 1: downgrading
|
||||
- User commits to 10 CHF for 30 days
|
||||
- Wants to downgrade after 15 days to 5 CHF product
|
||||
- Expected result:
|
||||
- order 1: 10 CHF until +30days
|
||||
- order 2: 5 CHF starting 30days + 1s
|
||||
- Sum of the two orders is 15 CHF
|
||||
- Question is
|
||||
- when is the VM shutdown?
|
||||
- a) instantly
|
||||
- b) at the end of the cycle
|
||||
- best solution
|
||||
- user can choose between a ... b any time
|
||||
******* Duration
|
||||
- You cannot cancel the duration
|
||||
- You can upgrade and with that cancel the duration
|
||||
- The idea of a duration is that you commit for it
|
||||
- If you want to commit lower (daily basis for instance) you
|
||||
have higher per period prices
|
||||
******* Case X
|
||||
- User has VM with 2 Core / 2 GB RAM
|
||||
- User modifies with to 1 core / 3 GB RAM
|
||||
- We treat it as down/upgrade independent of the modifications
|
||||
|
||||
******* Case 2: upgrading after 1 day
|
||||
- committed for 30 days
|
||||
- upgrade after 1 day
|
||||
- so first order will be charged for 1/30ths
|
||||
|
||||
******* Case 2: upgrading
|
||||
- User commits to 10 CHF for 30 days
|
||||
- Wants to upgrade after 15 days to 20 CHF product
|
||||
- Order 1 : 1 VM with 2 Core / 2 GB / 10 SSD -- 10 CHF
|
||||
- 30days period, stopped after 15, so quantity is 0.5 = 5 CHF
|
||||
- Order 2 : 1 VM with 2 Core / 6 GB / 10 SSD -- 20 CHF
|
||||
- after 15 days
|
||||
- VM is upgraded instantly
|
||||
- Expected result:
|
||||
- order 1: 10 CHF until +15days = 0.5 units = 5 CHF
|
||||
- order 2: 20 CHF starting 15days + 1s ... +30 days after
|
||||
the 15 days -> 45 days = 1 unit = 20 CHF
|
||||
- Total on bill: 25 CHF
|
||||
|
||||
******* Case 2: upgrading
|
||||
- User commits to 10 CHF for 30 days
|
||||
- Wants to upgrade after 15 days to 20 CHF product
|
||||
- Expected result:
|
||||
- order 1: 10 CHF until +30days = 1 units = 10 CHF
|
||||
|
||||
- order 2: 20 CHF starting 15days + 1s = 1 unit = 20 CHF
|
||||
- Total on bill: 30 CHF
|
||||
|
||||
|
||||
****** TODO Note: ending date not set if replaced by default (implicit!)
|
||||
- Should the new order modify the old order on save()?
|
||||
****** DONE Fix totally wrong bill dates in our test case
|
||||
CLOSED: [2020-09-09 Wed 01:00]
|
||||
- 2020 used instead of 2019
|
||||
- Was due to existing test data ...
|
||||
***** TODO Bill logic is still wrong
|
||||
- Bill starting_date is the date of the first order
|
||||
- However first encountered order does not have to be the
|
||||
earliest in the bill!
|
||||
- Bills should not have a duration
|
||||
- Bills should only have a (unique) issue date
|
||||
- We charge based on bill_records
|
||||
- Last time charged issue date of the bill OR earliest date
|
||||
after that
|
||||
- Every bill generation checks all (relevant) orders
|
||||
- add a flag "not_for_billing" or "closed"
|
||||
- query on that flag
|
||||
- verify it every time
|
||||
|
||||
|
||||
***** TODO Generating bill for admins/staff
|
||||
-
|
||||
|
|
17
opennebula/migrations/0005_remove_vm_orders.py
Normal file
17
opennebula/migrations/0005_remove_vm_orders.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('opennebula', '0004_auto_20200809_1237'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='vm',
|
||||
name='orders',
|
||||
),
|
||||
]
|
17
uncloud_net/migrations/0005_remove_vpnnetwork_orders.py
Normal file
17
uncloud_net/migrations/0005_remove_vpnnetwork_orders.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_net', '0004_auto_20200809_1237'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='vpnnetwork',
|
||||
name='orders',
|
||||
),
|
||||
]
|
25
uncloud_pay/migrations/0015_auto_20200928_1844.py
Normal file
25
uncloud_pay/migrations/0015_auto_20200928_1844.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0014_auto_20200825_1915'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='sampleonetimeproduct',
|
||||
name='orders',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='samplerecurringproduct',
|
||||
name='orders',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='samplerecurringproductonetimefee',
|
||||
name='orders',
|
||||
),
|
||||
]
|
|
@ -354,17 +354,18 @@ class Order(models.Model):
|
|||
return self.starting_date + datetime.timedelta(seconds=self.recurring_period)
|
||||
|
||||
@property
|
||||
def next_ending_date(self):
|
||||
def next_cancel_or_downgrade_date(self):
|
||||
"""
|
||||
Return the next proper ending date after n times the
|
||||
recurring_period, where n is an integer.
|
||||
recurring_period, where n is an integer that applies for downgrading
|
||||
or cancelling.
|
||||
"""
|
||||
|
||||
if self.recurring_period > 0:
|
||||
now = timezone.now()
|
||||
delta = now - self.starting_date
|
||||
|
||||
num_times = math.ceil(delta.total_seconds() / self.recurring_period)
|
||||
num_times = ceil(delta.total_seconds() / self.recurring_period)
|
||||
|
||||
next_date = self.starting_date + datetime.timedelta(seconds= num_times * self.recurring_period)
|
||||
else:
|
||||
|
@ -452,8 +453,9 @@ class Order(models.Model):
|
|||
if self.ending_date and self.ending_date < self.starting_date:
|
||||
raise ValidationError("End date cannot be before starting date")
|
||||
|
||||
if self.ending_date and self.ending_date < self.earliest_ending_date:
|
||||
raise ValidationError("Ending date is before minimum duration (starting_date + recurring period)")
|
||||
# do not check this if we upgrade
|
||||
# if self.ending_date and self.ending_date < self.earliest_ending_date:
|
||||
# raise ValidationError("Ending date is before minimum duration (starting_date + recurring period)")
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
@ -853,11 +855,9 @@ class BillRecord(models.Model):
|
|||
|
||||
class Product(UncloudModel):
|
||||
"""
|
||||
A product is something a user orders. To record the pricing, we
|
||||
A product is something a user can order. To record the pricing, we
|
||||
create order that define a state in time.
|
||||
|
||||
A product can *depend* on other products.
|
||||
|
||||
A product can have *one* one_time_order and/or *one*
|
||||
recurring_order.
|
||||
|
||||
|
@ -872,12 +872,12 @@ class Product(UncloudModel):
|
|||
|
||||
description = "Generic Product"
|
||||
|
||||
orders = models.ManyToManyField(Order)
|
||||
|
||||
status = models.CharField(max_length=32,
|
||||
choices=UncloudStatus.choices,
|
||||
default=UncloudStatus.AWAITING_PAYMENT)
|
||||
|
||||
# config = models.JSONField()
|
||||
|
||||
# Default period for all products
|
||||
default_recurring_period = RecurringPeriod.PER_30D
|
||||
|
||||
|
@ -941,6 +941,7 @@ class Product(UncloudModel):
|
|||
self.orders.add(recurring_order)
|
||||
|
||||
|
||||
# FIXME: this could/should be part of Order (?)
|
||||
def create_or_update_recurring_order(self, when_to_start=None, recurring_period=None):
|
||||
if not self.recurring_price:
|
||||
return
|
||||
|
@ -954,8 +955,8 @@ class Product(UncloudModel):
|
|||
if self.last_recurring_order:
|
||||
if self.recurring_price < self.last_recurring_order.price:
|
||||
|
||||
if when_to_start < self.last_recurring_order.next_ending_date:
|
||||
when_to_start = start_after(self.last_recurring_order.next_ending_date)
|
||||
if when_to_start < self.last_recurring_order.next_cancel_or_downgrade_date:
|
||||
when_to_start = start_after(self.last_recurring_order.next_cancel_or_downgrade_date)
|
||||
|
||||
when_to_end = end_before(when_to_start)
|
||||
|
||||
|
@ -967,10 +968,9 @@ class Product(UncloudModel):
|
|||
description=str(self),
|
||||
replaces=self.last_recurring_order)
|
||||
|
||||
self.last_recurring_order.end_date = when_to_end
|
||||
self.last_recurring_order.replace_with(new_order)
|
||||
self.orders.add(new_order)
|
||||
else:
|
||||
# This might be a bug as it might (re-)create the one time order
|
||||
self.create_order(when_to_start, recurring_period)
|
||||
|
||||
@property
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from datetime import datetime, date, timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import *
|
||||
from uncloud_service.models import GenericServiceProduct
|
||||
|
@ -93,7 +94,9 @@ class ProductTestCase(TestCase):
|
|||
p = SampleRecurringProduct.objects.create(owner=self.user)
|
||||
p.create_order(timezone.make_aware(datetime.datetime(2020,3,3)))
|
||||
|
||||
p.create_or_update_recurring_order(timezone.make_aware(datetime.datetime(2020,3,3)))
|
||||
p.create_or_update_recurring_order(timezone.make_aware(datetime.datetime(2020,3,4)))
|
||||
|
||||
# FIXME: where is the assert?
|
||||
|
||||
|
||||
class BillingAddressTestCase(TestCase):
|
||||
|
@ -413,38 +416,96 @@ class ModifyProductTestCase(TestCase):
|
|||
self.assertNotEqual(bills[0].sum, pro_rata_amount * price)
|
||||
self.assertEqual(bills[0].sum, price)
|
||||
|
||||
def test_bill_for_increasing_product_easy(self):
|
||||
|
||||
def test_downgrade_product(self):
|
||||
"""
|
||||
Modify product, check general logi
|
||||
Test downgrading behaviour:
|
||||
|
||||
We create a recurring product (recurring time: 30 days) and downgrade after 15 days.
|
||||
|
||||
We create the bill right AFTER the end of the first order.
|
||||
|
||||
Expected result:
|
||||
|
||||
- First bill record for 30 days
|
||||
- Second bill record starting after 30 days
|
||||
- Bill contains two bill records
|
||||
|
||||
"""
|
||||
|
||||
# Create product
|
||||
starting_date = timezone.make_aware(datetime.datetime(2019,3,3))
|
||||
user = self.user
|
||||
|
||||
|
||||
|
||||
starting_price = 10
|
||||
product = SampleRecurringProduct.objects.create(owner=self.user,
|
||||
rc_price=starting_price)
|
||||
downgrade_price = 5
|
||||
|
||||
|
||||
starting_date = timezone.make_aware(datetime.datetime(2019,3,3))
|
||||
first_order_should_end_at = starting_date + datetime.timedelta(days=30)
|
||||
change1_date = start_after(starting_date + datetime.timedelta(days=15))
|
||||
bill_ending_date = change1_date + datetime.timedelta(days=1)
|
||||
|
||||
|
||||
product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price)
|
||||
product.create_order(starting_date)
|
||||
|
||||
change1_date = timezone.make_aware(datetime.datetime(2019,4,17))
|
||||
product.rc_price = 20
|
||||
product.rc_price = downgrade_price
|
||||
product.save()
|
||||
product.create_or_update_recurring_order(when_to_start=change1_date)
|
||||
|
||||
bill_ending_date = timezone.make_aware(datetime.datetime(2019,6,30))
|
||||
bills = Bill.create_next_bills_for_user(self.user,
|
||||
ending_date=bill_ending_date)
|
||||
bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date)
|
||||
|
||||
bill = bills[0]
|
||||
bill_records = BillRecord.objects.filter(bill=bill)
|
||||
|
||||
self.assertEqual(len(bill_records), 3)
|
||||
self.assertEqual(int(bill.sum), 35)
|
||||
self.assertEqual(len(bill_records), 2)
|
||||
|
||||
self.assertEqual(bill_records[0].starting_date, starting_date)
|
||||
|
||||
self.assertEqual(bill_records[0].order.ending_date, first_order_should_end_at)
|
||||
|
||||
# self.assertEqual(bill_records[0].ending_date, first_order_should_end_at)
|
||||
|
||||
# self.assertEqual(bill_records[0].quantity, 1)
|
||||
|
||||
# self.assertEqual(bill_records[1].quantity, 1)
|
||||
# self.assertEqual(int(bill.sum), 15)
|
||||
|
||||
def test_upgrade_product(self):
|
||||
"""
|
||||
Test upgrading behaviour
|
||||
"""
|
||||
|
||||
user = self.user
|
||||
|
||||
# Create product
|
||||
starting_date = timezone.make_aware(datetime.datetime(2019,3,3))
|
||||
starting_price = 10
|
||||
product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price)
|
||||
product.create_order(starting_date)
|
||||
|
||||
change1_date = start_after(starting_date + datetime.timedelta(days=15))
|
||||
product.rc_price = 20
|
||||
product.save()
|
||||
product.create_or_update_recurring_order(when_to_start=change1_date)
|
||||
|
||||
bill_ending_date = change1_date + datetime.timedelta(days=1)
|
||||
bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date)
|
||||
|
||||
bill = bills[0]
|
||||
bill_records = BillRecord.objects.filter(bill=bill)
|
||||
|
||||
self.assertEqual(len(bill_records), 2)
|
||||
self.assertEqual(bill_records[0].quantity, .5)
|
||||
|
||||
self.assertEqual(bill_records[0].ending_date, end_before(change1_date))
|
||||
|
||||
self.assertEqual(bill_records[1].quantity, 1)
|
||||
self.assertEqual(bill_records[1].starting_date, change1_date)
|
||||
|
||||
self.assertEqual(int(bill.sum), 25)
|
||||
|
||||
# Expected bill sum & records:
|
||||
# 2019-03-03 - 2019-04-02 +30d: 10
|
||||
# 2019-04-02 - 2019-04-17: +15d: 5
|
||||
# 2019-04-17 - 2019-05-17: +30d: 20
|
||||
# total: 35
|
||||
|
||||
|
||||
# def test_bill_for_increasing_product(self):
|
||||
|
|
21
uncloud_service/migrations/0005_auto_20200928_1844.py
Normal file
21
uncloud_service/migrations/0005_auto_20200928_1844.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_service', '0004_auto_20200809_1237'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='genericserviceproduct',
|
||||
name='orders',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='matrixserviceproduct',
|
||||
name='orders',
|
||||
),
|
||||
]
|
25
uncloud_vm/migrations/0005_auto_20200928_1844.py
Normal file
25
uncloud_vm/migrations/0005_auto_20200928_1844.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.1 on 2020-09-28 18:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_vm', '0004_auto_20200809_1237'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='vmdiskproduct',
|
||||
name='orders',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vmproduct',
|
||||
name='orders',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vmsnapshotproduct',
|
||||
name='orders',
|
||||
),
|
||||
]
|
Loading…
Reference in a new issue